| Index: build.py | 
| diff --git a/build.py b/build.py | 
| index aa3c2035eea5688fcbbd9468ccafa250757f161c..877b2644e5229c3f3a5d414e2a115038e6ed8d76 100644 | 
| --- a/build.py | 
| +++ b/build.py | 
| @@ -2,211 +2,175 @@ | 
| # License, v. 2.0. If a copy of the MPL was not distributed with this | 
| # file, You can obtain one at http://mozilla.org/MPL/2.0/. | 
|  | 
| +import argparse | 
| +import logging | 
| import os | 
| -import sys | 
| import re | 
| -import subprocess | 
| import shutil | 
| -from getopt import getopt, GetoptError | 
| +import subprocess | 
| +import sys | 
| +from functools import partial | 
| from StringIO import StringIO | 
| from zipfile import ZipFile | 
|  | 
| -knownTypes = {'gecko', 'chrome', 'generic', 'edge'} | 
| +KNOWN_PLATFORMS = {'chrome', 'gecko', 'edge', 'generic'} | 
|  | 
| +MAIN_PARSER = argparse.ArgumentParser( | 
| +    description=__doc__, | 
| +    formatter_class=argparse.RawDescriptionHelpFormatter) | 
|  | 
| -class Command(object): | 
| -    name = property(lambda self: self._name) | 
| -    shortDescription = property( | 
| -        lambda self: self._shortDescription, | 
| -        lambda self, value: self.__dict__.update({'_shortDescription': value}) | 
| -    ) | 
| -    description = property( | 
| -        lambda self: self._description, | 
| -        lambda self, value: self.__dict__.update({'_description': value}) | 
| -    ) | 
| -    params = property( | 
| -        lambda self: self._params, | 
| -        lambda self, value: self.__dict__.update({'_params': value}) | 
| -    ) | 
| -    supportedTypes = property( | 
| -        lambda self: self._supportedTypes, | 
| -        lambda self, value: self.__dict__.update({'_supportedTypes': value}) | 
| -    ) | 
| -    options = property(lambda self: self._options) | 
| - | 
| -    def __init__(self, handler, name): | 
| -        self._handler = handler | 
| -        self._name = name | 
| -        self._shortDescription = '' | 
| -        self._description = '' | 
| -        self._params = '' | 
| -        self._supportedTypes = None | 
| -        self._options = [] | 
| -        self.addOption('Show this message and exit', short='h', long='help') | 
| - | 
| -    def __enter__(self): | 
| -        return self | 
| - | 
| -    def __exit__(self, exc_type, exc_value, traceback): | 
| -        pass | 
| - | 
| -    def __call__(self, baseDir, scriptName, opts, args, type): | 
| -        return self._handler(baseDir, scriptName, opts, args, type) | 
| - | 
| -    def isSupported(self, type): | 
| -        return self._supportedTypes == None or type in self._supportedTypes | 
| - | 
| -    def addOption(self, description, short=None, long=None, value=None, types=None): | 
| -        self._options.append((description, short, long, value, types)) | 
| - | 
| -    def parseArgs(self, type, args): | 
| -        shortOptions = map( | 
| -            lambda o: o[1] + ':' if o[3] != None else o[1], | 
| -            filter( | 
| -                lambda o: o[1] != None and (o[4] == None or type in o[4]), | 
| -                self._options | 
| -            ) | 
| -        ) | 
| -        longOptions = map( | 
| -            lambda o: o[2] + '=' if o[3] != None else o[2], | 
| -            filter( | 
| -                lambda o: o[2] != None and (o[4] == None or type in o[4]), | 
| -                self._options | 
| -            ) | 
| -        ) | 
| -        return getopt(args, ''.join(shortOptions), longOptions) | 
| +SUB_PARSERS = MAIN_PARSER.add_subparsers(title='Commands', dest='action', | 
| +                                         metavar='[command]') | 
|  | 
| +ALL_COMMANDS = [] | 
|  | 
| -commandsList = [] | 
| -commands = {} | 
|  | 
| +def make_argument(*args, **kwargs): | 
| +    def _make_argument(*args, **kwargs): | 
| +        parser = kwargs.pop('parser') | 
| +        parser.add_argument(*args, **kwargs) | 
|  | 
| -def addCommand(handler, name): | 
| -    if isinstance(name, basestring): | 
| -        aliases = () | 
| -    else: | 
| -        name, aliases = (name[0], name[1:]) | 
| - | 
| -    global commandsList, commands | 
| -    command = Command(handler, name) | 
| -    commandsList.append(command) | 
| -    commands[name] = command | 
| -    for alias in aliases: | 
| -        commands[alias] = command | 
| -    return command | 
| - | 
| - | 
| -def splitByLength(string, maxLen): | 
| -    parts = [] | 
| -    currentPart = '' | 
| -    for match in re.finditer(r'\s*(\S+)', string): | 
| -        if len(match.group(0)) + len(currentPart) < maxLen: | 
| -            currentPart += match.group(0) | 
| -        else: | 
| -            parts.append(currentPart) | 
| -            currentPart = match.group(1) | 
| -    if len(currentPart): | 
| -        parts.append(currentPart) | 
| -    return parts | 
| - | 
| - | 
| -def usage(scriptName, type, commandName=None): | 
| -    if commandName == None: | 
| -        global commandsList | 
| -        descriptions = [] | 
| -        for command in commandsList: | 
| -            if not command.isSupported(type): | 
| -                continue | 
| -            commandText = ('%s %s' % (command.name, command.params)).ljust(39) | 
| -            descriptionParts = splitByLength(command.shortDescription, 29) | 
| -            descriptions.append('  %s [-t %s] %s %s' % (scriptName, type, commandText, descriptionParts[0])) | 
| -            for part in descriptionParts[1:]: | 
| -                descriptions.append('  %s     %s  %s %s' % (' ' * len(scriptName), ' ' * len(type), ' ' * len(commandText), part)) | 
| -        print '''Usage: | 
| - | 
| -%(descriptions)s | 
| - | 
| -For details on a command run: | 
| - | 
| -  %(scriptName)s [-t %(type)s] <command> --help | 
| -''' % { | 
| -            'scriptName': scriptName, | 
| -            'type': type, | 
| -            'descriptions': '\n'.join(descriptions) | 
| -        } | 
| -    else: | 
| -        global commands | 
| -        command = commands[commandName] | 
| -        description = '\n'.join(map(lambda s: '\n'.join(splitByLength(s, 80)), command.description.split('\n'))) | 
| -        options = [] | 
| -        for descr, short, long, value, types in command.options: | 
| -            if types != None and type not in types: | 
| -                continue | 
| -            if short == None: | 
| -                shortText = '' | 
| -            elif value == None: | 
| -                shortText = '-%s' % short | 
| -            else: | 
| -                shortText = '-%s %s' % (short, value) | 
| -            if long == None: | 
| -                longText = '' | 
| -            elif value == None: | 
| -                longText = '--%s' % long | 
| -            else: | 
| -                longText = '--%s=%s' % (long, value) | 
| -            descrParts = splitByLength(descr, 46) | 
| -            options.append('  %s %s %s' % (shortText.ljust(11), longText.ljust(19), descrParts[0])) | 
| -            for part in descrParts[1:]: | 
| -                options.append('  %s %s %s' % (' ' * 11, ' ' * 19, part)) | 
| -        print '''%(scriptName)s [-t %(type)s] %(name)s %(params)s | 
| - | 
| -%(description)s | 
| - | 
| -Options: | 
| -%(options)s | 
| -''' % { | 
| -            'scriptName': scriptName, | 
| -            'type': type, | 
| -            'name': command.name, | 
| -            'params': command.params, | 
| -            'description': description, | 
| -            'options': '\n'.join(options) | 
| -        } | 
| +    return partial(_make_argument, *args, **kwargs) | 
| + | 
| + | 
| +def argparse_command(valid_platforms=None, arguments=()): | 
| +    def wrapper(func): | 
| +        def func_wrapper(*args, **kwargs): | 
| +            return func(*args, **kwargs) | 
|  | 
| +        short_desc, long_desc = func.__doc__.split(os.linesep + os.linesep, 1) | 
|  | 
| -def runBuild(baseDir, scriptName, opts, args, type): | 
| +        ALL_COMMANDS.append({ | 
| +            'name': func.__name__, | 
| +            'description': long_desc, | 
| +            'help_text': short_desc, | 
| +            'valid_platforms': valid_platforms or KNOWN_PLATFORMS, | 
| +            'function': func, | 
| +            'arguments': arguments, | 
| +        }) | 
| +        return func_wrapper | 
| + | 
| +    return wrapper | 
| + | 
| + | 
| +def make_subcommand(name, description, help_text, function, arguments): | 
| +    new_parser = SUB_PARSERS.add_parser( | 
| +        name, description=description, help=help_text, | 
| +        formatter_class=argparse.RawDescriptionHelpFormatter, | 
| +    ) | 
| + | 
| +    for argument in arguments: | 
| +        argument(parser=new_parser) | 
| + | 
| +    new_parser.set_defaults(function=function) | 
| +    return new_parser | 
| + | 
| + | 
| +def build_available_subcommands(base_dir): | 
| +    """Build subcommands, which are available for the repository in base_dir. | 
| + | 
| +    Search 'base_dir' for existing metadata.<type> files and make <type> an | 
| +    avaible choice for the subcommands, intersected with their respective valid | 
| +    platforms. | 
| + | 
| +    If no valid platform is found for a subcommand, it get's omitted. | 
| +    """ | 
| +    if build_available_subcommands._result is not None: | 
| +        # Tests might run this code multiple times, make sure the collection | 
| +        # of platforms is only run once. | 
| +        return build_available_subcommands._result | 
| + | 
| +    types = set() | 
| +    for p in KNOWN_PLATFORMS: | 
| +        if os.path.exists(os.path.join(base_dir, 'metadata.' + p)): | 
| +            types.add(p) | 
| + | 
| +    if len(types) == 0: | 
| +        logging.error('No metadata file found in this repository. Expecting ' | 
| +                      'one or more of {} to be present.'.format( | 
| +                          ', '.join('metadata.' + p for p in KNOWN_PLATFORMS))) | 
| +        build_available_subcommands._result = False | 
| +        return False | 
| + | 
| +    for command_params in ALL_COMMANDS: | 
| +        platforms = types.intersection(command_params.pop('valid_platforms')) | 
| +        if len(platforms) > 1: | 
| +            command_params['arguments'] += ( | 
| +                make_argument('-t', '--type', dest='platform', | 
| +                              choices=platforms), | 
| +            ) | 
| +            make_subcommand(**command_params) | 
| +        elif len(platforms) == 1: | 
| +            sub_parser = make_subcommand(**command_params) | 
| +            sub_parser.set_defaults(platform=platforms.pop()) | 
| + | 
| +    build_available_subcommands._result = True | 
| +    return True | 
| + | 
| + | 
| +build_available_subcommands._result = None | 
| + | 
| + | 
| +@argparse_command( | 
| +    valid_platforms={'chrome', 'gecko', 'edge'}, | 
| +    arguments=( | 
| +        make_argument( | 
| +            '-b', '--build-num', dest='build_num', | 
| +            help=('Use given build number (if omitted the build number will ' | 
| +                  'be retrieved from Mercurial)')), | 
| +        make_argument( | 
| +            '-k', '--key', dest='key_file', | 
| +            help=('File containing private key and certificates required to ' | 
| +                  'sign the package')), | 
| +        make_argument( | 
| +            '-r', '--release', action='store_true', | 
| +            help='Create a release build'), | 
| +        make_argument('output_file', nargs='?') | 
| +    ) | 
| +) | 
| +def build(base_dir, build_num, key_file, release, output_file, platform, | 
| +          **kwargs): | 
| +    """ | 
| +    Create a build. | 
| + | 
| +    Creates an extension build with given file name. If output_file is missing | 
| +    a default name will be chosen. | 
| +    """ | 
| kwargs = {} | 
| -    for option, value in opts: | 
| -        if option in {'-b', '--build'}: | 
| -            kwargs['buildNum'] = value | 
| -            if type != 'gecko' and not kwargs['buildNum'].isdigit(): | 
| -                raise TypeError('Build number must be numerical') | 
| -        elif option in {'-k', '--key'}: | 
| -            kwargs['keyFile'] = value | 
| -        elif option in {'-r', '--release'}: | 
| -            kwargs['releaseBuild'] = True | 
| -    if len(args) > 0: | 
| -        kwargs['outFile'] = args[0] | 
| - | 
| -    if type in {'chrome', 'gecko'}: | 
| -        import buildtools.packagerChrome as packager | 
| -    elif type == 'edge': | 
| +    if platform == 'edge': | 
| import buildtools.packagerEdge as packager | 
| +    else: | 
| +        import buildtools.packagerChrome as packager | 
| + | 
| +    kwargs['keyFile'] = key_file | 
| +    kwargs['outFile'] = output_file | 
| +    kwargs['releaseBuild'] = release | 
| +    kwargs['buildNum'] = build_num | 
| + | 
| +    packager.createBuild(base_dir, type=platform, **kwargs) | 
|  | 
| -    packager.createBuild(baseDir, type=type, **kwargs) | 
|  | 
| +@argparse_command( | 
| +    valid_platforms={'chrome', 'gecko', 'edge'} | 
| +) | 
| +def devenv(base_dir, platform, **kwargs): | 
| +    """ | 
| +    Set up a development environment. | 
|  | 
| -def createDevEnv(baseDir, scriptName, opts, args, type): | 
| -    if type == 'edge': | 
| +    Will set up or update the devenv folder as an unpacked extension folder ' | 
| +    for development. | 
| +    """ | 
| +    if platform == 'edge': | 
| import buildtools.packagerEdge as packager | 
| else: | 
| import buildtools.packagerChrome as packager | 
|  | 
| file = StringIO() | 
| -    packager.createBuild(baseDir, type=type, outFile=file, devenv=True, releaseBuild=True) | 
| +    packager.createBuild(base_dir, type=platform, outFile=file, devenv=True, | 
| +                         releaseBuild=True) | 
|  | 
| from buildtools.packager import getDevEnvPath | 
| -    devenv_dir = getDevEnvPath(baseDir, type) | 
| +    devenv_dir = getDevEnvPath(base_dir, platform) | 
|  | 
| shutil.rmtree(devenv_dir, ignore_errors=True) | 
|  | 
| @@ -215,129 +179,146 @@ def createDevEnv(baseDir, scriptName, opts, args, type): | 
| zip_file.extractall(devenv_dir) | 
|  | 
|  | 
| -def readLocaleConfig(baseDir, type, metadata): | 
| -    if type != 'generic': | 
| +def read_locale_config(base_dir, platform, metadata): | 
| +    if platform != 'generic': | 
| import buildtools.packagerChrome as packager | 
| -        localeDir = os.path.join(baseDir, '_locales') | 
| -        localeConfig = { | 
| +        locale_dir = os.path.join(base_dir, '_locales') | 
| +        locale_config = { | 
| 'default_locale': packager.defaultLocale, | 
| } | 
| else: | 
| -        localeDir = os.path.join( | 
| -            baseDir, *metadata.get('locales', 'base_path').split('/') | 
| +        locale_dir = os.path.join( | 
| +            base_dir, *metadata.get('locales', 'base_path').split('/') | 
| ) | 
| -        localeConfig = { | 
| +        locale_config = { | 
| 'default_locale': metadata.get('locales', 'default_locale') | 
| } | 
|  | 
| -    localeConfig['base_path'] = localeDir | 
| +    locale_config['base_path'] = locale_dir | 
|  | 
| -    locales = [(locale.replace('_', '-'), os.path.join(localeDir, locale)) | 
| -               for locale in os.listdir(localeDir)] | 
| -    localeConfig['locales'] = dict(locales) | 
| +    locales = [(locale.replace('_', '-'), os.path.join(locale_dir, locale)) | 
| +               for locale in os.listdir(locale_dir)] | 
| +    locale_config['locales'] = dict(locales) | 
|  | 
| -    return localeConfig | 
| +    return locale_config | 
|  | 
|  | 
| -def setupTranslations(baseDir, scriptName, opts, args, type): | 
| -    if len(args) < 1: | 
| -        print 'Project key is required to update translation master files.' | 
| -        usage(scriptName, type, 'setuptrans') | 
| -        return | 
| +project_key_argument = make_argument( | 
| +    'project_key', help='The crowdin project key.' | 
| +) | 
| + | 
|  | 
| -    key = args[0] | 
| +@argparse_command( | 
| +    arguments=(project_key_argument, ) | 
| +) | 
| +def setuptrans(base_dir, project_key, platform, **kwargs): | 
| +    """ | 
| +    Set up translation languages. | 
|  | 
| +    Set up translation languages for the project on crowdin.com. | 
| +    """ | 
| from buildtools.packager import readMetadata | 
| -    metadata = readMetadata(baseDir, type) | 
| +    metadata = readMetadata(base_dir, platform) | 
|  | 
| basename = metadata.get('general', 'basename') | 
| -    localeConfig = readLocaleConfig(baseDir, type, metadata) | 
| +    locale_config = read_locale_config(base_dir, platform, metadata) | 
|  | 
| import buildtools.localeTools as localeTools | 
| -    localeTools.setupTranslations(localeConfig, basename, key) | 
| - | 
| +    localeTools.setupTranslations(locale_config, basename, project_key) | 
|  | 
| -def updateTranslationMaster(baseDir, scriptName, opts, args, type): | 
| -    if len(args) < 1: | 
| -        print 'Project key is required to update translation master files.' | 
| -        usage(scriptName, type, 'translate') | 
| -        return | 
|  | 
| -    key = args[0] | 
| +@argparse_command( | 
| +    arguments=(project_key_argument, ) | 
| +) | 
| +def translate(base_dir, project_key, platform, **kwargs): | 
| +    """ | 
| +    Update translation master files. | 
|  | 
| +    Update the translation master files in the project on crowdin.com. | 
| +    """ | 
| from buildtools.packager import readMetadata | 
| -    metadata = readMetadata(baseDir, type) | 
| +    metadata = readMetadata(base_dir, platform) | 
|  | 
| basename = metadata.get('general', 'basename') | 
| -    localeConfig = readLocaleConfig(baseDir, type, metadata) | 
| +    locale_config = read_locale_config(base_dir, platform, metadata) | 
|  | 
| -    defaultLocaleDir = os.path.join(localeConfig['base_path'], | 
| -                                    localeConfig['default_locale']) | 
| +    default_locale_dir = os.path.join(locale_config['base_path'], | 
| +                                      locale_config['default_locale']) | 
|  | 
| import buildtools.localeTools as localeTools | 
| -    localeTools.updateTranslationMaster(localeConfig, metadata, defaultLocaleDir, | 
| -                                        basename, key) | 
| - | 
| +    localeTools.updateTranslationMaster(locale_config, metadata, | 
| +                                        default_locale_dir, basename, | 
| +                                        project_key) | 
|  | 
| -def uploadTranslations(baseDir, scriptName, opts, args, type): | 
| -    if len(args) < 1: | 
| -        print 'Project key is required to upload existing translations.' | 
| -        usage(scriptName, type, 'uploadtrans') | 
| -        return | 
|  | 
| -    key = args[0] | 
| +@argparse_command( | 
| +    arguments=(project_key_argument, ) | 
| +) | 
| +def uploadtrans(base_dir, project_key, platform, **kwargs): | 
| +    """ | 
| +    Upload existing translations. | 
|  | 
| +    Upload already existing translations to the project on crowdin.com. | 
| +    """ | 
| from buildtools.packager import readMetadata | 
| -    metadata = readMetadata(baseDir, type) | 
| +    metadata = readMetadata(base_dir, platform) | 
|  | 
| basename = metadata.get('general', 'basename') | 
| -    localeConfig = readLocaleConfig(baseDir, type, metadata) | 
| +    locale_config = read_locale_config(base_dir, platform, metadata) | 
|  | 
| import buildtools.localeTools as localeTools | 
| -    for locale, localeDir in localeConfig['locales'].iteritems(): | 
| -        if locale != localeConfig['default_locale'].replace('_', '-'): | 
| -            localeTools.uploadTranslations(localeConfig, metadata, localeDir, locale, | 
| -                                           basename, key) | 
| +    for locale, locale_dir in locale_config['locales'].iteritems(): | 
| +        if locale != locale_config['default_locale'].replace('_', '-'): | 
| +            localeTools.uploadTranslations(locale_config, metadata, locale_dir, | 
| +                                           locale, basename, project_key) | 
|  | 
|  | 
| -def getTranslations(baseDir, scriptName, opts, args, type): | 
| -    if len(args) < 1: | 
| -        print 'Project key is required to update translation master files.' | 
| -        usage(scriptName, type, 'translate') | 
| -        return | 
| - | 
| -    key = args[0] | 
| +@argparse_command( | 
| +    arguments=(project_key_argument, ) | 
| +) | 
| +def gettranslations(base_dir, project_key, platform, **kwargs): | 
| +    """ | 
| +    Download translation updates. | 
|  | 
| +    Download updated translations from crowdin.com. | 
| +    """ | 
| from buildtools.packager import readMetadata | 
| -    metadata = readMetadata(baseDir, type) | 
| +    metadata = readMetadata(base_dir, platform) | 
|  | 
| basename = metadata.get('general', 'basename') | 
| -    localeConfig = readLocaleConfig(baseDir, type, metadata) | 
| +    locale_config = read_locale_config(base_dir, platform, metadata) | 
|  | 
| import buildtools.localeTools as localeTools | 
| -    localeTools.getTranslations(localeConfig, basename, key) | 
| +    localeTools.getTranslations(locale_config, basename, project_key) | 
|  | 
|  | 
| -def generateDocs(baseDir, scriptName, opts, args, type): | 
| -    if len(args) == 0: | 
| -        print 'No target directory specified for the documentation' | 
| -        usage(scriptName, type, 'docs') | 
| -        return | 
| -    targetDir = args[0] | 
| +@argparse_command( | 
| +    valid_platforms={'chrome'}, | 
| +    arguments=( | 
| +        make_argument('target_dir'), | 
| +        make_argument('-q', '--quiet', help='Suppress JsDoc output'), | 
| +    ) | 
| +) | 
| +def docs(base_dir, target_dir, quiet, platform, **kwargs): | 
| +    """ | 
| +    Generate documentation (requires node.js). | 
|  | 
| -    source_dir = os.path.join(baseDir, 'lib') | 
| -    sources = [source_dir] | 
| +    Generate documentation files and write them into the specified directory. | 
| +    """ | 
| +    source_dir = os.path.join(base_dir, 'lib') | 
|  | 
| -    # JSDoc struggles wih huge objects: https://github.com/jsdoc3/jsdoc/issues/976 | 
| -    if type == 'chrome': | 
| -        sources = [os.path.join(source_dir, filename) for filename in os.listdir(source_dir) if filename != 'publicSuffixList.js'] | 
| +    # JSDoc struggles wih huge objects: | 
| +    # https://github.com/jsdoc3/jsdoc/issues/976 | 
| +    sources = [os.path.join(source_dir, filename) | 
| +               for filename in os.listdir(source_dir) | 
| +               if filename != 'publicSuffixList.js'] | 
|  | 
| buildtools_path = os.path.dirname(__file__) | 
| config = os.path.join(buildtools_path, 'jsdoc.conf') | 
|  | 
| -    command = ['npm', 'run-script', 'jsdoc', '--', '--destination', targetDir, | 
| +    command = ['npm', 'run-script', 'jsdoc', '--', '--destination', target_dir, | 
| '--configure', config] + sources | 
| -    if any(opt in {'-q', '--quiet'} for opt, _ in opts): | 
| +    if quiet: | 
| process = subprocess.Popen(command, stdout=subprocess.PIPE, | 
| stderr=subprocess.PIPE, cwd=buildtools_path) | 
| stderr = process.communicate()[1] | 
| @@ -349,167 +330,71 @@ def generateDocs(baseDir, scriptName, opts, args, type): | 
| subprocess.check_call(command, cwd=buildtools_path) | 
|  | 
|  | 
| -def runReleaseAutomation(baseDir, scriptName, opts, args, type): | 
| -    keyFile = None | 
| -    downloadsRepo = os.path.join(baseDir, '..', 'downloads') | 
| -    for option, value in opts: | 
| -        if option in {'-k', '--key'}: | 
| -            keyFile = value | 
| -        elif option in {'-d', '--downloads'}: | 
| -            downloadsRepo = value | 
| +def valid_version_format(value): | 
| +    if re.search(r'[^\d\.]', value): | 
| +        raise argparse.ArgumentTypeError('Wrong version number format') | 
|  | 
| -    if len(args) == 0: | 
| -        print 'No version number specified for the release' | 
| -        usage(scriptName, type, 'release') | 
| -        return | 
| -    version = args[0] | 
| -    if re.search(r'[^\d\.]', version): | 
| -        print 'Wrong version number format' | 
| -        usage(scriptName, type, 'release') | 
| -        return | 
| +    return value | 
|  | 
| -    if type == 'chrome' and keyFile is None: | 
| -        print >>sys.stderr, 'Error: you must specify a key file for this release' | 
| -        usage(scriptName, type, 'release') | 
| + | 
| +@argparse_command( | 
| +    valid_platforms={'chrome', 'edge'}, | 
| +    arguments=( | 
| +        make_argument( | 
| +            '-k', '--key', dest='key_file', | 
| +            help=('File containing private key and certificates required to ' | 
| +                  'sign the release.')), | 
| +        make_argument( | 
| +            '-d', '--downloads-repository', dest='downloads_repository', | 
| +            help=('Directory containing downloads repository (if omitted ' | 
| +                  '../downloads is assumed)')), | 
| +        make_argument( | 
| +            'version', help='Version number of the release', | 
| +            type=valid_version_format) | 
| +    ) | 
| +) | 
| +def release(base_dir, downloads_repository, key_file, platform, version, | 
| +            **kwargs): | 
| +    """ | 
| +    Run release automation. | 
| + | 
| +    Note: If you are not the project owner then you probably don't want to run | 
| +    this! | 
| + | 
| +    Run release automation: create downloads for the new version, tag source | 
| +    code repository as well as downloads and buildtools repository. | 
| +    """ | 
| +    if downloads_repository is None: | 
| +        downloads_repository = os.path.join(base_dir, os.pardir, 'downloads') | 
| + | 
| +    if platform == 'chrome' and key_file is None: | 
| +        logging.error('You must specify a key file for this release') | 
| return | 
|  | 
| import buildtools.releaseAutomation as releaseAutomation | 
| -    releaseAutomation.run(baseDir, type, version, keyFile, downloadsRepo) | 
| +    releaseAutomation.run(base_dir, platform, version, key_file, | 
| +                          downloads_repository) | 
| + | 
|  | 
| +@argparse_command(valid_platforms={'chrome'}) | 
| +def updatepsl(base_dir, **kwargs): | 
| +    """Update Public Suffix List. | 
|  | 
| -def updatePSL(baseDir, scriptName, opts, args, type): | 
| +    Downloads Public Suffix List (see http://publicsuffix.org/) and generates | 
| +    lib/publicSuffixList.js from it. | 
| +    """ | 
| import buildtools.publicSuffixListUpdater as publicSuffixListUpdater | 
| -    publicSuffixListUpdater.updatePSL(baseDir) | 
| - | 
| - | 
| -with addCommand(lambda baseDir, scriptName, opts, args, type: usage(scriptName, type), ('help', '-h', '--help')) as command: | 
| -    command.shortDescription = 'Show this message' | 
| - | 
| -with addCommand(runBuild, 'build') as command: | 
| -    command.shortDescription = 'Create a build' | 
| -    command.description = 'Creates an extension build with given file name. If output_file is missing a default name will be chosen.' | 
| -    command.params = '[options] [output_file]' | 
| -    command.addOption('Use given build number (if omitted the build number will be retrieved from Mercurial)', short='b', long='build', value='num') | 
| -    command.addOption('File containing private key and certificates required to sign the package', short='k', long='key', value='file', types={'chrome'}) | 
| -    command.addOption('Create a release build', short='r', long='release') | 
| -    command.supportedTypes = {'gecko', 'chrome', 'edge'} | 
| - | 
| -with addCommand(createDevEnv, 'devenv') as command: | 
| -    command.shortDescription = 'Set up a development environment' | 
| -    command.description = 'Will set up or update the devenv folder as an unpacked extension folder for development.' | 
| -    command.supportedTypes = {'gecko', 'chrome', 'edge'} | 
| - | 
| -with addCommand(setupTranslations, 'setuptrans') as command: | 
| -    command.shortDescription = 'Sets up translation languages' | 
| -    command.description = 'Sets up translation languages for the project on crowdin.net.' | 
| -    command.params = '[options] project-key' | 
| - | 
| -with addCommand(updateTranslationMaster, 'translate') as command: | 
| -    command.shortDescription = 'Updates translation master files' | 
| -    command.description = 'Updates the translation master files in the project on crowdin.net.' | 
| -    command.params = '[options] project-key' | 
| - | 
| -with addCommand(uploadTranslations, 'uploadtrans') as command: | 
| -    command.shortDescription = 'Uploads existing translations' | 
| -    command.description = 'Uploads already existing translations to the project on crowdin.net.' | 
| -    command.params = '[options] project-key' | 
| - | 
| -with addCommand(getTranslations, 'gettranslations') as command: | 
| -    command.shortDescription = 'Downloads translation updates' | 
| -    command.description = 'Downloads updated translations from crowdin.net.' | 
| -    command.params = '[options] project-key' | 
| - | 
| -with addCommand(generateDocs, 'docs') as command: | 
| -    command.shortDescription = 'Generate documentation (requires node.js)' | 
| -    command.description = ('Generate documentation files and write them into ' | 
| -                           'the specified directory.') | 
| -    command.addOption('Suppress JsDoc output', short='q', long='quiet') | 
| -    command.params = '[options] <directory>' | 
| -    command.supportedTypes = {'chrome'} | 
| - | 
| -with addCommand(runReleaseAutomation, 'release') as command: | 
| -    command.shortDescription = 'Run release automation' | 
| -    command.description = 'Note: If you are not the project owner then you '        "probably don't want to run this!\n\n"        'Runs release automation: creates downloads for the new version, tags '        'source code repository as well as downloads and buildtools repository.' | 
| -    command.addOption('File containing private key and certificates required to sign the release.', short='k', long='key', value='file', types={'chrome', 'edge'}) | 
| -    command.addOption('Directory containing downloads repository (if omitted ../downloads is assumed)', short='d', long='downloads', value='dir') | 
| -    command.params = '[options] <version>' | 
| -    command.supportedTypes = {'chrome', 'edge'} | 
| - | 
| -with addCommand(updatePSL, 'updatepsl') as command: | 
| -    command.shortDescription = 'Updates Public Suffix List' | 
| -    command.description = 'Downloads Public Suffix List (see http://publicsuffix.org/) and generates lib/publicSuffixList.js from it.' | 
| -    command.supportedTypes = {'chrome'} | 
| - | 
| - | 
| -def getType(baseDir, scriptName, args): | 
| -    # Look for an explicit type parameter (has to be the first parameter) | 
| -    if len(args) >= 2 and args[0] == '-t': | 
| -        type = args[1] | 
| -        del args[1] | 
| -        del args[0] | 
| -        if type not in knownTypes: | 
| -            print ''' | 
| -Unknown type %s specified, supported types are: %s | 
| -''' % (type, ', '.join(knownTypes)) | 
| -            return None | 
| -        return type | 
| - | 
| -    # Try to guess repository type | 
| -    types = [] | 
| -    for t in knownTypes: | 
| -        if os.path.exists(os.path.join(baseDir, 'metadata.%s' % t)): | 
| -            types.append(t) | 
| - | 
| -    if len(types) == 1: | 
| -        return types[0] | 
| -    elif len(types) > 1: | 
| -        print ''' | 
| -Ambiguous repository type, please specify -t parameter explicitly, e.g. | 
| -%s -t %s build | 
| -''' % (scriptName, types[0]) | 
| -        return None | 
| -    else: | 
| -        print ''' | 
| -No metadata file found in this repository, a metadata file like | 
| -metadata.* is required. | 
| -''' | 
| -        return None | 
| +    publicSuffixListUpdater.updatePSL(base_dir) | 
|  | 
|  | 
| -def processArgs(baseDir, args): | 
| -    global commands | 
| +def process_args(base_dir, *args): | 
| +    if build_available_subcommands(base_dir): | 
| +        MAIN_PARSER.set_defaults(base_dir=base_dir) | 
|  | 
| -    scriptName = os.path.basename(args[0]) | 
| -    args = args[1:] | 
| -    type = getType(baseDir, scriptName, args) | 
| -    if type == None: | 
| -        return | 
| +        # If no args are provided, this module is run directly from the command | 
| +        # line. argparse will take care of consuming sys.argv. | 
| +        arguments = MAIN_PARSER.parse_args(args if len(args) > 0 else None) | 
|  | 
| -    if len(args) == 0: | 
| -        args = ['build'] | 
| -        print ''' | 
| -No command given, assuming "build". For a list of commands run: | 
| - | 
| -  %s help | 
| -''' % scriptName | 
| - | 
| -    command = args[0] | 
| -    if command in commands: | 
| -        if commands[command].isSupported(type): | 
| -            try: | 
| -                opts, args = commands[command].parseArgs(type, args[1:]) | 
| -            except GetoptError as e: | 
| -                print str(e) | 
| -                usage(scriptName, type, command) | 
| -                sys.exit(2) | 
| -            for option, value in opts: | 
| -                if option in {'-h', '--help'}: | 
| -                    usage(scriptName, type, command) | 
| -                    sys.exit() | 
| -            commands[command](baseDir, scriptName, opts, args, type) | 
| -        else: | 
| -            print 'Command %s is not supported for this application type' % command | 
| -            usage(scriptName, type) | 
| -    else: | 
| -        print 'Command %s is unrecognized' % command | 
| -        usage(scriptName, type) | 
| +        function = arguments.function | 
| +        del arguments.function | 
| +        function(**vars(arguments)) | 
|  |