| 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                       action='store_true', default=False), | 
 |  301     ) | 
 |  302 ) | 
 |  303 def docs(base_dir, target_dir, quiet, platform, **kwargs): | 
 |  304     """ | 
 |  305     Generate documentation (requires node.js). | 
|  327  |  306  | 
|  328     source_dir = os.path.join(baseDir, 'lib') |  307     Generate documentation files and write them into the specified directory. | 
|  329     sources = [source_dir] |  308     """ | 
 |  309     source_dir = os.path.join(base_dir, 'lib') | 
|  330  |  310  | 
|  331     # JSDoc struggles wih huge objects: https://github.com/jsdoc3/jsdoc/issues/9
     76 |  311     # JSDoc struggles wih huge objects: | 
|  332     if type == 'chrome': |  312     # 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'] |  313     sources = [os.path.join(source_dir, filename) | 
 |  314                for filename in os.listdir(source_dir) | 
 |  315                if filename != 'publicSuffixList.js'] | 
|  334  |  316  | 
|  335     buildtools_path = os.path.dirname(__file__) |  317     buildtools_path = os.path.dirname(__file__) | 
|  336     config = os.path.join(buildtools_path, 'jsdoc.conf') |  318     config = os.path.join(buildtools_path, 'jsdoc.conf') | 
|  337  |  319  | 
|  338     command = ['npm', 'run-script', 'jsdoc', '--', '--destination', targetDir, |  320     command = ['npm', 'run-script', 'jsdoc', '--', '--destination', target_dir, | 
|  339                '--configure', config] + sources |  321                '--configure', config] + sources | 
|  340     if any(opt in {'-q', '--quiet'} for opt, _ in opts): |  322     if quiet: | 
|  341         process = subprocess.Popen(command, stdout=subprocess.PIPE, |  323         process = subprocess.Popen(command, stdout=subprocess.PIPE, | 
|  342                                    stderr=subprocess.PIPE, cwd=buildtools_path) |  324                                    stderr=subprocess.PIPE, cwd=buildtools_path) | 
|  343         stderr = process.communicate()[1] |  325         stderr = process.communicate()[1] | 
|  344         retcode = process.poll() |  326         retcode = process.poll() | 
|  345         if retcode: |  327         if retcode: | 
|  346             sys.stderr.write(stderr) |  328             sys.stderr.write(stderr) | 
|  347             raise subprocess.CalledProcessError(command, retcode) |  329             raise subprocess.CalledProcessError(command, retcode) | 
|  348     else: |  330     else: | 
|  349         subprocess.check_call(command, cwd=buildtools_path) |  331         subprocess.check_call(command, cwd=buildtools_path) | 
|  350  |  332  | 
|  351  |  333  | 
|  352 def runReleaseAutomation(baseDir, scriptName, opts, args, type): |  334 def valid_version_format(value): | 
|  353     keyFile = None |  335     if re.search(r'[^\d\.]', value): | 
|  354     downloadsRepo = os.path.join(baseDir, '..', 'downloads') |  336         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  |  337  | 
|  361     if len(args) == 0: |  338     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  |  339  | 
|  371     if type == 'chrome' and keyFile is None: |  340  | 
|  372         print >>sys.stderr, 'Error: you must specify a key file for this release
     ' |  341 @argparse_command( | 
|  373         usage(scriptName, type, 'release') |  342     valid_platforms={'chrome', 'edge'}, | 
 |  343     arguments=( | 
 |  344         make_argument( | 
 |  345             '-k', '--key', dest='key_file', | 
 |  346             help=('File containing private key and certificates required to ' | 
 |  347                   'sign the release.')), | 
 |  348         make_argument( | 
 |  349             '-d', '--downloads-repository', dest='downloads_repository', | 
 |  350             help=('Directory containing downloads repository (if omitted ' | 
 |  351                   '../downloads is assumed)')), | 
 |  352         make_argument( | 
 |  353             'version', help='Version number of the release', | 
 |  354             type=valid_version_format) | 
 |  355     ) | 
 |  356 ) | 
 |  357 def release(base_dir, downloads_repository, key_file, platform, version, | 
 |  358             **kwargs): | 
 |  359     """ | 
 |  360     Run release automation. | 
 |  361  | 
 |  362     Note: If you are not the project owner then you probably don't want to run | 
 |  363     this! | 
 |  364  | 
 |  365     Run release automation: create downloads for the new version, tag source | 
 |  366     code repository as well as downloads and buildtools repository. | 
 |  367     """ | 
 |  368     if downloads_repository is None: | 
 |  369         downloads_repository = os.path.join(base_dir, os.pardir, 'downloads') | 
 |  370  | 
 |  371     if platform == 'chrome' and key_file is None: | 
 |  372         logging.error('You must specify a key file for this release') | 
|  374         return |  373         return | 
|  375  |  374  | 
|  376     import buildtools.releaseAutomation as releaseAutomation |  375     import buildtools.releaseAutomation as releaseAutomation | 
|  377     releaseAutomation.run(baseDir, type, version, keyFile, downloadsRepo) |  376     releaseAutomation.run(base_dir, platform, version, key_file, | 
 |  377                           downloads_repository) | 
|  378  |  378  | 
|  379  |  379  | 
|  380 def updatePSL(baseDir, scriptName, opts, args, type): |  380 @argparse_command(valid_platforms={'chrome'}) | 
 |  381 def updatepsl(base_dir, **kwargs): | 
 |  382     """Update Public Suffix List. | 
 |  383  | 
 |  384     Downloads Public Suffix List (see http://publicsuffix.org/) and generates | 
 |  385     lib/publicSuffixList.js from it. | 
 |  386     """ | 
|  381     import buildtools.publicSuffixListUpdater as publicSuffixListUpdater |  387     import buildtools.publicSuffixListUpdater as publicSuffixListUpdater | 
|  382     publicSuffixListUpdater.updatePSL(baseDir) |  388     publicSuffixListUpdater.updatePSL(base_dir) | 
|  383  |  389  | 
|  384  |  390  | 
|  385 with addCommand(lambda baseDir, scriptName, opts, args, type: usage(scriptName, 
     type), ('help', '-h', '--help')) as command: |  391 def process_args(base_dir, *args): | 
|  386     command.shortDescription = 'Show this message' |  392     if build_available_subcommands(base_dir): | 
 |  393         MAIN_PARSER.set_defaults(base_dir=base_dir) | 
|  387  |  394  | 
|  388 with addCommand(runBuild, 'build') as command: |  395         # If no args are provided, this module is run directly from the command | 
|  389     command.shortDescription = 'Create a build' |  396         # 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.' |  397         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  |  398  | 
|  397 with addCommand(createDevEnv, 'devenv') as command: |  399         function = arguments.function | 
|  398     command.shortDescription = 'Set up a development environment' |  400         del arguments.function | 
|  399     command.description = 'Will set up or update the devenv folder as an unpacke
     d extension folder for development.' |  401         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 |