Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: packagerEdge.py

Issue 29825555: Issue 6291 - add ManifoldJS packaging for Edge (Closed) Base URL: https://hg.adblockplus.org/buildtools/file/9a56d76cd951
Patch Set: Created July 19, 2018, 12:26 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
« no previous file with comments | « package.json ('k') | templates/edge/AppxBlockMap.xml.tmpl » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 # This Source Code Form is subject to the terms of the Mozilla Public 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 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/. 3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 4
5 import base64
6 import hashlib
7 import json
8 import mimetypes
9 import os 5 import os
10 import zipfile 6 import shutil
7 from StringIO import StringIO
8 import subprocess
9 import tempfile
10 from xml.etree import ElementTree
11 from zipfile import ZipFile
11 12
12 import packager 13 import packager
13 import packagerChrome 14 import packagerChrome
14 15
15 # Files and directories expected inside of the .APPX archive. 16 MANIFEST = 'appxmanifest.xml'
16 MANIFEST = 'AppxManifest.xml'
17 CONTENT_TYPES = '[Content_Types].xml'
18 BLOCKMAP = 'AppxBlockMap.xml'
19 EXTENSION_DIR = 'Extension'
20 ASSETS_DIR = 'Assets' 17 ASSETS_DIR = 'Assets'
21 18
22 # Size of uncompressed block in the APPX block map.
23 BLOCKSIZE = 64 * 1024
24
25 defaultLocale = packagerChrome.defaultLocale 19 defaultLocale = packagerChrome.defaultLocale
26 20
27 21
28 def _get_template_for(filename): 22 def _get_template_for(filename):
29 return packager.getTemplate('edge/{}.tmpl'.format(filename)) 23 return packager.getTemplate('edge/{}.tmpl'.format(filename))
30 24
31 25
32 def _lfh_size(filename): 26 def register_xml_namespaces(manifest_path):
33 """Compute the size of zip local file header for `filename`.""" 27 """Register namespaces of the given file, in order to preserve defaults."""
34 try: 28 with open(manifest_path, 'r') as fp:
35 filename = filename.encode('utf-8') 29 ns = dict([node for _, node in ElementTree.iterparse(
36 except UnicodeDecodeError: 30 fp, events=['start-ns'])])
37 pass # filename is already a byte string. 31 for prefix, uri in ns.items():
38 return zipfile.sizeFileHeader + len(filename) 32 ElementTree.register_namespace(prefix, uri)
33
34 return ns
39 35
40 36
41 def _make_blockmap_entry(filename, data): 37 def update_appx_manifest(manifest_path, base_dir, files, metadata,
42 blocks = [data[i:i + BLOCKSIZE] for i in range(0, len(data), BLOCKSIZE)] 38 release_build):
43 return { 39 namespaces = register_xml_namespaces(manifest_path)
44 'name': filename.replace('/', '\\'), 40
45 'size': len(data), 41 def traverse(current_elem, overwrite):
46 'lfh_size': _lfh_size(filename), 42 if isinstance(overwrite, dict):
47 'blocks': [ 43 for key, value in overwrite.items():
48 {'hash': base64.b64encode(hashlib.sha256(block).digest())} 44 if isinstance(key, tuple):
49 for block in blocks 45 prefix, element = key
50 ], 46 next_elem = current_elem.find(
47 '{{{}}}{}'.format(namespaces[prefix], element))
48 traverse(next_elem, value)
49 else:
50 current_elem.attrib.update(overwrite)
51 else:
52 current_elem.text = overwrite
53
54 v_min, v_max = metadata.get('compat', 'windows').split('/')
55
56 filenames = []
57 if metadata.has_section('appx_assets'):
Sebastian Noack 2018/07/25 19:18:41 This check seems redundant as we fail anyway below
tlucas 2018/08/08 09:35:55 Done.
58 for name, path in metadata.items('appx_assets'):
59 path = os.path.join(base_dir, path)
60 icon_path = '{}/{}'.format(ASSETS_DIR, name)
61
62 files.read(path, icon_path)
63 filenames.append(icon_path)
64
65 assets = packagerChrome.makeIcons(files, filenames)
66
67 overwrite = {
68 ('', 'Identity'): {
69 'Name': packager.get_app_id(release_build, metadata),
70 'Publisher': metadata.get('general', 'publisher_id'),
71 },
72 ('', 'Properties'): {
73 ('', 'PublisherDisplayName'): metadata.get('general', 'author'),
74 ('', 'Logo'): assets[50],
75 },
76 ('', 'Dependencies'): {
77 ('', 'TargetDeviceFamily'): {
78 'MaxVersionTested': v_max,
79 'MinVersion': v_min,
80 },
81 },
82 ('', 'Applications'): {
83 ('', 'Application'): {
84 ('uap', 'VisualElements'): {
85 'Square150x150Logo': assets[150],
86 'Square44x44Logo': assets[44],
87 },
88 },
89 },
51 } 90 }
52 91
92 tree = ElementTree.parse(manifest_path)
93 root = tree.getroot()
53 94
54 def create_appx_blockmap(files): 95 traverse(root, overwrite)
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 96
66 97 tree.write(manifest_path, encoding='utf-8', xml_declaration=True)
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 get_appx_version(metadata, build_num):
74 """Get the version number for usage in AppxManifest.xml.
75
76 As required by the Windows Store, the returned version string has four
77 components, where the 3rd component is replaced with the build number
78 if available, and the 4th component is always zero (e.g. 1.2.1000.0).
79 """
80 components = metadata.get('general', 'version').split('.')[:3]
81 components.extend(['0'] * (4 - len(components)))
82 if build_num:
83 components[2] = build_num
84 return '.'.join(components)
85
86
87 def create_appx_manifest(params, files, build_num, release_build):
88 """Create AppxManifest.xml."""
89 params = dict(params)
90 metadata = params['metadata']
91 w = params['windows_version'] = {}
92 w['min'], w['max'] = metadata.get('compat', 'windows').split('/')
93 params['version'] = get_appx_version(metadata, build_num)
94
95 metadata_suffix = 'release' if release_build else 'devbuild'
96 app_extension_id = 'extension_id_' + metadata_suffix
97 if metadata.has_option('general', app_extension_id):
98 params['app_extension_id'] = metadata.get('general', app_extension_id)
99 else:
100 params['app_extension_id'] = 'EdgeExtension'
101
102 params['app_id'] = packager.get_app_id(release_build, metadata)
103
104 translation = load_translation(files, defaultLocale)
105 name_key = 'name' if release_build else 'name_devbuild'
106 params['display_name'] = translation[name_key]['message']
107 params['description'] = translation['description']['message']
108
109 for size in ['44', '50', '150']:
110 path = '{}/logo_{}.png'.format(ASSETS_DIR, size)
111 if path not in files:
112 raise KeyError(path + ' is not found in files')
113 params['logo_' + size] = path.replace('/', '\\')
114
115 template = _get_template_for(MANIFEST)
116 return template.render(params).encode('utf-8')
117
118
119 def move_files_to_extension(files):
120 """Move all files into `Extension` folder for APPX packaging."""
121 # We sort the files to ensure that 'Extension/xyz' is moved before 'xyz'.
122 # If 'xyz' is moved first, it would overwrite 'Extension/xyz' and its
123 # original content would be lost.
124 names = sorted(files.keys(), key=len, reverse=True)
125 for filename in names:
126 files['{}/{}'.format(EXTENSION_DIR, filename)] = files.pop(filename)
127
128
129 def create_content_types_map(filenames):
130 """Create [Content_Types].xml -- a mime type map."""
131 params = {'defaults': {}, 'overrides': {}}
132 overrides = {
133 BLOCKMAP: 'application/vnd.ms-appx.blockmap+xml',
134 MANIFEST: 'application/vnd.ms-appx.manifest+xml',
135 }
136 types = mimetypes.MimeTypes()
137 types.add_type('application/octet-stream', '.otf')
138 for filename in filenames:
139 ext = os.path.splitext(filename)[1]
140 if ext:
141 content_type = types.guess_type(filename, strict=False)[0]
142 if content_type is not None:
143 params['defaults'][ext[1:]] = content_type
144 if filename in overrides:
145 params['overrides']['/' + filename] = overrides[filename]
146 content_types_template = _get_template_for(CONTENT_TYPES)
147 return content_types_template.render(params).encode('utf-8')
148 98
149 99
150 def createBuild(baseDir, type='edge', outFile=None, # noqa: preserve API. 100 def createBuild(baseDir, type='edge', outFile=None, # noqa: preserve API.
151 buildNum=None, releaseBuild=False, keyFile=None, 101 buildNum=None, releaseBuild=False, keyFile=None,
152 devenv=False): 102 devenv=False):
153 103
154 metadata = packager.readMetadata(baseDir, type) 104 metadata = packager.readMetadata(baseDir, type)
155 version = packager.getBuildVersion(baseDir, metadata, releaseBuild, 105 version = packager.getBuildVersion(baseDir, metadata, releaseBuild,
156 buildNum) 106 buildNum)
157 107
(...skipping 26 matching lines...) Expand all
184 files.preprocess(metadata.options('preprocess'), {'needsExt': True}) 134 files.preprocess(metadata.options('preprocess'), {'needsExt': True})
185 135
186 if metadata.has_section('import_locales'): 136 if metadata.has_section('import_locales'):
187 packagerChrome.import_locales(params, files) 137 packagerChrome.import_locales(params, files)
188 138
189 files['manifest.json'] = packagerChrome.createManifest(params, files) 139 files['manifest.json'] = packagerChrome.createManifest(params, files)
190 140
191 if devenv: 141 if devenv:
192 packagerChrome.add_devenv_requirements(files, metadata, params) 142 packagerChrome.add_devenv_requirements(files, metadata, params)
193 143
194 move_files_to_extension(files) 144 zipped = StringIO()
145 files.zip(zipped)
195 146
196 if metadata.has_section('appx_assets'): 147 zipped.seek(0)
197 for name, path in metadata.items('appx_assets'):
198 path = os.path.join(baseDir, path)
199 files.read(path, '{}/{}'.format(ASSETS_DIR, name))
200 148
201 files[MANIFEST] = create_appx_manifest(params, files, 149 if devenv:
202 buildNum, releaseBuild) 150 shutil.copyfileobj(zipped, outfile)
203 files[BLOCKMAP] = create_appx_blockmap(files) 151 return
204 files[CONTENT_TYPES] = create_content_types_map(files.keys() + [BLOCKMAP])
205 152
206 files.zip(outfile, compression=zipfile.ZIP_STORED) 153 tmp_dir = tempfile.mkdtemp('adblockplus_package')
154 try:
155 src_dir = os.path.join(tmp_dir, 'src')
156 ext_dir = os.path.join(tmp_dir, 'ext')
157
158 with ZipFile(zipped, 'r') as zip_file:
159 zip_file.extractall(src_dir)
160
161 cmd_env = os.environ.copy()
162 cmd_env['SRC_FOLDER'] = src_dir
163 cmd_env['EXT_FOLDER'] = ext_dir
164
165 manifold_folder = os.path.join(ext_dir, 'MSGname', 'edgeextension')
166 manifest_folder = os.path.join(manifold_folder, 'manifest')
167 asset_folder = os.path.join(manifest_folder, ASSETS_DIR)
168
169 # prepare the extension with manifoldjs
170 cmd = ['npm', 'run', '--silent', 'build-edge']
171 subprocess.check_call(cmd, env=cmd_env, cwd=os.path.dirname(__file__))
172
173 # update incomplete appxmanifest
174 intermediate_manifest = os.path.join(manifest_folder, MANIFEST)
175 update_appx_manifest(intermediate_manifest, baseDir, files, metadata,
176 releaseBuild)
177
178 # cleanup placeholders, copy actual images
179 shutil.rmtree(asset_folder)
180 os.mkdir(asset_folder)
181 if metadata.has_section('appx_assets'):
182 for name, path in metadata.items('appx_assets'):
183 path = os.path.join(baseDir, path)
184 target = os.path.join(asset_folder, name)
185 shutil.copyfile(path, target)
186
187 # package app with manifoldjs
188 cmd = ['npm', 'run', '--silent', 'package-edge']
189
190 subprocess.check_call(cmd, env=cmd_env, cwd=os.path.dirname(__file__))
191
192 package = os.path.join(manifold_folder, 'package',
193 'edgeExtension.appx')
194
195 shutil.copyfile(package, outfile)
196 finally:
197 shutil.rmtree(tmp_dir, ignore_errors=True)
OLDNEW
« no previous file with comments | « package.json ('k') | templates/edge/AppxBlockMap.xml.tmpl » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld