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

Delta Between Two Patch Sets: build.py

Issue 29609559: Issue 6021 - Refactoring build.py (Closed) Base URL: https://hg.adblockplus.org/buildtools/file/79688f4a4aff
Left Patch Set: Created Nov. 15, 2017, 10:25 a.m.
Right Patch Set: Fixing a regression in 'docs' Created Nov. 20, 2017, 9:17 a.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 | « no previous file | tests/test_packagerWebExt.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 argparse 5 import argparse
6 import logging
6 import os 7 import os
8 import re
9 import shutil
10 import subprocess
7 import sys 11 import sys
8 import re
9 import subprocess
10 import shutil
11 from functools import partial 12 from functools import partial
12 from StringIO import StringIO 13 from StringIO import StringIO
13 from zipfile import ZipFile 14 from zipfile import ZipFile
14 15
15 KNOWN_PLATFORMS = {'gecko', 'chrome', 'generic', 'edge'} 16 KNOWN_PLATFORMS = {'chrome', 'gecko', 'edge', 'generic'}
16 17
17 parser = argparse.ArgumentParser( 18 MAIN_PARSER = argparse.ArgumentParser(
18 description=__doc__, 19 description=__doc__,
19 formatter_class=argparse.RawDescriptionHelpFormatter) 20 formatter_class=argparse.RawDescriptionHelpFormatter)
20 21
21 subs = parser.add_subparsers(title='Commands', dest='action', 22 SUB_PARSERS = MAIN_PARSER.add_subparsers(title='Commands', dest='action',
22 metavar='[command]') 23 metavar='[command]')
24
25 ALL_COMMANDS = []
23 26
24 27
25 def make_argument(*args, **kwargs): 28 def make_argument(*args, **kwargs):
26 def _make_argument(*args, **kwargs): 29 def _make_argument(*args, **kwargs):
27 parser = kwargs.pop('parser') 30 parser = kwargs.pop('parser')
28 parser.add_argument(*args, **kwargs) 31 parser.add_argument(*args, **kwargs)
29 32
30 return partial(_make_argument, *args, **kwargs) 33 return partial(_make_argument, *args, **kwargs)
31 34
32 35
33 def argparse_command(name=None, platforms=None, arguments=()): 36 def argparse_command(valid_platforms=None, arguments=()):
34 def wrapper(func): 37 def wrapper(func):
35 def func_wrapper(*args, **kwargs): 38 def func_wrapper(*args, **kwargs):
36 return func(*args, **kwargs) 39 return func(*args, **kwargs)
37 40
38 short_desc, long_desc = func.__doc__.split(os.linesep + os.linesep, 1) 41 short_desc, long_desc = func.__doc__.split(os.linesep + os.linesep, 1)
39 42
40 new_parser = subs.add_parser( 43 ALL_COMMANDS.append({
41 name or func.__name__, 44 'name': func.__name__,
42 description=long_desc, 45 'description': long_desc,
43 formatter_class=argparse.RawDescriptionHelpFormatter, 46 'help_text': short_desc,
44 help=short_desc, 47 'valid_platforms': valid_platforms or KNOWN_PLATFORMS,
45 ) 48 'function': func,
46 49 'arguments': arguments,
50 })
51 return func_wrapper
52
53 return wrapper
54
55
56 def make_subcommand(name, description, help_text, function, arguments):
57 new_parser = SUB_PARSERS.add_parser(
58 name, description=description, help=help_text,
59 formatter_class=argparse.RawDescriptionHelpFormatter,
60 )
61
62 for argument in arguments:
63 argument(parser=new_parser)
64
65 new_parser.set_defaults(function=function)
66 return new_parser
67
68
69 def build_available_subcommands(base_dir):
70 """Build subcommands, which are available for the repository in base_dir.
71
72 Search 'base_dir' for existing metadata.<type> files and make <type> an
73 avaible choice for the subcommands, intersected with their respective valid
74 platforms.
75
76 If no valid platform is found for a subcommand, it get's omitted.
77 """
78 if build_available_subcommands._result is not None:
79 # Tests might run this code multiple times, make sure the collection
80 # of platforms is only run once.
81 return build_available_subcommands._result
82
83 types = set()
84 for p in KNOWN_PLATFORMS:
85 if os.path.exists(os.path.join(base_dir, 'metadata.' + p)):
86 types.add(p)
87
88 if len(types) == 0:
89 logging.error('No metadata file found in this repository. Expecting '
90 'one or more of {} to be present.'.format(
91 ', '.join('metadata.' + p for p in KNOWN_PLATFORMS)))
92 build_available_subcommands._result = False
93 return False
94
95 for command_params in ALL_COMMANDS:
96 platforms = types.intersection(command_params.pop('valid_platforms'))
47 if len(platforms) > 1: 97 if len(platforms) > 1:
48 new_parser.add_argument('-t', '--type', dest='platform', 98 command_params['arguments'] += (
49 choices=platforms, required=True) 99 make_argument('-t', '--type', dest='platform', required=True,
Wladimir Palant 2017/11/17 09:26:10 This is suboptimal for repositories where only one
tlucas 2017/11/17 11:36:45 Done. (See collect_platforms)
100 choices=platforms),
101 )
102 make_subcommand(**command_params)
50 elif len(platforms) == 1: 103 elif len(platforms) == 1:
51 new_parser.set_defaults(platform=platforms.pop()) 104 sub_parser = make_subcommand(**command_params)
52 105 sub_parser.set_defaults(platform=platforms.pop())
53 for argument in arguments: 106
54 argument(parser=new_parser) 107 build_available_subcommands._result = True
55 108 return True
56 new_parser.set_defaults(function=func) 109
57 return func_wrapper 110
58 111 build_available_subcommands._result = None
59 return wrapper 112
60 113
61 114 @argparse_command(
62 @argparse_command( 115 valid_platforms={'chrome', 'gecko', 'edge'},
63 name='build', platforms={'gecko', 'chrome', 'edge'},
64 arguments=( 116 arguments=(
65 make_argument( 117 make_argument(
66 '-b', '--build', 118 '-b', '--build-num', dest='build_num',
67 help=('Use given build number (if omitted the build number will ' 119 help=('Use given build number (if omitted the build number will '
68 'be retrieved from Mercurial)')), 120 'be retrieved from Mercurial)')),
69 make_argument( 121 make_argument(
70 '-k', '--key', dest='key_file', 122 '-k', '--key', dest='key_file',
71 help=('File containing private key and certificates required to ' 123 help=('File containing private key and certificates required to '
72 'sign the package')), 124 'sign the package')),
73 make_argument( 125 make_argument(
74 '-r', '--release', action='store_true', 126 '-r', '--release', action='store_true',
75 help='Create a release build'), 127 help='Create a release build'),
76 make_argument('output_file', nargs='?') 128 make_argument('output_file', nargs='?')
77 ) 129 )
78 ) 130 )
79 def run_build(base_dir, build, key_file, release, output_file, platform, 131 def build(base_dir, build_num, key_file, release, output_file, platform,
Wladimir Palant 2017/11/17 09:26:10 Nit: Maybe rename this function into build and dro
tlucas 2017/11/17 11:36:44 Done.
80 **kwargs): 132 **kwargs):
81 """ 133 """
82 Create a build. 134 Create a build.
83 135
84 Creates an extension build with given file name. If output_file is missing 136 Creates an extension build with given file name. If output_file is missing
85 a default name will be chosen. 137 a default name will be chosen.
86 """ 138 """
87 kwargs = {} 139 kwargs = {}
88 if platform in {'chrome', 'gecko'}: 140 if platform == 'edge':
141 import buildtools.packagerEdge as packager
142 else:
89 import buildtools.packagerChrome as packager 143 import buildtools.packagerChrome as packager
90 elif platform == 'edge':
91 import buildtools.packagerEdge as packager
Wladimir Palant 2017/11/17 09:26:10 Nit: This is code you merely moved around but cons
tlucas 2017/11/17 11:36:44 Done.
92 144
93 kwargs['keyFile'] = key_file 145 kwargs['keyFile'] = key_file
94 kwargs['outFile'] = output_file 146 kwargs['outFile'] = output_file
95 kwargs['releaseBuild'] = release 147 kwargs['releaseBuild'] = release
96 kwargs['buildNum'] = build 148 kwargs['buildNum'] = build_num
97 149
98 packager.createBuild(base_dir, type=platform, **kwargs) 150 packager.createBuild(base_dir, type=platform, **kwargs)
99 151
100 152
101 @argparse_command( 153 @argparse_command(
102 name='devenv', platforms={'chrome', 'gecko', 'edge'} 154 valid_platforms={'chrome', 'gecko', 'edge'}
103 ) 155 )
104 def create_devenv(base_dir, platform, **kwargs): 156 def devenv(base_dir, platform, **kwargs):
105 """ 157 """
106 Set up a development environment. 158 Set up a development environment.
107 159
108 Will set up or update the devenv folder as an unpacked extension folder ' 160 Will set up or update the devenv folder as an unpacked extension folder '
109 for development. 161 for development.
110 """ 162 """
111 if platform == 'edge': 163 if platform == 'edge':
112 import buildtools.packagerEdge as packager 164 import buildtools.packagerEdge as packager
113 else: 165 else:
114 import buildtools.packagerChrome as packager 166 import buildtools.packagerChrome as packager
(...skipping 30 matching lines...) Expand all
145 locale_config['base_path'] = locale_dir 197 locale_config['base_path'] = locale_dir
146 198
147 locales = [(locale.replace('_', '-'), os.path.join(locale_dir, locale)) 199 locales = [(locale.replace('_', '-'), os.path.join(locale_dir, locale))
148 for locale in os.listdir(locale_dir)] 200 for locale in os.listdir(locale_dir)]
149 locale_config['locales'] = dict(locales) 201 locale_config['locales'] = dict(locales)
150 202
151 return locale_config 203 return locale_config
152 204
153 205
154 project_key_argument = make_argument( 206 project_key_argument = make_argument(
155 'project_key', help='The crowdin project key (required).', nargs='?' 207 'project_key', help='The crowdin project key.'
Wladimir Palant 2017/11/17 09:26:10 This is a required parameter, why not nargs=1? Thi
tlucas 2017/11/17 11:36:44 nargs=1 would produce a list of one value, leaving
156 ) 208 )
157 209
158 210
159 @argparse_command( 211 @argparse_command(
160 name='setuptrans', platforms=KNOWN_PLATFORMS,
161 arguments=(project_key_argument, ) 212 arguments=(project_key_argument, )
162 ) 213 )
163 def setup_translations(base_dir, project_key, platform, **kwargs): 214 def setuptrans(base_dir, project_key, platform, **kwargs):
164 """ 215 """
165 Set up translation languages. 216 Set up translation languages.
166 217
167 Set up translation languages for the project on crowdin.net. 218 Set up translation languages for the project on crowdin.com.
Wladimir Palant 2017/11/17 09:26:10 Here and for the commands below: it's crowdin.com.
tlucas 2017/11/17 11:36:44 Done.
168 """ 219 """
169 from buildtools.packager import readMetadata 220 from buildtools.packager import readMetadata
170 metadata = readMetadata(base_dir, platform) 221 metadata = readMetadata(base_dir, platform)
171 222
172 basename = metadata.get('general', 'basename') 223 basename = metadata.get('general', 'basename')
173 locale_config = read_locale_config(base_dir, platform, metadata) 224 locale_config = read_locale_config(base_dir, platform, metadata)
174 225
175 import buildtools.localeTools as localeTools 226 import buildtools.localeTools as localeTools
176 localeTools.setupTranslations(locale_config, basename, project_key) 227 localeTools.setupTranslations(locale_config, basename, project_key)
177 228
178 229
179 @argparse_command( 230 @argparse_command(
180 name='translate', platforms=KNOWN_PLATFORMS,
181 arguments=(project_key_argument, ) 231 arguments=(project_key_argument, )
182 ) 232 )
183 def update_translation_master(base_dir, project_key, platform, **kwargs): 233 def translate(base_dir, project_key, platform, **kwargs):
184 """ 234 """
185 Update translation master files. 235 Update translation master files.
186 236
187 Update the translation master files in the project on crowdin.net. 237 Update the translation master files in the project on crowdin.com.
188 """ 238 """
189 from buildtools.packager import readMetadata 239 from buildtools.packager import readMetadata
190 metadata = readMetadata(base_dir, platform) 240 metadata = readMetadata(base_dir, platform)
191 241
192 basename = metadata.get('general', 'basename') 242 basename = metadata.get('general', 'basename')
193 locale_config = read_locale_config(base_dir, platform, metadata) 243 locale_config = read_locale_config(base_dir, platform, metadata)
194 244
195 default_locale_dir = os.path.join(locale_config['base_path'], 245 default_locale_dir = os.path.join(locale_config['base_path'],
196 locale_config['default_locale']) 246 locale_config['default_locale'])
197 247
198 import buildtools.localeTools as localeTools 248 import buildtools.localeTools as localeTools
199 localeTools.updateTranslationMaster(locale_config, metadata, 249 localeTools.updateTranslationMaster(locale_config, metadata,
200 default_locale_dir, basename, 250 default_locale_dir, basename,
201 project_key) 251 project_key)
202 252
203 253
204 @argparse_command( 254 @argparse_command(
205 name='uploadtrans', platforms=KNOWN_PLATFORMS,
206 arguments=(project_key_argument, ) 255 arguments=(project_key_argument, )
207 ) 256 )
208 def upload_translations(base_dir, project_key, platform, **kwargs): 257 def uploadtrans(base_dir, project_key, platform, **kwargs):
209 """ 258 """
210 Upload existing translations. 259 Upload existing translations.
211 260
212 Upload already existing translations to the project on crowdin.net. 261 Upload already existing translations to the project on crowdin.com.
213 """ 262 """
214 from buildtools.packager import readMetadata 263 from buildtools.packager import readMetadata
215 metadata = readMetadata(base_dir, platform) 264 metadata = readMetadata(base_dir, platform)
216 265
217 basename = metadata.get('general', 'basename') 266 basename = metadata.get('general', 'basename')
218 locale_config = read_locale_config(base_dir, platform, metadata) 267 locale_config = read_locale_config(base_dir, platform, metadata)
219 268
220 import buildtools.localeTools as localeTools 269 import buildtools.localeTools as localeTools
221 for locale, locale_dir in locale_config['locales'].iteritems(): 270 for locale, locale_dir in locale_config['locales'].iteritems():
222 if locale != locale_config['default_locale'].replace('_', '-'): 271 if locale != locale_config['default_locale'].replace('_', '-'):
223 localeTools.uploadTranslations(locale_config, metadata, locale_dir, 272 localeTools.uploadTranslations(locale_config, metadata, locale_dir,
224 locale, basename, project_key) 273 locale, basename, project_key)
225 274
226 275
227 @argparse_command( 276 @argparse_command(
228 name='gettranslations', platforms=KNOWN_PLATFORMS,
229 arguments=(project_key_argument, ) 277 arguments=(project_key_argument, )
230 ) 278 )
231 def get_translations(base_dir, project_key, platform, **kwargs): 279 def gettranslations(base_dir, project_key, platform, **kwargs):
232 """ 280 """
233 Download translation updates. 281 Download translation updates.
234 282
235 Download updated translations from crowdin.net. 283 Download updated translations from crowdin.com.
236 """ 284 """
237 from buildtools.packager import readMetadata 285 from buildtools.packager import readMetadata
238 metadata = readMetadata(base_dir, platform) 286 metadata = readMetadata(base_dir, platform)
239 287
240 basename = metadata.get('general', 'basename') 288 basename = metadata.get('general', 'basename')
241 locale_config = read_locale_config(base_dir, platform, metadata) 289 locale_config = read_locale_config(base_dir, platform, metadata)
242 290
243 import buildtools.localeTools as localeTools 291 import buildtools.localeTools as localeTools
244 localeTools.getTranslations(locale_config, basename, project_key) 292 localeTools.getTranslations(locale_config, basename, project_key)
245 293
246 294
247 @argparse_command( 295 @argparse_command(
248 name='docs', platforms={'chrome'}, 296 valid_platforms={'chrome'},
249 arguments=( 297 arguments=(
250 make_argument('target_dir'), 298 make_argument('target_dir'),
251 make_argument('-q', '--quiet', help='Suppress JsDoc output'), 299 make_argument('-q', '--quiet', help='Suppress JsDoc output',
300 action='store_true', default=False),
252 ) 301 )
253 ) 302 )
254 def generate_docs(base_dir, target_dir, quiet, platform, **kwargs): 303 def docs(base_dir, target_dir, quiet, platform, **kwargs):
255 """ 304 """
256 Generate documentation (requires node.js). 305 Generate documentation (requires node.js).
257 306
258 Generate documentation files and write them into the specified directory. 307 Generate documentation files and write them into the specified directory.
259 """ 308 """
260 source_dir = os.path.join(base_dir, 'lib') 309 source_dir = os.path.join(base_dir, 'lib')
261 sources = [source_dir]
262 310
263 # JSDoc struggles wih huge objects: 311 # JSDoc struggles wih huge objects:
264 # https://github.com/jsdoc3/jsdoc/issues/976 312 # https://github.com/jsdoc3/jsdoc/issues/976
265 if platform == 'chrome': 313 sources = [os.path.join(source_dir, filename)
Wladimir Palant 2017/11/17 09:26:10 Nit: This check can be removed, we have no other p
tlucas 2017/11/17 11:36:44 Done.
266 sources = [os.path.join(source_dir, filename) 314 for filename in os.listdir(source_dir)
267 for filename in os.listdir(source_dir) 315 if filename != 'publicSuffixList.js']
268 if filename != 'publicSuffixList.js']
269 316
270 buildtools_path = os.path.dirname(__file__) 317 buildtools_path = os.path.dirname(__file__)
271 config = os.path.join(buildtools_path, 'jsdoc.conf') 318 config = os.path.join(buildtools_path, 'jsdoc.conf')
272 319
273 command = ['npm', 'run-script', 'jsdoc', '--', '--destination', target_dir, 320 command = ['npm', 'run-script', 'jsdoc', '--', '--destination', target_dir,
274 '--configure', config] + sources 321 '--configure', config] + sources
275 if quiet: 322 if quiet:
276 process = subprocess.Popen(command, stdout=subprocess.PIPE, 323 process = subprocess.Popen(command, stdout=subprocess.PIPE,
277 stderr=subprocess.PIPE, cwd=buildtools_path) 324 stderr=subprocess.PIPE, cwd=buildtools_path)
278 stderr = process.communicate()[1] 325 stderr = process.communicate()[1]
279 retcode = process.poll() 326 retcode = process.poll()
280 if retcode: 327 if retcode:
281 sys.stderr.write(stderr) 328 sys.stderr.write(stderr)
282 raise subprocess.CalledProcessError(command, retcode) 329 raise subprocess.CalledProcessError(command, retcode)
283 else: 330 else:
284 subprocess.check_call(command, cwd=buildtools_path) 331 subprocess.check_call(command, cwd=buildtools_path)
285 332
286 333
287 def valid_version_format(value): 334 def valid_version_format(value):
288 if re.search(r'[^\d\.]', value): 335 if re.search(r'[^\d\.]', value):
289 raise argparse.ArgumentTypeError('Wrong version number format') 336 raise argparse.ArgumentTypeError('Wrong version number format')
290 337
291 return value 338 return value
292 339
293 340
294 @argparse_command( 341 @argparse_command(
295 name='release', platforms={'chrome', 'edge'}, 342 valid_platforms={'chrome', 'edge'},
296 arguments=( 343 arguments=(
297 make_argument( 344 make_argument(
298 '-k', '--key', dest='key_file', 345 '-k', '--key', dest='key_file',
299 help=('File containing private key and certificates required to ' 346 help=('File containing private key and certificates required to '
300 'sign the release.')), 347 'sign the release.')),
301 make_argument( 348 make_argument(
302 '-d', '--downloads-repository', dest='download_repository', 349 '-d', '--downloads-repository', dest='downloads_repository',
Wladimir Palant 2017/11/17 09:26:10 Is the naming download_repository rather than down
tlucas 2017/11/17 11:36:44 No, this was not intentional. Done.
303 default='../downloads',
Wladimir Palant 2017/11/17 09:26:10 Please use os.path.join() here. Also, the default
tlucas 2017/11/17 11:36:44 Done.
304 help=('Directory containing downloads repository (if omitted ' 350 help=('Directory containing downloads repository (if omitted '
305 '../downloads is assumed)')), 351 '../downloads is assumed)')),
306 make_argument( 352 make_argument(
307 'version', nargs='?', help='Version number of the release', 353 'version', help='Version number of the release',
Wladimir Palant 2017/11/17 09:26:10 This parameter isn't optional.
tlucas 2017/11/17 11:36:44 Done.
308 type=valid_version_format) 354 type=valid_version_format)
309 ) 355 )
310 ) 356 )
311 def run_release_automation(base_dir, download_repository, key_file, platform, 357 def release(base_dir, downloads_repository, key_file, platform, version,
312 version, **kwargs): 358 **kwargs):
313 """ 359 """
314 Run release automation. 360 Run release automation.
315 361
316 Note: If you are not the project owner then you probably don't want to run 362 Note: If you are not the project owner then you probably don't want to run
317 this! 363 this!
318 364
319 Run release automation: create downloads for the new version, tag source 365 Run release automation: create downloads for the new version, tag source
320 code repository as well as downloads and buildtools repository. 366 code repository as well as downloads and buildtools repository.
321 """ 367 """
368 if downloads_repository is None:
369 downloads_repository = os.path.join(base_dir, os.pardir, 'downloads')
370
322 if platform == 'chrome' and key_file is None: 371 if platform == 'chrome' and key_file is None:
323 raise ValueError('You must specify a key file for this release') 372 logging.error('You must specify a key file for this release')
373 return
324 374
325 import buildtools.releaseAutomation as releaseAutomation 375 import buildtools.releaseAutomation as releaseAutomation
326 releaseAutomation.run(base_dir, platform, version, key_file, 376 releaseAutomation.run(base_dir, platform, version, key_file,
327 download_repository) 377 downloads_repository)
328 378
329 379
330 @argparse_command(name='updatepsl', platforms={'chrome'}) 380 @argparse_command(valid_platforms={'chrome'})
331 def update_psl(base_dir, **kwargs): 381 def updatepsl(base_dir, **kwargs):
332 """Update Public Suffix List. 382 """Update Public Suffix List.
333 383
334 Downloads Public Suffix List (see http://publicsuffix.org/) and generates 384 Downloads Public Suffix List (see http://publicsuffix.org/) and generates
335 lib/publicSuffixList.js from it. 385 lib/publicSuffixList.js from it.
336 """ 386 """
337 import buildtools.publicSuffixListUpdater as publicSuffixListUpdater 387 import buildtools.publicSuffixListUpdater as publicSuffixListUpdater
338 publicSuffixListUpdater.updatePSL(base_dir) 388 publicSuffixListUpdater.updatePSL(base_dir)
339 389
340 390
341 def process_args(base_dir, *args): 391 def process_args(base_dir, *args):
342 parser.set_defaults(base_dir=base_dir) 392 if build_available_subcommands(base_dir):
343 393 MAIN_PARSER.set_defaults(base_dir=base_dir)
344 # If no args are provided, this module is run directly from the command 394
345 # line. argparse will take care of consuming sys.argv. 395 # If no args are provided, this module is run directly from the command
346 arguments = parser.parse_args(args if len(args) > 0 else None) 396 # line. argparse will take care of consuming sys.argv.
347 397 arguments = MAIN_PARSER.parse_args(args if len(args) > 0 else None)
348 function = arguments.function 398
349 del arguments.function 399 function = arguments.function
350 function(**vars(arguments)) 400 del arguments.function
401 function(**vars(arguments))
LEFTRIGHT

Powered by Google App Engine
This is Rietveld