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

Delta Between Two Patch Sets: packagerChrome.py

Issue 29549786: Issue 5535 - Replace our module system with webpack (Closed)
Left Patch Set: Addressed Sebastian's feedback Created Sept. 23, 2017, 8:24 p.m.
Right Patch Set: Addressed final nits Created Oct. 10, 2017, 5:02 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
Left: Side by side diff | Download
Right: Side by side diff | Download
« no previous file with change/comment | « package-lock.json ('k') | packagerEdge.py » ('j') | no next file with change/comment »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
LEFTRIGHT
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 errno 5 import errno
6 import glob
6 import io 7 import io
7 import json 8 import json
8 import os 9 import os
9 import re 10 import re
10 import shutil
11 from StringIO import StringIO 11 from StringIO import StringIO
12 import struct 12 import struct
13 import subprocess 13 import subprocess
14 import sys 14 import sys
15 import tempfile 15
16
17 from ensure_dependencies import read_deps
18 from packager import (readMetadata, getDefaultFileName, getBuildVersion, 16 from packager import (readMetadata, getDefaultFileName, getBuildVersion,
19 getTemplate, Files) 17 getTemplate, Files)
20 18
21 defaultLocale = 'en_US' 19 defaultLocale = 'en_US'
22 20
23 21
24 def getIgnoredFiles(params): 22 def getIgnoredFiles(params):
25 return {'store.description'} 23 return {'store.description'}
26 24
27 25
(...skipping 107 matching lines...) Expand 10 before | Expand all | Expand 10 after
135 # Normalize JSON structure 133 # Normalize JSON structure
136 licenseComment = re.compile(r'/\*.*?\*/', re.S) 134 licenseComment = re.compile(r'/\*.*?\*/', re.S)
137 data = json.loads(re.sub(licenseComment, '', manifest, 1)) 135 data = json.loads(re.sub(licenseComment, '', manifest, 1))
138 if '_dummy' in data: 136 if '_dummy' in data:
139 del data['_dummy'] 137 del data['_dummy']
140 manifest = json.dumps(data, sort_keys=True, indent=2) 138 manifest = json.dumps(data, sort_keys=True, indent=2)
141 139
142 return manifest.encode('utf-8') 140 return manifest.encode('utf-8')
143 141
144 142
143 def toJson(data):
144 return json.dumps(
145 data, ensure_ascii=False, sort_keys=True,
146 indent=2, separators=(',', ': ')
147 ).encode('utf-8') + '\n'
148
149
145 def create_bundles(params, files): 150 def create_bundles(params, files):
146 base_extension_path = params['baseDir'] 151 base_extension_path = params['baseDir']
147 info_templates = { 152 info_templates = {
148 'chrome': 'chromeInfo.js.tmpl', 153 'chrome': 'chromeInfo.js.tmpl',
149 'edge': 'edgeInfo.js.tmpl', 154 'edge': 'edgeInfo.js.tmpl',
150 'gecko-webext': 'geckoInfo.js.tmpl' 155 'gecko-webext': 'geckoInfo.js.tmpl'
151 } 156 }
152 info_module = None
153 157
154 # Historically we didn't use relative paths when requiring modules, so in 158 # Historically we didn't use relative paths when requiring modules, so in
155 # order for webpack to know where to find them we need to pass in a list 159 # order for webpack to know where to find them we need to pass in a list of
156 # of resolve paths - "./lib" and the "lib" directory of all immediate 160 # resolve paths. Going forward we should always use relative paths, once we
157 # dependencies (except adblockplus). 161 # do that consistently this can be removed. See issues 5760, 5761 and 5762.
158 # Going forward we should always use relative paths when requiring modules 162 resolve_paths = [os.path.join(base_extension_path, dir, 'lib')
159 # and once we do that everywhere we can remove this logic. 163 for dir in ['', 'adblockpluscore', 'adblockplusui']]
160 resolve_paths = [os.path.join(base_extension_path, 'lib')] 164
161 deps = read_deps(base_extension_path) 165 info_template = getTemplate(info_templates[params['type']])
162 if deps: 166 info_module = info_template.render(
163 for dep in deps: 167 basename=params['metadata'].get('general', 'basename'),
164 if not dep.startswith('_') and dep != 'adblockplus': 168 version=params['metadata'].get('general', 'version')
165 dep_lib_path = os.path.join(base_extension_path, dep, 'lib') 169 ).encode('utf-8')
166 if os.path.exists(dep_lib_path): 170
167 resolve_paths.append(dep_lib_path) 171 configuration = {
168 resolve_paths = ' '.join(resolve_paths) 172 'bundles': [],
169 173 'extension_path': base_extension_path,
170 temp_dir = tempfile.mkdtemp() 174 'info_module': info_module,
171 try: 175 'resolve_paths': resolve_paths,
172 info_module = os.path.join(temp_dir, 'info.js') 176 }
173 template = getTemplate(info_templates[params['type']]) 177
174 with open(info_module, 'w') as info_file: 178 for item in params['metadata'].items('bundles'):
175 info_file.write( 179 name, value = item
176 template.render( 180 base_item_path = os.path.dirname(item.source)
177 basename=params['metadata'].get('general', 'basename'), 181
178 version=params['metadata'].get('general', 'version') 182 bundle_file = os.path.relpath(os.path.join(base_item_path, name),
179 ).encode('utf-8') 183 base_extension_path)
180 ) 184 entry_files = [os.path.join(base_item_path, module_path)
181 185 for module_path in value.split()]
182 for item in params['metadata'].items('bundles'): 186 configuration['bundles'].append({
183 name, value = item 187 'bundle_name': bundle_file,
184 base_item_path = os.path.dirname(item.source) 188 'entry_points': entry_files,
185 189 })
186 bundle_file = os.path.relpath(os.path.join(base_item_path, name), 190
187 base_extension_path) 191 cmd = ['node', os.path.join(os.path.dirname(__file__), 'webpack_runner.js')]
188 entry_files = [ 192 process = subprocess.Popen(cmd, stdout=subprocess.PIPE,
189 os.path.join( 193 stdin=subprocess.PIPE)
190 '.', 194 output = process.communicate(input=toJson(configuration))[0]
191 os.path.relpath(os.path.join(base_item_path, module_path), 195 if process.returncode != 0:
192 base_extension_path) 196 raise subprocess.CalledProcessError(process.returncode, cmd=cmd)
193 ) 197
194 for module_path in value.split() 198 bundles = json.loads(output)
195 ] 199 for bundle in bundles:
196 subprocess.check_call( 200 files[bundle] = bundles[bundle].encode('utf-8')
197 ['npm', 'run-script', 'webpack', '--silent'],
198 cwd=os.path.dirname(__file__),
199 env={
200 'EXTENSION_PATH': base_extension_path,
201 'ENTRY_POINTS': ' '.join(entry_files),
202 'OUTPUT_PATH': temp_dir,
203 'BUNDLE_NAME': bundle_file,
204 'RESOLVE_PATHS': resolve_paths,
205 'INFO_PATH': info_module,
206 'PATH': os.environ['PATH']
207 }
208 )
209 for file_name in [bundle_file, bundle_file + '.map']:
210 with open(os.path.join(temp_dir, file_name), 'r') as f:
211 files[file_name] = f.read()
212 finally:
213 shutil.rmtree(temp_dir)
214
215
216 def toJson(data):
217 return json.dumps(
218 data, ensure_ascii=False, sort_keys=True,
219 indent=2, separators=(',', ': ')
220 ).encode('utf-8') + '\n'
221
222
223 def import_string_webext(data, key, source):
224 """Import a single translation from the source dictionary into data"""
225 data[key] = source
226
227
228 def import_string_gecko(data, key, value):
229 """Import Gecko-style locales into data.
230
231 Only sets {'message': value} in the data-dictionary, after stripping
232 undesired Gecko-style access keys.
233 """
234 match = re.search(r'^(.*?)\s*\(&.\)$', value)
235 if match:
236 value = match.group(1)
237 else:
238 index = value.find('&')
239 if index >= 0:
240 value = value[0:index] + value[index + 1:]
241
242 data[key] = {'message': value}
243 201
244 202
245 def import_locales(params, files): 203 def import_locales(params, files):
246 import localeTools 204 for item in params['metadata'].items('import_locales'):
247 205 filename, keys = item
248 # FIXME: localeTools doesn't use real Chrome locales, it uses dash as 206 for sourceFile in glob.glob(os.path.join(os.path.dirname(item.source),
249 # separator instead. 207 *filename.split('/'))):
250 convert_locale_code = lambda code: code.replace('-', '_') 208 locale = sourceFile.split(os.path.sep)[-2]
251 209 targetFile = os.path.join('_locales', locale, 'messages.json')
252 # We need to map Chrome locales to Gecko locales. Start by mapping Chrome 210 data = json.loads(files.get(targetFile, '{}').decode('utf-8'))
253 # locales to themselves, merely with the dash as separator.
254 locale_mapping = {convert_locale_code(l): l for l in localeTools.chromeLocal es}
255
256 # Convert values to Crowdin locales first (use Chrome => Crowdin mapping).
257 for chrome_locale, crowdin_locale in localeTools.langMappingChrome.iteritems ():
258 locale_mapping[convert_locale_code(chrome_locale)] = crowdin_locale
259
260 # Now convert values to Gecko locales (use Gecko => Crowdin mapping).
261 reverse_mapping = {v: k for k, v in locale_mapping.iteritems()}
262 for gecko_locale, crowdin_locale in localeTools.langMappingGecko.iteritems() :
263 if crowdin_locale in reverse_mapping:
264 locale_mapping[reverse_mapping[crowdin_locale]] = gecko_locale
265
266 for target, source in locale_mapping.iteritems():
267 targetFile = '_locales/%s/messages.json' % target
268 if not targetFile in files:
269 continue
270
271 for item in params['metadata'].items('import_locales'):
272 fileName, keys = item
273 parts = map(lambda n: source if n == '*' else n, fileName.split('/') )
274 sourceFile = os.path.join(os.path.dirname(item.source), *parts)
275 incompleteMarker = os.path.join(os.path.dirname(sourceFile), '.incom plete')
276 if not os.path.exists(sourceFile) or os.path.exists(incompleteMarker ):
277 continue
278
279 data = json.loads(files[targetFile].decode('utf-8'))
280 211
281 try: 212 try:
282 # The WebExtensions (.json) and Gecko format provide 213 with io.open(sourceFile, 'r', encoding='utf-8') as handle:
283 # translations differently and/or provide additional 214 sourceData = json.load(handle)
284 # information like e.g. "placeholders". We want to adhere to
285 # that and preserve the addtional info.
286 if sourceFile.endswith('.json'):
287 with io.open(sourceFile, 'r', encoding='utf-8') as handle:
288 sourceData = json.load(handle)
289 import_string = import_string_webext
290 else:
291 sourceData = localeTools.readFile(sourceFile)
292 import_string = import_string_gecko
293 215
294 # Resolve wildcard imports 216 # Resolve wildcard imports
295 if keys == '*' or keys == '=*': 217 if keys == '*':
296 importList = sourceData.keys() 218 importList = sourceData.keys()
297 importList = filter(lambda k: not k.startswith('_'), importL ist) 219 importList = filter(lambda k: not k.startswith('_'), importL ist)
298 if keys == '=*':
299 importList = map(lambda k: '=' + k, importList)
300 keys = ' '.join(importList) 220 keys = ' '.join(importList)
301 221
302 for stringID in keys.split(): 222 for stringID in keys.split():
303 noMangling = False
304 if stringID.startswith('='):
305 stringID = stringID[1:]
306 noMangling = True
307
308 if stringID in sourceData: 223 if stringID in sourceData:
309 if noMangling: 224 if stringID in data:
310 key = re.sub(r'\W', '_', stringID) 225 print ('Warning: locale string {} defined multiple'
311 else: 226 ' times').format(stringID)
312 key = re.sub(r'\..*', '', parts[-1]) + '_' + re.sub( r'\W', '_', stringID) 227
313 if key in data: 228 data[stringID] = sourceData[stringID]
314 print 'Warning: locale string %s defined multiple ti mes' % key
315
316 import_string(data, key, sourceData[stringID])
317 except Exception as e: 229 except Exception as e:
318 print 'Warning: error importing locale data from %s: %s' % (sour ceFile, e) 230 print 'Warning: error importing locale data from %s: %s' % (sour ceFile, e)
319 231
320 files[targetFile] = toJson(data) 232 files[targetFile] = toJson(data)
321 233
322 234
323 def truncate(text, length_limit): 235 def truncate(text, length_limit):
324 if len(text) <= length_limit: 236 if len(text) <= length_limit:
325 return text 237 return text
326 return text[:length_limit - 1].rstrip() + u'\u2026' 238 return text[:length_limit - 1].rstrip() + u'\u2026'
327 239
328 240
329 def fixTranslationsForCWS(files): 241 def fix_translations_for_chrome(files):
330 # Chrome Web Store requires messages used in manifest.json to be present in
331 # all languages. It also enforces length limits for extension names and
332 # descriptions.
333 defaults = {} 242 defaults = {}
334 data = json.loads(files['_locales/%s/messages.json' % defaultLocale]) 243 data = json.loads(files['_locales/%s/messages.json' % defaultLocale])
335 for match in re.finditer(r'__MSG_(\S+)__', files['manifest.json']): 244 for match in re.finditer(r'__MSG_(\S+)__', files['manifest.json']):
336 name = match.group(1) 245 name = match.group(1)
337 defaults[name] = data[name] 246 defaults[name] = data[name]
338 247
339 limits = {} 248 limits = {}
340 manifest = json.loads(files['manifest.json']) 249 manifest = json.loads(files['manifest.json'])
341 for key, limit in (('name', 45), ('description', 132), ('short_name', 12)): 250 for key, limit in (('name', 45), ('description', 132), ('short_name', 12)):
342 match = re.search(r'__MSG_(\S+)__', manifest.get(key, '')) 251 match = re.search(r'__MSG_(\S+)__', manifest.get(key, ''))
343 if match: 252 if match:
344 limits[match.group(1)] = limit 253 limits[match.group(1)] = limit
345 254
346 for filename in files: 255 for path in list(files):
347 if not filename.startswith('_locales/') or not filename.endswith('/messa ges.json'): 256 match = re.search(r'^_locales/(?:es_(AR|CL|(MX))|[^/]+)/(.*)', path)
257 if not match:
348 continue 258 continue
349 259
350 data = json.loads(files[filename]) 260 # The Chrome Web Store requires messages used in manifest.json to
351 for name, info in defaults.iteritems(): 261 # be present in all languages, and enforces length limits on
352 data.setdefault(name, info) 262 # extension name and description.
353 for name, limit in limits.iteritems(): 263 is_latam, is_mexican, filename = match.groups()
354 if name in data: 264 if filename == 'messages.json':
355 data[name]['message'] = truncate(data[name]['message'], limit) 265 data = json.loads(files[path])
356 files[filename] = toJson(data) 266 for name, info in defaults.iteritems():
267 data.setdefault(name, info)
268 for name, limit in limits.iteritems():
269 info = data.get(name)
270 if info:
271 info['message'] = truncate(info['message'], limit)
272 files[path] = toJson(data)
273
274 # Chrome combines Latin American dialects of Spanish into es-419.
275 if is_latam:
276 data = files.pop(path)
277 if is_mexican:
278 files['_locales/es_419/' + filename] = data
357 279
358 280
359 def signBinary(zipdata, keyFile): 281 def signBinary(zipdata, keyFile):
360 from Crypto.Hash import SHA 282 from Crypto.Hash import SHA
361 from Crypto.PublicKey import RSA 283 from Crypto.PublicKey import RSA
362 from Crypto.Signature import PKCS1_v1_5 284 from Crypto.Signature import PKCS1_v1_5
363 285
364 try: 286 try:
365 with open(keyFile, 'rb') as file: 287 with open(keyFile, 'rb') as file:
366 key = RSA.importKey(file.read()) 288 key = RSA.importKey(file.read())
(...skipping 59 matching lines...) Expand 10 before | Expand all | Expand 10 after
426 files.preprocess( 348 files.preprocess(
427 [f for f, _ in metadata.items('preprocess')], 349 [f for f, _ in metadata.items('preprocess')],
428 {'needsExt': True} 350 {'needsExt': True}
429 ) 351 )
430 352
431 if metadata.has_section('import_locales'): 353 if metadata.has_section('import_locales'):
432 import_locales(params, files) 354 import_locales(params, files)
433 355
434 files['manifest.json'] = createManifest(params, files) 356 files['manifest.json'] = createManifest(params, files)
435 if type == 'chrome': 357 if type == 'chrome':
436 fixTranslationsForCWS(files) 358 fix_translations_for_chrome(files)
437 359
438 if devenv: 360 if devenv:
439 import buildtools 361 import buildtools
440 import random 362 import random
441 files.read(os.path.join(buildtools.__path__[0], 'chromeDevenvPoller__.js '), relpath='devenvPoller__.js') 363 files.read(os.path.join(buildtools.__path__[0], 'chromeDevenvPoller__.js '), relpath='devenvPoller__.js')
442 files['devenvVersion__'] = str(random.random()) 364 files['devenvVersion__'] = str(random.random())
443 365
444 if metadata.has_option('general', 'testScripts'): 366 if metadata.has_option('general', 'testScripts'):
445 files['qunit/index.html'] = createScriptPage( 367 files['qunit/index.html'] = createScriptPage(
446 params, 'testIndex.html.tmpl', ('general', 'testScripts') 368 params, 'testIndex.html.tmpl', ('general', 'testScripts')
447 ) 369 )
448 370
449 zipdata = files.zipToString() 371 zipdata = files.zipToString()
450 signature = None 372 signature = None
451 pubkey = None 373 pubkey = None
452 if keyFile != None: 374 if keyFile != None:
453 signature = signBinary(zipdata, keyFile) 375 signature = signBinary(zipdata, keyFile)
454 pubkey = getPublicKey(keyFile) 376 pubkey = getPublicKey(keyFile)
455 writePackage(outFile, pubkey, signature, zipdata) 377 writePackage(outFile, pubkey, signature, zipdata)
LEFTRIGHT

Powered by Google App Engine
This is Rietveld