Index: packagerEdge.py |
=================================================================== |
new file mode 100644 |
--- /dev/null |
+++ b/packagerEdge.py |
@@ -0,0 +1,165 @@ |
+# This Source Code Form is subject to the terms of the Mozilla Public |
+# License, v. 2.0. If a copy of the MPL was not distributed with this |
+# file, You can obtain one at http://mozilla.org/MPL/2.0/. |
+ |
+import base64 |
+import hashlib |
+import mimetypes |
+import os |
+import zipfile |
+ |
+import packager |
+import packagerChrome |
+ |
+MANIFEST = 'AppxManifest.xml' |
+CONTENT_TYPES = '[Content_Types].xml' |
+BLOCKMAP = 'AppxBlockMap.xml' |
+BLOCKSIZE = 64 * 1024 |
+ |
+ |
+def _get_template_for(filename): |
+ return packager.getTemplate('edge/{}.tmpl'.format(filename)) |
+ |
+ |
+def _lfh_size(filename): |
+ """Compute the size of zip local file header for `filename`.""" |
+ try: |
+ filename = filename.encode('utf-8') |
+ except UnicodeDecodeError: |
+ pass # filename is already a byte string. |
+ return zipfile.sizeFileHeader + len(filename) |
+ |
+ |
+def _make_blockmap_entry(filename, data): |
+ blocks = [data[i:i + BLOCKSIZE] for i in range(0, len(data), BLOCKSIZE)] |
+ return { |
+ 'name': filename.replace('/', '\\'), |
+ 'size': len(data), |
+ 'lfh_size': _lfh_size(filename), |
+ 'blocks': [ |
+ {'hash': base64.b64encode(hashlib.sha256(block).digest())} |
Sebastian Noack
2016/07/11 16:29:45
Wrapping the value into a dict seems unnecessary.
Vasily Kuznetsov
2016/07/12 09:38:09
The dictionary might also contain the `compressed_
|
+ for block in blocks |
+ ] |
+ } |
+ |
+ |
+def create_appx_blockmap(files): |
+ """Create APPX blockmap for the list of files.""" |
+ template = _get_template_for(BLOCKMAP) |
+ files = [_make_blockmap_entry(n, d) for n, d in files.items()] |
+ return template.render(files=files).encode('utf-8') |
+ |
+ |
+def create_appx_manifest(params, files): |
+ """Create AppxManifest.xml.""" |
+ template = _get_template_for(MANIFEST) |
+ params = dict(params) |
+ metadata = params['metadata'] |
+ params['package_identity'] = dict(metadata.items('package_identity')) |
+ w = params['windows_version'] = {} |
+ w['min'], w['max'] = metadata.get('compat', 'windows').split('/') |
+ params.update(metadata.items('general')) |
+ for size in ['44', '50', '150']: |
+ path = 'Assets/logo_{}.png'.format(size) |
+ if path not in files: |
+ raise KeyError('{} is not found in files'.format(path)) |
Sebastian Noack
2016/07/11 16:29:45
Use the + operator here?
Vasily Kuznetsov
2016/07/12 09:38:09
Done.
|
+ params['logo_' + size] = path.replace('/', '\\') |
+ return template.render(params).encode('utf-8') |
+ |
+ |
+def move_files_to_extension(files): |
+ """Move all files into `Extension` folder for APPX packaging.""" |
+ # We sort the files to ensure that 'Extension/xyz' is moved before 'xyz'. |
+ # If 'xyz' is moved first, it would overwrite 'Extension/xyz' and its |
+ # original content would be lost. |
+ names = sorted(files.keys(), key=len, reverse=True) |
+ for filename in names: |
+ files['Extension/' + filename] = files.pop(filename) |
+ |
+ |
+def create_content_types_map(files): |
+ """Create [Content_Types].xml -- a mime type map.""" |
+ # We always have a manifest and a blockmap, so we include those by default. |
+ params = { |
+ 'defaults': { |
+ 'xml': 'text/xml' |
Sebastian Noack
2016/07/11 16:29:44
Isn't that overridden below?
Vasily Kuznetsov
2016/07/12 09:38:08
Indeed it is, but since I'm not sure how the conte
Sebastian Noack
2016/07/12 21:15:25
I mean, isn't the value here overridden by the log
Vasily Kuznetsov
2016/07/13 10:35:31
My thinking was that we are normally adding the co
Sebastian Noack
2016/07/13 16:29:22
Do we even need to consider AppxBlockMap.xml and A
Vasily Kuznetsov
2016/07/13 17:36:45
Yeah, this is possible, but we'll later have other
Sebastian Noack
2016/07/13 17:42:39
The package is signed by the Windows Store. In fac
|
+ }, |
+ 'overrides': { |
+ '/AppxBlockMap.xml': 'application/vnd.ms-appx.blockmap+xml', |
+ '/AppxManifest.xml': 'application/vnd.ms-appx.manifest+xml' |
+ } |
+ } |
+ for filename in files: |
+ if '.' in filename[1:]: |
+ ext = filename.split('.')[-1] |
Sebastian Noack
2016/07/11 16:29:45
Use os.path.splitext?
Vasily Kuznetsov
2016/07/12 09:38:10
Done.
|
+ content_type = mimetypes.guess_type(filename, strict=False) |
+ # The return value can be a string or a tuple: |
Sebastian Noack
2016/07/11 16:29:44
Can it? I think it is always a tuple, or do I miss
Vasily Kuznetsov
2016/07/12 09:38:09
"The return value is a tuple (type, encoding) wher
|
+ # https://docs.python.org/2/library/mimetypes.html#mimetypes.guess_type |
+ if isinstance(content_type, tuple): |
+ content_type = content_type[0] |
+ if content_type is not None: |
+ params['defaults'][ext] = content_type |
+ content_types_template = _get_template_for(CONTENT_TYPES) |
+ return content_types_template.render(params).encode('utf-8') |
+ |
+ |
+def createBuild(baseDir, type='edge', outFile=None, # noqa: preserve API. |
+ buildNum=None, releaseBuild=False, keyFile=None, |
+ devenv=False): |
+ |
+ metadata = packager.readMetadata(baseDir, type) |
+ version = packager.getBuildVersion(baseDir, metadata, releaseBuild, |
+ buildNum) |
+ |
+ outfile = outFile or packager.getDefaultFileName(metadata, version, 'appx') |
+ |
+ params = { |
+ 'type': type, |
+ 'baseDir': baseDir, |
+ 'releaseBuild': releaseBuild, |
+ 'version': version, |
+ 'devenv': devenv, |
+ 'metadata': metadata, |
+ } |
+ |
+ files = packager.Files(packagerChrome.getPackageFiles(params), |
+ packagerChrome.getIgnoredFiles(params)) |
+ |
+ if metadata.has_section('mapping'): |
+ mapped = metadata.items('mapping') |
+ files.readMappedFiles(mapped) |
+ files.read(baseDir, skip=[filename for filename, _ in mapped]) |
+ else: |
+ files.read(baseDir) |
+ |
+ if metadata.has_section('convert_js'): |
+ packagerChrome.convertJS(params, files) |
+ |
+ if metadata.has_section('preprocess'): |
+ files.preprocess(metadata.options('preprocess'), {'needsExt': True}) |
+ |
+ if metadata.has_section('import_locales'): |
+ packagerChrome.importGeckoLocales(params, files) |
+ |
+ files['manifest.json'] = packagerChrome.createManifest(params, files) |
+ |
+ move_files_to_extension(files) |
+ |
+ if metadata.has_section('appx_assets'): |
+ for name, path in metadata.items('appx_assets'): |
+ path = os.path.join(baseDir, path) |
+ files.read(path, 'Assets/' + name) |
+ |
+ files[MANIFEST] = create_appx_manifest(params, files) |
+ files[CONTENT_TYPES] = create_content_types_map(files) |
+ |
+ # We don't support AppxBlockmap.xml generation for compressed zip files at |
+ # the moment. The only way to reliably calculate the compressed size of |
+ # each 64k chunk in the zip file is to override the relevant parts of |
+ # `zipfile` library. We have chosen to not do it for now, so we produce |
+ # an uncompressed zip file. |
+ files[BLOCKMAP] = create_appx_blockmap(files) |
+ |
+ # TODO: Implement AppxBlockmap.xml generation for compressed zip files. |
+ # https://issues.adblockplus.org/ticket/4149 |
+ files.zip(outfile, compress=False) |