| OLD | NEW | 
| (Empty) |  | 
 |    1 # This Source Code Form is subject to the terms of the Mozilla Public | 
 |    2 # License, v. 2.0. If a copy of the MPL was not distributed with this | 
 |    3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. | 
 |    4  | 
 |    5 import base64 | 
 |    6 import hashlib | 
 |    7 import json | 
 |    8 import mimetypes | 
 |    9 import os | 
 |   10 import zipfile | 
 |   11  | 
 |   12 import packager | 
 |   13 import packagerChrome | 
 |   14  | 
 |   15 # Files and directories expected inside of the .APPX archive. | 
 |   16 MANIFEST = 'AppxManifest.xml' | 
 |   17 CONTENT_TYPES = '[Content_Types].xml' | 
 |   18 BLOCKMAP = 'AppxBlockMap.xml' | 
 |   19 EXTENSION_DIR = 'Extension' | 
 |   20 ASSETS_DIR = 'Assets' | 
 |   21  | 
 |   22 # Size of uncompressed block in the APPX block map. | 
 |   23 BLOCKSIZE = 64 * 1024 | 
 |   24  | 
 |   25 defaultLocale = packagerChrome.defaultLocale | 
 |   26  | 
 |   27  | 
 |   28 def _get_template_for(filename): | 
 |   29     return packager.getTemplate('edge/{}.tmpl'.format(filename)) | 
 |   30  | 
 |   31  | 
 |   32 def _lfh_size(filename): | 
 |   33     """Compute the size of zip local file header for `filename`.""" | 
 |   34     try: | 
 |   35         filename = filename.encode('utf-8') | 
 |   36     except UnicodeDecodeError: | 
 |   37         pass  # filename is already a byte string. | 
 |   38     return zipfile.sizeFileHeader + len(filename) | 
 |   39  | 
 |   40  | 
 |   41 def _make_blockmap_entry(filename, data): | 
 |   42     blocks = [data[i:i + BLOCKSIZE] for i in range(0, len(data), BLOCKSIZE)] | 
 |   43     return { | 
 |   44         'name': filename.replace('/', '\\'), | 
 |   45         'size': len(data), | 
 |   46         'lfh_size': _lfh_size(filename), | 
 |   47         'blocks': [ | 
 |   48             {'hash': base64.b64encode(hashlib.sha256(block).digest())} | 
 |   49             for block in blocks | 
 |   50         ] | 
 |   51     } | 
 |   52  | 
 |   53  | 
 |   54 def create_appx_blockmap(files): | 
 |   55     """Create APPX blockmap for the list of files.""" | 
 |   56     # We don't support AppxBlockmap.xml generation for compressed zip files at | 
 |   57     # the moment. The only way to reliably calculate the compressed size of | 
 |   58     # each 64k chunk in the zip file is to override the relevant parts of | 
 |   59     # `zipfile` library. We have chosen to not do it so we produce an | 
 |   60     # uncompressed zip file that is later repackaged by Windows Store with | 
 |   61     # compression. | 
 |   62     template = _get_template_for(BLOCKMAP) | 
 |   63     files = [_make_blockmap_entry(n, d) for n, d in files.items()] | 
 |   64     return template.render(files=files).encode('utf-8') | 
 |   65  | 
 |   66  | 
 |   67 def load_translation(files, locale): | 
 |   68     """Load translation strings for locale from files.""" | 
 |   69     path = '{}/_locales/{}/messages.json'.format(EXTENSION_DIR, locale) | 
 |   70     return json.loads(files[path]) | 
 |   71  | 
 |   72  | 
 |   73 def pad_version(version): | 
 |   74     """Make sure version number has 4 groups of digits.""" | 
 |   75     groups = (version.split('.') + ['0', '0', '0'])[:4] | 
 |   76     return '.'.join(groups) | 
 |   77  | 
 |   78  | 
 |   79 def create_appx_manifest(params, files, release_build=False): | 
 |   80     """Create AppxManifest.xml.""" | 
 |   81     params = dict(params) | 
 |   82     metadata = params['metadata'] | 
 |   83     w = params['windows_version'] = {} | 
 |   84     w['min'], w['max'] = metadata.get('compat', 'windows').split('/') | 
 |   85     params.update(metadata.items('general')) | 
 |   86     params['version'] = pad_version(params['version']) | 
 |   87  | 
 |   88     translation = load_translation(files, defaultLocale) | 
 |   89     name_key = 'name' if release_build else 'name_devbuild' | 
 |   90     params['display_name'] = translation[name_key]['message'] | 
 |   91     params['description'] = translation['description']['message'] | 
 |   92  | 
 |   93     for size in ['44', '50', '150']: | 
 |   94         path = '{}/logo_{}.png'.format(ASSETS_DIR, size) | 
 |   95         if path not in files: | 
 |   96             raise KeyError(path + 'is not found in files') | 
 |   97         params['logo_' + size] = path.replace('/', '\\') | 
 |   98  | 
 |   99     template = _get_template_for(MANIFEST) | 
 |  100     return template.render(params).encode('utf-8') | 
 |  101  | 
 |  102  | 
 |  103 def move_files_to_extension(files): | 
 |  104     """Move all files into `Extension` folder for APPX packaging.""" | 
 |  105     # We sort the files to ensure that 'Extension/xyz' is moved before 'xyz'. | 
 |  106     # If 'xyz' is moved first, it would overwrite 'Extension/xyz' and its | 
 |  107     # original content would be lost. | 
 |  108     names = sorted(files.keys(), key=len, reverse=True) | 
 |  109     for filename in names: | 
 |  110         files['{}/{}'.format(EXTENSION_DIR, filename)] = files.pop(filename) | 
 |  111  | 
 |  112  | 
 |  113 def create_content_types_map(filenames): | 
 |  114     """Create [Content_Types].xml -- a mime type map.""" | 
 |  115     params = {'defaults': {}, 'overrides': {}} | 
 |  116     overrides = { | 
 |  117         BLOCKMAP: 'application/vnd.ms-appx.blockmap+xml', | 
 |  118         MANIFEST: 'application/vnd.ms-appx.manifest+xml' | 
 |  119     } | 
 |  120     for filename in filenames: | 
 |  121         ext = os.path.splitext(filename)[1] | 
 |  122         if ext: | 
 |  123             content_type = mimetypes.guess_type(filename, strict=False)[0] | 
 |  124             if content_type is not None: | 
 |  125                 params['defaults'][ext[1:]] = content_type | 
 |  126         if filename in overrides: | 
 |  127             params['overrides']['/' + filename] = overrides[filename] | 
 |  128     content_types_template = _get_template_for(CONTENT_TYPES) | 
 |  129     return content_types_template.render(params).encode('utf-8') | 
 |  130  | 
 |  131  | 
 |  132 def createBuild(baseDir, type='edge', outFile=None,  # noqa: preserve API. | 
 |  133                 buildNum=None, releaseBuild=False, keyFile=None, | 
 |  134                 devenv=False): | 
 |  135  | 
 |  136     metadata = packager.readMetadata(baseDir, type) | 
 |  137     version = packager.getBuildVersion(baseDir, metadata, releaseBuild, | 
 |  138                                        buildNum) | 
 |  139  | 
 |  140     outfile = outFile or packager.getDefaultFileName(metadata, version, 'appx') | 
 |  141  | 
 |  142     params = { | 
 |  143         'type': type, | 
 |  144         'baseDir': baseDir, | 
 |  145         'releaseBuild': releaseBuild, | 
 |  146         'version': version, | 
 |  147         'devenv': devenv, | 
 |  148         'metadata': metadata, | 
 |  149     } | 
 |  150  | 
 |  151     files = packager.Files(packagerChrome.getPackageFiles(params), | 
 |  152                            packagerChrome.getIgnoredFiles(params)) | 
 |  153  | 
 |  154     if metadata.has_section('mapping'): | 
 |  155         mapped = metadata.items('mapping') | 
 |  156         files.readMappedFiles(mapped) | 
 |  157         files.read(baseDir, skip=[filename for filename, _ in mapped]) | 
 |  158     else: | 
 |  159         files.read(baseDir) | 
 |  160  | 
 |  161     if metadata.has_section('convert_js'): | 
 |  162         packagerChrome.convertJS(params, files) | 
 |  163  | 
 |  164     if metadata.has_section('preprocess'): | 
 |  165         files.preprocess(metadata.options('preprocess'), {'needsExt': True}) | 
 |  166  | 
 |  167     if metadata.has_section('import_locales'): | 
 |  168         packagerChrome.importGeckoLocales(params, files) | 
 |  169  | 
 |  170     files['manifest.json'] = packagerChrome.createManifest(params, files) | 
 |  171  | 
 |  172     move_files_to_extension(files) | 
 |  173  | 
 |  174     if metadata.has_section('appx_assets'): | 
 |  175         for name, path in metadata.items('appx_assets'): | 
 |  176             path = os.path.join(baseDir, path) | 
 |  177             files.read(path, '{}/{}'.format(ASSETS_DIR, name)) | 
 |  178  | 
 |  179     files[MANIFEST] = create_appx_manifest(params, files, releaseBuild) | 
 |  180     files[BLOCKMAP] = create_appx_blockmap(files) | 
 |  181     files[CONTENT_TYPES] = create_content_types_map(files.keys() + [BLOCKMAP]) | 
 |  182  | 
 |  183     files.zip(outfile, compression=zipfile.ZIP_STORED) | 
| OLD | NEW |