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 12, 2018, 9:24 a.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 5 import json
8 import mimetypes
9 import os 6 import os
10 import zipfile 7 import shutil
8 from StringIO import StringIO
9 import subprocess
10 import tempfile
11 from xml.etree import ElementTree
12 from zipfile import ZipFile
11 13
12 import packager 14 import packager
13 import packagerChrome 15 import packagerChrome
14 16
15 # Files and directories expected inside of the .APPX archive. 17 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'
21
22 # Size of uncompressed block in the APPX block map.
23 BLOCKSIZE = 64 * 1024
24 18
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):
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): 26 def load_translation(files, locale):
68 """Load translation strings for locale from files.""" 27 """Load translation strings for locale from files."""
69 path = '{}/_locales/{}/messages.json'.format(EXTENSION_DIR, locale) 28 path = '_locales/{}/messages.json'.format(locale)
70 return json.loads(files[path]) 29 return json.loads(files[path])
71 30
72 31
73 def get_appx_version(metadata, build_num): 32 def register_xml_namespaces(manifest_path):
74 """Get the version number for usage in AppxManifest.xml. 33 """Register namespaces of the given file, in order to preserve defaults."""
34 with open(manifest_path, 'r') as fp:
35 ns = dict([node for _, node in ElementTree.iterparse(
36 fp, events=['start-ns'])])
37 for prefix, uri in ns.items():
38 ElementTree.register_namespace(prefix, uri)
75 39
76 As required by the Windows Store, the returned version string has four 40 return ns
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 41
86 42
87 def create_appx_manifest(params, files, build_num, release_build): 43 def update_appx_manifest(manifest_path, files, metadata, release_build):
88 """Create AppxManifest.xml.""" 44 namespaces = register_xml_namespaces(manifest_path)
Sebastian Noack 2018/07/12 14:43:10 I wonder if this is necessary? What happens if you
tlucas 2018/07/19 12:30:27 This is necessary so that xml.etree.ElementTree.wr
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 45
95 metadata_suffix = 'release' if release_build else 'devbuild' 46 def traverse(current_elem, overwrite):
96 app_extension_id = 'extension_id_' + metadata_suffix 47 if isinstance(overwrite, dict):
97 if metadata.has_option('general', app_extension_id): 48 for key, value in overwrite.items():
98 params['app_extension_id'] = metadata.get('general', app_extension_id) 49 if isinstance(key, tuple):
99 else: 50 prefix, element = key
100 params['app_extension_id'] = 'EdgeExtension' 51 next_elem = current_elem.find(
101 52 '{{{}}}{}'.format(namespaces[prefix], element))
102 params['app_id'] = packager.get_app_id(release_build, metadata) 53 traverse(next_elem, value)
54 else:
55 current_elem.attrib.update(overwrite)
56 else:
57 current_elem.text = overwrite
103 58
104 translation = load_translation(files, defaultLocale) 59 translation = load_translation(files, defaultLocale)
105 name_key = 'name' if release_build else 'name_devbuild' 60 name_key = 'name' if release_build else 'name_devbuild'
106 params['display_name'] = translation[name_key]['message'] 61 v_min, v_max = metadata.get('compat', 'windows').split('/')
107 params['description'] = translation['description']['message']
108 62
109 for size in ['44', '50', '150']: 63 overwrite = {
Sebastian Noack 2018/07/12 14:43:10 Instead of a dictionaries you might want to use li
tlucas 2018/07/19 12:30:28 Since order doesn't matter in the resulting XML (o
Sebastian Noack 2018/07/25 19:18:41 Originally I thought that nodes may be created for
tlucas 2018/08/08 09:35:54 It looks like adding the default namespaces with a
110 path = '{}/logo_{}.png'.format(ASSETS_DIR, size) 64 ('', 'Identity'): {
111 if path not in files: 65 'Name': packager.get_app_id(release_build, metadata),
112 raise KeyError(path + ' is not found in files') 66 'Publisher': metadata.get('general', 'publisher_id'),
113 params['logo_' + size] = path.replace('/', '\\') 67 },
68 ('', 'Properties'): {
69 ('', 'DisplayName'): translation[name_key]['message'],
Sebastian Noack 2018/07/12 14:43:10 It seems we can just leave the DisplayName alone.
tlucas 2018/07/19 12:30:27 True indeed, removed this.
70 ('', 'PublisherDisplayName'): metadata.get('general', 'author'),
71 ('', 'Logo'): 'Assets/logo_50.png',
72 },
73 ('', 'Dependencies'): {
74 ('', 'TargetDeviceFamily'): {
75 'MaxVersionTested': v_max,
76 'MinVersion': v_min,
77 },
78 },
79 ('', 'Applications'): {
80 ('', 'Application'): {
81 ('uap', 'VisualElements'): {
Sebastian Noack 2018/07/12 14:43:10 Perhaps this structure is more readable if keys ar
tlucas 2018/07/19 12:30:27 You may be right about it being (only imho) a bit
Sebastian Noack 2018/07/25 19:18:41 What I meant is using "uap:VisualElements" as key
tlucas 2018/08/08 09:35:54 Done.
82 'Square150x150Logo': 'Assets/logo_150.png',
83 'Square44x44Logo': 'Assets/logo_44.png',
Sebastian Noack 2018/07/12 14:43:10 Instead of hard-coding the assets, we could identi
tlucas 2018/07/19 12:30:27 Done.
84 },
85 },
86 },
87 }
114 88
115 template = _get_template_for(MANIFEST) 89 tree = ElementTree.parse(manifest_path)
116 return template.render(params).encode('utf-8') 90 root = tree.getroot()
117 91
92 traverse(root, overwrite)
118 93
119 def move_files_to_extension(files): 94 tree.write(manifest_path, encoding='utf-8', xml_declaration=True)
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 95
149 96
150 def createBuild(baseDir, type='edge', outFile=None, # noqa: preserve API. 97 def createBuild(baseDir, type='edge', outFile=None, # noqa: preserve API.
151 buildNum=None, releaseBuild=False, keyFile=None, 98 buildNum=None, releaseBuild=False, keyFile=None,
152 devenv=False): 99 devenv=False):
153 100
154 metadata = packager.readMetadata(baseDir, type) 101 metadata = packager.readMetadata(baseDir, type)
155 version = packager.getBuildVersion(baseDir, metadata, releaseBuild, 102 version = packager.getBuildVersion(baseDir, metadata, releaseBuild,
156 buildNum) 103 buildNum)
157 104
(...skipping 26 matching lines...) Expand all
184 files.preprocess(metadata.options('preprocess'), {'needsExt': True}) 131 files.preprocess(metadata.options('preprocess'), {'needsExt': True})
185 132
186 if metadata.has_section('import_locales'): 133 if metadata.has_section('import_locales'):
187 packagerChrome.import_locales(params, files) 134 packagerChrome.import_locales(params, files)
188 135
189 files['manifest.json'] = packagerChrome.createManifest(params, files) 136 files['manifest.json'] = packagerChrome.createManifest(params, files)
190 137
191 if devenv: 138 if devenv:
192 packagerChrome.add_devenv_requirements(files, metadata, params) 139 packagerChrome.add_devenv_requirements(files, metadata, params)
193 140
194 move_files_to_extension(files) 141 zipped = StringIO()
142 files.zip(zipped)
195 143
196 if metadata.has_section('appx_assets'): 144 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 145
201 files[MANIFEST] = create_appx_manifest(params, files, 146 if devenv:
202 buildNum, releaseBuild) 147 shutil.copyfileobj(zipped, outfile)
203 files[BLOCKMAP] = create_appx_blockmap(files) 148 return
204 files[CONTENT_TYPES] = create_content_types_map(files.keys() + [BLOCKMAP])
205 149
206 files.zip(outfile, compression=zipfile.ZIP_STORED) 150 tmp_dir = tempfile.mkdtemp('adblockplus_package')
151 try:
152 src_dir = os.path.join(tmp_dir, 'src')
153 ext_dir = os.path.join(tmp_dir, 'ext')
154
155 with ZipFile(zipped, 'r') as zip_file:
156 zip_file.extractall(src_dir)
157
158 cmd_env = os.environ.copy()
159 cmd_env['SRC_FOLDER'] = src_dir
160 cmd_env['EXT_FOLDER'] = ext_dir
161
162 manifold_folder = os.path.join(ext_dir, 'MSGname', 'edgeextension')
163 manifest_folder = os.path.join(manifold_folder, 'manifest')
164 asset_folder = os.path.join(manifest_folder, 'Assets')
165
166 # prepare the extension with manifoldjs
167 cmd = ['npm', 'run', '--silent', 'build-edge']
168 subprocess.check_call(cmd, env=cmd_env, cwd=os.path.dirname(__file__))
169
170 # update incomplete appxmanifest
171 intermediate_manifest = os.path.join(manifest_folder, MANIFEST)
172 update_appx_manifest(intermediate_manifest, files, metadata,
173 releaseBuild)
174
175 # cleanup placeholders, copy actual images
176 shutil.rmtree(asset_folder)
177 os.mkdir(asset_folder)
178 if metadata.has_section('appx_assets'):
179 for name, path in metadata.items('appx_assets'):
180 path = os.path.join(baseDir, path)
181 target = os.path.join(asset_folder, name)
182 shutil.copyfile(path, target)
183
184 # package app with manifoldjs
185 cmd = ['npm', 'run', '--silent', 'package-edge']
186
187 subprocess.check_call(cmd, env=cmd_env, cwd=os.path.dirname(__file__))
188
189 package = os.path.join(manifold_folder, 'package',
190 'edgeExtension.appx')
191
192 shutil.copyfile(package, outfile)
193 finally:
194 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