| 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', | 
|  | 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 | 
|---|