OLD | NEW |
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 |
| 6 import logging |
5 import os | 7 import os |
| 8 import re |
| 9 import shutil |
| 10 import subprocess |
6 import sys | 11 import sys |
7 import re | 12 from functools import partial |
8 import subprocess | |
9 import shutil | |
10 from getopt import getopt, GetoptError | |
11 from StringIO import StringIO | 13 from StringIO import StringIO |
12 from zipfile import ZipFile | 14 from zipfile import ZipFile |
13 | 15 |
14 knownTypes = {'gecko', 'chrome', 'generic', 'edge'} | 16 KNOWN_PLATFORMS = {'chrome', 'gecko', 'edge', 'generic'} |
| 17 |
| 18 MAIN_PARSER = argparse.ArgumentParser( |
| 19 description=__doc__, |
| 20 formatter_class=argparse.RawDescriptionHelpFormatter) |
| 21 |
| 22 SUB_PARSERS = MAIN_PARSER.add_subparsers(title='Commands', dest='action', |
| 23 metavar='[command]') |
| 24 |
| 25 ALL_COMMANDS = [] |
15 | 26 |
16 | 27 |
17 class Command(object): | 28 def make_argument(*args, **kwargs): |
18 name = property(lambda self: self._name) | 29 def _make_argument(*args, **kwargs): |
19 shortDescription = property( | 30 parser = kwargs.pop('parser') |
20 lambda self: self._shortDescription, | 31 parser.add_argument(*args, **kwargs) |
21 lambda self, value: self.__dict__.update({'_shortDescription': value}) | |
22 ) | |
23 description = property( | |
24 lambda self: self._description, | |
25 lambda self, value: self.__dict__.update({'_description': value}) | |
26 ) | |
27 params = property( | |
28 lambda self: self._params, | |
29 lambda self, value: self.__dict__.update({'_params': value}) | |
30 ) | |
31 supportedTypes = property( | |
32 lambda self: self._supportedTypes, | |
33 lambda self, value: self.__dict__.update({'_supportedTypes': value}) | |
34 ) | |
35 options = property(lambda self: self._options) | |
36 | 32 |
37 def __init__(self, handler, name): | 33 return partial(_make_argument, *args, **kwargs) |
38 self._handler = handler | |
39 self._name = name | |
40 self._shortDescription = '' | |
41 self._description = '' | |
42 self._params = '' | |
43 self._supportedTypes = None | |
44 self._options = [] | |
45 self.addOption('Show this message and exit', short='h', long='help') | |
46 | |
47 def __enter__(self): | |
48 return self | |
49 | |
50 def __exit__(self, exc_type, exc_value, traceback): | |
51 pass | |
52 | |
53 def __call__(self, baseDir, scriptName, opts, args, type): | |
54 return self._handler(baseDir, scriptName, opts, args, type) | |
55 | |
56 def isSupported(self, type): | |
57 return self._supportedTypes == None or type in self._supportedTypes | |
58 | |
59 def addOption(self, description, short=None, long=None, value=None, types=No
ne): | |
60 self._options.append((description, short, long, value, types)) | |
61 | |
62 def parseArgs(self, type, args): | |
63 shortOptions = map( | |
64 lambda o: o[1] + ':' if o[3] != None else o[1], | |
65 filter( | |
66 lambda o: o[1] != None and (o[4] == None or type in o[4]), | |
67 self._options | |
68 ) | |
69 ) | |
70 longOptions = map( | |
71 lambda o: o[2] + '=' if o[3] != None else o[2], | |
72 filter( | |
73 lambda o: o[2] != None and (o[4] == None or type in o[4]), | |
74 self._options | |
75 ) | |
76 ) | |
77 return getopt(args, ''.join(shortOptions), longOptions) | |
78 | 34 |
79 | 35 |
80 commandsList = [] | 36 def argparse_command(valid_platforms=None, arguments=()): |
81 commands = {} | 37 def wrapper(func): |
| 38 def func_wrapper(*args, **kwargs): |
| 39 return func(*args, **kwargs) |
| 40 |
| 41 short_desc, long_desc = func.__doc__.split(os.linesep + os.linesep, 1) |
| 42 |
| 43 ALL_COMMANDS.append({ |
| 44 'name': func.__name__, |
| 45 'description': long_desc, |
| 46 'help_text': short_desc, |
| 47 'valid_platforms': valid_platforms or KNOWN_PLATFORMS, |
| 48 'function': func, |
| 49 'arguments': arguments, |
| 50 }) |
| 51 return func_wrapper |
| 52 |
| 53 return wrapper |
82 | 54 |
83 | 55 |
84 def addCommand(handler, name): | 56 def make_subcommand(name, description, help_text, function, arguments): |
85 if isinstance(name, basestring): | 57 new_parser = SUB_PARSERS.add_parser( |
86 aliases = () | 58 name, description=description, help=help_text, |
87 else: | 59 formatter_class=argparse.RawDescriptionHelpFormatter, |
88 name, aliases = (name[0], name[1:]) | 60 ) |
89 | 61 |
90 global commandsList, commands | 62 for argument in arguments: |
91 command = Command(handler, name) | 63 argument(parser=new_parser) |
92 commandsList.append(command) | 64 |
93 commands[name] = command | 65 new_parser.set_defaults(function=function) |
94 for alias in aliases: | 66 return new_parser |
95 commands[alias] = command | |
96 return command | |
97 | 67 |
98 | 68 |
99 def splitByLength(string, maxLen): | 69 def build_available_subcommands(base_dir): |
100 parts = [] | 70 """Build subcommands, which are available for the repository in base_dir. |
101 currentPart = '' | 71 |
102 for match in re.finditer(r'\s*(\S+)', string): | 72 Search 'base_dir' for existing metadata.<type> files and make <type> an |
103 if len(match.group(0)) + len(currentPart) < maxLen: | 73 avaible choice for the subcommands, intersected with their respective valid |
104 currentPart += match.group(0) | 74 platforms. |
105 else: | 75 |
106 parts.append(currentPart) | 76 If no valid platform is found for a subcommand, it get's omitted. |
107 currentPart = match.group(1) | 77 """ |
108 if len(currentPart): | 78 if build_available_subcommands._result is not None: |
109 parts.append(currentPart) | 79 # Tests might run this code multiple times, make sure the collection |
110 return parts | 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')) |
| 97 if len(platforms) > 1: |
| 98 command_params['arguments'] += ( |
| 99 make_argument('-t', '--type', dest='platform', required=True, |
| 100 choices=platforms), |
| 101 ) |
| 102 make_subcommand(**command_params) |
| 103 elif len(platforms) == 1: |
| 104 sub_parser = make_subcommand(**command_params) |
| 105 sub_parser.set_defaults(platform=platforms.pop()) |
| 106 |
| 107 build_available_subcommands._result = True |
| 108 return True |
111 | 109 |
112 | 110 |
113 def usage(scriptName, type, commandName=None): | 111 build_available_subcommands._result = None |
114 if commandName == None: | |
115 global commandsList | |
116 descriptions = [] | |
117 for command in commandsList: | |
118 if not command.isSupported(type): | |
119 continue | |
120 commandText = ('%s %s' % (command.name, command.params)).ljust(39) | |
121 descriptionParts = splitByLength(command.shortDescription, 29) | |
122 descriptions.append(' %s [-t %s] %s %s' % (scriptName, type, comman
dText, descriptionParts[0])) | |
123 for part in descriptionParts[1:]: | |
124 descriptions.append(' %s %s %s %s' % (' ' * len(scriptName
), ' ' * len(type), ' ' * len(commandText), part)) | |
125 print '''Usage: | |
126 | |
127 %(descriptions)s | |
128 | |
129 For details on a command run: | |
130 | |
131 %(scriptName)s [-t %(type)s] <command> --help | |
132 ''' % { | |
133 'scriptName': scriptName, | |
134 'type': type, | |
135 'descriptions': '\n'.join(descriptions) | |
136 } | |
137 else: | |
138 global commands | |
139 command = commands[commandName] | |
140 description = '\n'.join(map(lambda s: '\n'.join(splitByLength(s, 80)), c
ommand.description.split('\n'))) | |
141 options = [] | |
142 for descr, short, long, value, types in command.options: | |
143 if types != None and type not in types: | |
144 continue | |
145 if short == None: | |
146 shortText = '' | |
147 elif value == None: | |
148 shortText = '-%s' % short | |
149 else: | |
150 shortText = '-%s %s' % (short, value) | |
151 if long == None: | |
152 longText = '' | |
153 elif value == None: | |
154 longText = '--%s' % long | |
155 else: | |
156 longText = '--%s=%s' % (long, value) | |
157 descrParts = splitByLength(descr, 46) | |
158 options.append(' %s %s %s' % (shortText.ljust(11), longText.ljust(1
9), descrParts[0])) | |
159 for part in descrParts[1:]: | |
160 options.append(' %s %s %s' % (' ' * 11, ' ' * 19, part)) | |
161 print '''%(scriptName)s [-t %(type)s] %(name)s %(params)s | |
162 | |
163 %(description)s | |
164 | |
165 Options: | |
166 %(options)s | |
167 ''' % { | |
168 'scriptName': scriptName, | |
169 'type': type, | |
170 'name': command.name, | |
171 'params': command.params, | |
172 'description': description, | |
173 'options': '\n'.join(options) | |
174 } | |
175 | 112 |
176 | 113 |
177 def runBuild(baseDir, scriptName, opts, args, type): | 114 @argparse_command( |
| 115 valid_platforms={'chrome', 'gecko', 'edge'}, |
| 116 arguments=( |
| 117 make_argument( |
| 118 '-b', '--build-num', dest='build_num', |
| 119 help=('Use given build number (if omitted the build number will ' |
| 120 'be retrieved from Mercurial)')), |
| 121 make_argument( |
| 122 '-k', '--key', dest='key_file', |
| 123 help=('File containing private key and certificates required to ' |
| 124 'sign the package')), |
| 125 make_argument( |
| 126 '-r', '--release', action='store_true', |
| 127 help='Create a release build'), |
| 128 make_argument('output_file', nargs='?') |
| 129 ) |
| 130 ) |
| 131 def build(base_dir, build_num, key_file, release, output_file, platform, |
| 132 **kwargs): |
| 133 """ |
| 134 Create a build. |
| 135 |
| 136 Creates an extension build with given file name. If output_file is missing |
| 137 a default name will be chosen. |
| 138 """ |
178 kwargs = {} | 139 kwargs = {} |
179 for option, value in opts: | 140 if platform == 'edge': |
180 if option in {'-b', '--build'}: | 141 import buildtools.packagerEdge as packager |
181 kwargs['buildNum'] = value | 142 else: |
182 if type != 'gecko' and not kwargs['buildNum'].isdigit(): | 143 import buildtools.packagerChrome as packager |
183 raise TypeError('Build number must be numerical') | |
184 elif option in {'-k', '--key'}: | |
185 kwargs['keyFile'] = value | |
186 elif option in {'-r', '--release'}: | |
187 kwargs['releaseBuild'] = True | |
188 if len(args) > 0: | |
189 kwargs['outFile'] = args[0] | |
190 | 144 |
191 if type in {'chrome', 'gecko'}: | 145 kwargs['keyFile'] = key_file |
192 import buildtools.packagerChrome as packager | 146 kwargs['outFile'] = output_file |
193 elif type == 'edge': | 147 kwargs['releaseBuild'] = release |
194 import buildtools.packagerEdge as packager | 148 kwargs['buildNum'] = build_num |
195 | 149 |
196 packager.createBuild(baseDir, type=type, **kwargs) | 150 packager.createBuild(base_dir, type=platform, **kwargs) |
197 | 151 |
198 | 152 |
199 def createDevEnv(baseDir, scriptName, opts, args, type): | 153 @argparse_command( |
200 if type == 'edge': | 154 valid_platforms={'chrome', 'gecko', 'edge'} |
| 155 ) |
| 156 def devenv(base_dir, platform, **kwargs): |
| 157 """ |
| 158 Set up a development environment. |
| 159 |
| 160 Will set up or update the devenv folder as an unpacked extension folder ' |
| 161 for development. |
| 162 """ |
| 163 if platform == 'edge': |
201 import buildtools.packagerEdge as packager | 164 import buildtools.packagerEdge as packager |
202 else: | 165 else: |
203 import buildtools.packagerChrome as packager | 166 import buildtools.packagerChrome as packager |
204 | 167 |
205 file = StringIO() | 168 file = StringIO() |
206 packager.createBuild(baseDir, type=type, outFile=file, devenv=True, releaseB
uild=True) | 169 packager.createBuild(base_dir, type=platform, outFile=file, devenv=True, |
| 170 releaseBuild=True) |
207 | 171 |
208 from buildtools.packager import getDevEnvPath | 172 from buildtools.packager import getDevEnvPath |
209 devenv_dir = getDevEnvPath(baseDir, type) | 173 devenv_dir = getDevEnvPath(base_dir, platform) |
210 | 174 |
211 shutil.rmtree(devenv_dir, ignore_errors=True) | 175 shutil.rmtree(devenv_dir, ignore_errors=True) |
212 | 176 |
213 file.seek(0) | 177 file.seek(0) |
214 with ZipFile(file, 'r') as zip_file: | 178 with ZipFile(file, 'r') as zip_file: |
215 zip_file.extractall(devenv_dir) | 179 zip_file.extractall(devenv_dir) |
216 | 180 |
217 | 181 |
218 def readLocaleConfig(baseDir, type, metadata): | 182 def read_locale_config(base_dir, platform, metadata): |
219 if type != 'generic': | 183 if platform != 'generic': |
220 import buildtools.packagerChrome as packager | 184 import buildtools.packagerChrome as packager |
221 localeDir = os.path.join(baseDir, '_locales') | 185 locale_dir = os.path.join(base_dir, '_locales') |
222 localeConfig = { | 186 locale_config = { |
223 'default_locale': packager.defaultLocale, | 187 'default_locale': packager.defaultLocale, |
224 } | 188 } |
225 else: | 189 else: |
226 localeDir = os.path.join( | 190 locale_dir = os.path.join( |
227 baseDir, *metadata.get('locales', 'base_path').split('/') | 191 base_dir, *metadata.get('locales', 'base_path').split('/') |
228 ) | 192 ) |
229 localeConfig = { | 193 locale_config = { |
230 'default_locale': metadata.get('locales', 'default_locale') | 194 'default_locale': metadata.get('locales', 'default_locale') |
231 } | 195 } |
232 | 196 |
233 localeConfig['base_path'] = localeDir | 197 locale_config['base_path'] = locale_dir |
234 | 198 |
235 locales = [(locale.replace('_', '-'), os.path.join(localeDir, locale)) | 199 locales = [(locale.replace('_', '-'), os.path.join(locale_dir, locale)) |
236 for locale in os.listdir(localeDir)] | 200 for locale in os.listdir(locale_dir)] |
237 localeConfig['locales'] = dict(locales) | 201 locale_config['locales'] = dict(locales) |
238 | 202 |
239 return localeConfig | 203 return locale_config |
240 | 204 |
241 | 205 |
242 def setupTranslations(baseDir, scriptName, opts, args, type): | 206 project_key_argument = make_argument( |
243 if len(args) < 1: | 207 'project_key', help='The crowdin project key.' |
244 print 'Project key is required to update translation master files.' | 208 ) |
245 usage(scriptName, type, 'setuptrans') | |
246 return | |
247 | 209 |
248 key = args[0] | |
249 | 210 |
| 211 @argparse_command( |
| 212 arguments=(project_key_argument, ) |
| 213 ) |
| 214 def setuptrans(base_dir, project_key, platform, **kwargs): |
| 215 """ |
| 216 Set up translation languages. |
| 217 |
| 218 Set up translation languages for the project on crowdin.com. |
| 219 """ |
250 from buildtools.packager import readMetadata | 220 from buildtools.packager import readMetadata |
251 metadata = readMetadata(baseDir, type) | 221 metadata = readMetadata(base_dir, platform) |
252 | 222 |
253 basename = metadata.get('general', 'basename') | 223 basename = metadata.get('general', 'basename') |
254 localeConfig = readLocaleConfig(baseDir, type, metadata) | 224 locale_config = read_locale_config(base_dir, platform, metadata) |
255 | 225 |
256 import buildtools.localeTools as localeTools | 226 import buildtools.localeTools as localeTools |
257 localeTools.setupTranslations(localeConfig, basename, key) | 227 localeTools.setupTranslations(locale_config, basename, project_key) |
258 | 228 |
259 | 229 |
260 def updateTranslationMaster(baseDir, scriptName, opts, args, type): | 230 @argparse_command( |
261 if len(args) < 1: | 231 arguments=(project_key_argument, ) |
262 print 'Project key is required to update translation master files.' | 232 ) |
263 usage(scriptName, type, 'translate') | 233 def translate(base_dir, project_key, platform, **kwargs): |
264 return | 234 """ |
| 235 Update translation master files. |
265 | 236 |
266 key = args[0] | 237 Update the translation master files in the project on crowdin.com. |
267 | 238 """ |
268 from buildtools.packager import readMetadata | 239 from buildtools.packager import readMetadata |
269 metadata = readMetadata(baseDir, type) | 240 metadata = readMetadata(base_dir, platform) |
270 | 241 |
271 basename = metadata.get('general', 'basename') | 242 basename = metadata.get('general', 'basename') |
272 localeConfig = readLocaleConfig(baseDir, type, metadata) | 243 locale_config = read_locale_config(base_dir, platform, metadata) |
273 | 244 |
274 defaultLocaleDir = os.path.join(localeConfig['base_path'], | 245 default_locale_dir = os.path.join(locale_config['base_path'], |
275 localeConfig['default_locale']) | 246 locale_config['default_locale']) |
276 | 247 |
277 import buildtools.localeTools as localeTools | 248 import buildtools.localeTools as localeTools |
278 localeTools.updateTranslationMaster(localeConfig, metadata, defaultLocaleDir
, | 249 localeTools.updateTranslationMaster(locale_config, metadata, |
279 basename, key) | 250 default_locale_dir, basename, |
| 251 project_key) |
280 | 252 |
281 | 253 |
282 def uploadTranslations(baseDir, scriptName, opts, args, type): | 254 @argparse_command( |
283 if len(args) < 1: | 255 arguments=(project_key_argument, ) |
284 print 'Project key is required to upload existing translations.' | 256 ) |
285 usage(scriptName, type, 'uploadtrans') | 257 def uploadtrans(base_dir, project_key, platform, **kwargs): |
286 return | 258 """ |
| 259 Upload existing translations. |
287 | 260 |
288 key = args[0] | 261 Upload already existing translations to the project on crowdin.com. |
289 | 262 """ |
290 from buildtools.packager import readMetadata | 263 from buildtools.packager import readMetadata |
291 metadata = readMetadata(baseDir, type) | 264 metadata = readMetadata(base_dir, platform) |
292 | 265 |
293 basename = metadata.get('general', 'basename') | 266 basename = metadata.get('general', 'basename') |
294 localeConfig = readLocaleConfig(baseDir, type, metadata) | 267 locale_config = read_locale_config(base_dir, platform, metadata) |
295 | 268 |
296 import buildtools.localeTools as localeTools | 269 import buildtools.localeTools as localeTools |
297 for locale, localeDir in localeConfig['locales'].iteritems(): | 270 for locale, locale_dir in locale_config['locales'].iteritems(): |
298 if locale != localeConfig['default_locale'].replace('_', '-'): | 271 if locale != locale_config['default_locale'].replace('_', '-'): |
299 localeTools.uploadTranslations(localeConfig, metadata, localeDir, lo
cale, | 272 localeTools.uploadTranslations(locale_config, metadata, locale_dir, |
300 basename, key) | 273 locale, basename, project_key) |
301 | 274 |
302 | 275 |
303 def getTranslations(baseDir, scriptName, opts, args, type): | 276 @argparse_command( |
304 if len(args) < 1: | 277 arguments=(project_key_argument, ) |
305 print 'Project key is required to update translation master files.' | 278 ) |
306 usage(scriptName, type, 'translate') | 279 def gettranslations(base_dir, project_key, platform, **kwargs): |
307 return | 280 """ |
| 281 Download translation updates. |
308 | 282 |
309 key = args[0] | 283 Download updated translations from crowdin.com. |
310 | 284 """ |
311 from buildtools.packager import readMetadata | 285 from buildtools.packager import readMetadata |
312 metadata = readMetadata(baseDir, type) | 286 metadata = readMetadata(base_dir, platform) |
313 | 287 |
314 basename = metadata.get('general', 'basename') | 288 basename = metadata.get('general', 'basename') |
315 localeConfig = readLocaleConfig(baseDir, type, metadata) | 289 locale_config = read_locale_config(base_dir, platform, metadata) |
316 | 290 |
317 import buildtools.localeTools as localeTools | 291 import buildtools.localeTools as localeTools |
318 localeTools.getTranslations(localeConfig, basename, key) | 292 localeTools.getTranslations(locale_config, basename, project_key) |
319 | 293 |
320 | 294 |
321 def generateDocs(baseDir, scriptName, opts, args, type): | 295 @argparse_command( |
322 if len(args) == 0: | 296 valid_platforms={'chrome'}, |
323 print 'No target directory specified for the documentation' | 297 arguments=( |
324 usage(scriptName, type, 'docs') | 298 make_argument('target_dir'), |
325 return | 299 make_argument('-q', '--quiet', help='Suppress JsDoc output'), |
326 targetDir = args[0] | 300 ) |
| 301 ) |
| 302 def docs(base_dir, target_dir, quiet, platform, **kwargs): |
| 303 """ |
| 304 Generate documentation (requires node.js). |
327 | 305 |
328 source_dir = os.path.join(baseDir, 'lib') | 306 Generate documentation files and write them into the specified directory. |
329 sources = [source_dir] | 307 """ |
| 308 source_dir = os.path.join(base_dir, 'lib') |
330 | 309 |
331 # JSDoc struggles wih huge objects: https://github.com/jsdoc3/jsdoc/issues/9
76 | 310 # JSDoc struggles wih huge objects: |
332 if type == 'chrome': | 311 # https://github.com/jsdoc3/jsdoc/issues/976 |
333 sources = [os.path.join(source_dir, filename) for filename in os.listdir
(source_dir) if filename != 'publicSuffixList.js'] | 312 sources = [os.path.join(source_dir, filename) |
| 313 for filename in os.listdir(source_dir) |
| 314 if filename != 'publicSuffixList.js'] |
334 | 315 |
335 buildtools_path = os.path.dirname(__file__) | 316 buildtools_path = os.path.dirname(__file__) |
336 config = os.path.join(buildtools_path, 'jsdoc.conf') | 317 config = os.path.join(buildtools_path, 'jsdoc.conf') |
337 | 318 |
338 command = ['npm', 'run-script', 'jsdoc', '--', '--destination', targetDir, | 319 command = ['npm', 'run-script', 'jsdoc', '--', '--destination', target_dir, |
339 '--configure', config] + sources | 320 '--configure', config] + sources |
340 if any(opt in {'-q', '--quiet'} for opt, _ in opts): | 321 if quiet: |
341 process = subprocess.Popen(command, stdout=subprocess.PIPE, | 322 process = subprocess.Popen(command, stdout=subprocess.PIPE, |
342 stderr=subprocess.PIPE, cwd=buildtools_path) | 323 stderr=subprocess.PIPE, cwd=buildtools_path) |
343 stderr = process.communicate()[1] | 324 stderr = process.communicate()[1] |
344 retcode = process.poll() | 325 retcode = process.poll() |
345 if retcode: | 326 if retcode: |
346 sys.stderr.write(stderr) | 327 sys.stderr.write(stderr) |
347 raise subprocess.CalledProcessError(command, retcode) | 328 raise subprocess.CalledProcessError(command, retcode) |
348 else: | 329 else: |
349 subprocess.check_call(command, cwd=buildtools_path) | 330 subprocess.check_call(command, cwd=buildtools_path) |
350 | 331 |
351 | 332 |
352 def runReleaseAutomation(baseDir, scriptName, opts, args, type): | 333 def valid_version_format(value): |
353 keyFile = None | 334 if re.search(r'[^\d\.]', value): |
354 downloadsRepo = os.path.join(baseDir, '..', 'downloads') | 335 raise argparse.ArgumentTypeError('Wrong version number format') |
355 for option, value in opts: | |
356 if option in {'-k', '--key'}: | |
357 keyFile = value | |
358 elif option in {'-d', '--downloads'}: | |
359 downloadsRepo = value | |
360 | 336 |
361 if len(args) == 0: | 337 return value |
362 print 'No version number specified for the release' | |
363 usage(scriptName, type, 'release') | |
364 return | |
365 version = args[0] | |
366 if re.search(r'[^\d\.]', version): | |
367 print 'Wrong version number format' | |
368 usage(scriptName, type, 'release') | |
369 return | |
370 | 338 |
371 if type == 'chrome' and keyFile is None: | 339 |
372 print >>sys.stderr, 'Error: you must specify a key file for this release
' | 340 @argparse_command( |
373 usage(scriptName, type, 'release') | 341 valid_platforms={'chrome', 'edge'}, |
| 342 arguments=( |
| 343 make_argument( |
| 344 '-k', '--key', dest='key_file', |
| 345 help=('File containing private key and certificates required to ' |
| 346 'sign the release.')), |
| 347 make_argument( |
| 348 '-d', '--downloads-repository', dest='downloads_repository', |
| 349 help=('Directory containing downloads repository (if omitted ' |
| 350 '../downloads is assumed)')), |
| 351 make_argument( |
| 352 'version', help='Version number of the release', |
| 353 type=valid_version_format) |
| 354 ) |
| 355 ) |
| 356 def release(base_dir, downloads_repository, key_file, platform, version, |
| 357 **kwargs): |
| 358 """ |
| 359 Run release automation. |
| 360 |
| 361 Note: If you are not the project owner then you probably don't want to run |
| 362 this! |
| 363 |
| 364 Run release automation: create downloads for the new version, tag source |
| 365 code repository as well as downloads and buildtools repository. |
| 366 """ |
| 367 if downloads_repository is None: |
| 368 downloads_repository = os.path.join(base_dir, os.pardir, 'downloads') |
| 369 |
| 370 if platform == 'chrome' and key_file is None: |
| 371 logging.error('You must specify a key file for this release') |
374 return | 372 return |
375 | 373 |
376 import buildtools.releaseAutomation as releaseAutomation | 374 import buildtools.releaseAutomation as releaseAutomation |
377 releaseAutomation.run(baseDir, type, version, keyFile, downloadsRepo) | 375 releaseAutomation.run(base_dir, platform, version, key_file, |
| 376 downloads_repository) |
378 | 377 |
379 | 378 |
380 def updatePSL(baseDir, scriptName, opts, args, type): | 379 @argparse_command(valid_platforms={'chrome'}) |
| 380 def updatepsl(base_dir, **kwargs): |
| 381 """Update Public Suffix List. |
| 382 |
| 383 Downloads Public Suffix List (see http://publicsuffix.org/) and generates |
| 384 lib/publicSuffixList.js from it. |
| 385 """ |
381 import buildtools.publicSuffixListUpdater as publicSuffixListUpdater | 386 import buildtools.publicSuffixListUpdater as publicSuffixListUpdater |
382 publicSuffixListUpdater.updatePSL(baseDir) | 387 publicSuffixListUpdater.updatePSL(base_dir) |
383 | 388 |
384 | 389 |
385 with addCommand(lambda baseDir, scriptName, opts, args, type: usage(scriptName,
type), ('help', '-h', '--help')) as command: | 390 def process_args(base_dir, *args): |
386 command.shortDescription = 'Show this message' | 391 if build_available_subcommands(base_dir): |
| 392 MAIN_PARSER.set_defaults(base_dir=base_dir) |
387 | 393 |
388 with addCommand(runBuild, 'build') as command: | 394 # If no args are provided, this module is run directly from the command |
389 command.shortDescription = 'Create a build' | 395 # line. argparse will take care of consuming sys.argv. |
390 command.description = 'Creates an extension build with given file name. If o
utput_file is missing a default name will be chosen.' | 396 arguments = MAIN_PARSER.parse_args(args if len(args) > 0 else None) |
391 command.params = '[options] [output_file]' | |
392 command.addOption('Use given build number (if omitted the build number will
be retrieved from Mercurial)', short='b', long='build', value='num') | |
393 command.addOption('File containing private key and certificates required to
sign the package', short='k', long='key', value='file', types={'chrome'}) | |
394 command.addOption('Create a release build', short='r', long='release') | |
395 command.supportedTypes = {'gecko', 'chrome', 'edge'} | |
396 | 397 |
397 with addCommand(createDevEnv, 'devenv') as command: | 398 function = arguments.function |
398 command.shortDescription = 'Set up a development environment' | 399 del arguments.function |
399 command.description = 'Will set up or update the devenv folder as an unpacke
d extension folder for development.' | 400 function(**vars(arguments)) |
400 command.supportedTypes = {'gecko', 'chrome', 'edge'} | |
401 | |
402 with addCommand(setupTranslations, 'setuptrans') as command: | |
403 command.shortDescription = 'Sets up translation languages' | |
404 command.description = 'Sets up translation languages for the project on crow
din.net.' | |
405 command.params = '[options] project-key' | |
406 | |
407 with addCommand(updateTranslationMaster, 'translate') as command: | |
408 command.shortDescription = 'Updates translation master files' | |
409 command.description = 'Updates the translation master files in the project o
n crowdin.net.' | |
410 command.params = '[options] project-key' | |
411 | |
412 with addCommand(uploadTranslations, 'uploadtrans') as command: | |
413 command.shortDescription = 'Uploads existing translations' | |
414 command.description = 'Uploads already existing translations to the project
on crowdin.net.' | |
415 command.params = '[options] project-key' | |
416 | |
417 with addCommand(getTranslations, 'gettranslations') as command: | |
418 command.shortDescription = 'Downloads translation updates' | |
419 command.description = 'Downloads updated translations from crowdin.net.' | |
420 command.params = '[options] project-key' | |
421 | |
422 with addCommand(generateDocs, 'docs') as command: | |
423 command.shortDescription = 'Generate documentation (requires node.js)' | |
424 command.description = ('Generate documentation files and write them into ' | |
425 'the specified directory.') | |
426 command.addOption('Suppress JsDoc output', short='q', long='quiet') | |
427 command.params = '[options] <directory>' | |
428 command.supportedTypes = {'chrome'} | |
429 | |
430 with addCommand(runReleaseAutomation, 'release') as command: | |
431 command.shortDescription = 'Run release automation' | |
432 command.description = 'Note: If you are not the project owner then you '
"probably don't want to run this!\n\n" 'Runs release automation: crea
tes downloads for the new version, tags ' 'source code repository as well
as downloads and buildtools repository.' | |
433 command.addOption('File containing private key and certificates required to
sign the release.', short='k', long='key', value='file', types={'chrome', 'edge'
}) | |
434 command.addOption('Directory containing downloads repository (if omitted ../
downloads is assumed)', short='d', long='downloads', value='dir') | |
435 command.params = '[options] <version>' | |
436 command.supportedTypes = {'chrome', 'edge'} | |
437 | |
438 with addCommand(updatePSL, 'updatepsl') as command: | |
439 command.shortDescription = 'Updates Public Suffix List' | |
440 command.description = 'Downloads Public Suffix List (see http://publicsuffix
.org/) and generates lib/publicSuffixList.js from it.' | |
441 command.supportedTypes = {'chrome'} | |
442 | |
443 | |
444 def getType(baseDir, scriptName, args): | |
445 # Look for an explicit type parameter (has to be the first parameter) | |
446 if len(args) >= 2 and args[0] == '-t': | |
447 type = args[1] | |
448 del args[1] | |
449 del args[0] | |
450 if type not in knownTypes: | |
451 print ''' | |
452 Unknown type %s specified, supported types are: %s | |
453 ''' % (type, ', '.join(knownTypes)) | |
454 return None | |
455 return type | |
456 | |
457 # Try to guess repository type | |
458 types = [] | |
459 for t in knownTypes: | |
460 if os.path.exists(os.path.join(baseDir, 'metadata.%s' % t)): | |
461 types.append(t) | |
462 | |
463 if len(types) == 1: | |
464 return types[0] | |
465 elif len(types) > 1: | |
466 print ''' | |
467 Ambiguous repository type, please specify -t parameter explicitly, e.g. | |
468 %s -t %s build | |
469 ''' % (scriptName, types[0]) | |
470 return None | |
471 else: | |
472 print ''' | |
473 No metadata file found in this repository, a metadata file like | |
474 metadata.* is required. | |
475 ''' | |
476 return None | |
477 | |
478 | |
479 def processArgs(baseDir, args): | |
480 global commands | |
481 | |
482 scriptName = os.path.basename(args[0]) | |
483 args = args[1:] | |
484 type = getType(baseDir, scriptName, args) | |
485 if type == None: | |
486 return | |
487 | |
488 if len(args) == 0: | |
489 args = ['build'] | |
490 print ''' | |
491 No command given, assuming "build". For a list of commands run: | |
492 | |
493 %s help | |
494 ''' % scriptName | |
495 | |
496 command = args[0] | |
497 if command in commands: | |
498 if commands[command].isSupported(type): | |
499 try: | |
500 opts, args = commands[command].parseArgs(type, args[1:]) | |
501 except GetoptError as e: | |
502 print str(e) | |
503 usage(scriptName, type, command) | |
504 sys.exit(2) | |
505 for option, value in opts: | |
506 if option in {'-h', '--help'}: | |
507 usage(scriptName, type, command) | |
508 sys.exit() | |
509 commands[command](baseDir, scriptName, opts, args, type) | |
510 else: | |
511 print 'Command %s is not supported for this application type' % comm
and | |
512 usage(scriptName, type) | |
513 else: | |
514 print 'Command %s is unrecognized' % command | |
515 usage(scriptName, type) | |
OLD | NEW |