Exemple #1
0
def init_cli(verbose_output=None):
    # Click is imported here to run it after check_environment()
    import click

    class Deprecation(object):
        """Construct deprecation notice for help messages"""
        def __init__(self, deprecated=False):
            self.deprecated = deprecated
            self.since = None
            self.removed = None
            self.exit_with_error = None
            self.custom_message = ''

            if isinstance(deprecated, dict):
                self.custom_message = deprecated.get('message', '')
                self.since = deprecated.get('since', None)
                self.removed = deprecated.get('removed', None)
                self.exit_with_error = deprecated.get('exit_with_error', None)
            elif isinstance(deprecated, str):
                self.custom_message = deprecated

        def full_message(self, type='Option'):
            if self.exit_with_error:
                return '%s is deprecated %sand was removed%s.%s' % (
                    type,
                    'since %s ' % self.since if self.since else '',
                    ' in %s' % self.removed if self.removed else '',
                    ' %s' % self.custom_message if self.custom_message else '',
                )
            else:
                return '%s is deprecated %sand will be removed in%s.%s' % (
                    type,
                    'since %s ' % self.since if self.since else '',
                    ' %s' % self.removed if self.removed else ' future versions',
                    ' %s' % self.custom_message if self.custom_message else '',
                )

        def help(self, text, type='Option', separator=' '):
            text = text or ''
            return self.full_message(type) + separator + text if self.deprecated else text

        def short_help(self, text):
            text = text or ''
            return ('Deprecated! ' + text) if self.deprecated else text

    def check_deprecation(ctx):
        """Prints deprecation warnings for arguments in given context"""
        for option in ctx.command.params:
            default = () if option.multiple else option.default
            if isinstance(option, Option) and option.deprecated and ctx.params[option.name] != default:
                deprecation = Deprecation(option.deprecated)
                if deprecation.exit_with_error:
                    raise FatalError('Error: %s' % deprecation.full_message('Option "%s"' % option.name))
                else:
                    print_warning('Warning: %s' % deprecation.full_message('Option "%s"' % option.name))

    class Task(object):
        def __init__(self, callback, name, aliases, dependencies, order_dependencies, action_args):
            self.callback = callback
            self.name = name
            self.dependencies = dependencies
            self.order_dependencies = order_dependencies
            self.action_args = action_args
            self.aliases = aliases

        def __call__(self, context, global_args, action_args=None):
            if action_args is None:
                action_args = self.action_args

            self.callback(self.name, context, global_args, **action_args)

    class Action(click.Command):
        def __init__(
                self,
                name=None,
                aliases=None,
                deprecated=False,
                dependencies=None,
                order_dependencies=None,
                hidden=False,
                **kwargs):
            super(Action, self).__init__(name, **kwargs)

            self.name = self.name or self.callback.__name__
            self.deprecated = deprecated
            self.hidden = hidden

            if aliases is None:
                aliases = []
            self.aliases = aliases

            self.help = self.help or self.callback.__doc__
            if self.help is None:
                self.help = ''

            if dependencies is None:
                dependencies = []

            if order_dependencies is None:
                order_dependencies = []

            # Show first line of help if short help is missing
            self.short_help = self.short_help or self.help.split('\n')[0]

            if deprecated:
                deprecation = Deprecation(deprecated)
                self.short_help = deprecation.short_help(self.short_help)
                self.help = deprecation.help(self.help, type='Command', separator='\n')

            # Add aliases to help string
            if aliases:
                aliases_help = 'Aliases: %s.' % ', '.join(aliases)

                self.help = '\n'.join([self.help, aliases_help])
                self.short_help = ' '.join([aliases_help, self.short_help])

            self.unwrapped_callback = self.callback
            if self.callback is not None:

                def wrapped_callback(**action_args):
                    return Task(
                        callback=self.unwrapped_callback,
                        name=self.name,
                        dependencies=dependencies,
                        order_dependencies=order_dependencies,
                        action_args=action_args,
                        aliases=self.aliases,
                    )

                self.callback = wrapped_callback

        def invoke(self, ctx):
            if self.deprecated:
                deprecation = Deprecation(self.deprecated)
                message = deprecation.full_message('Command "%s"' % self.name)

                if deprecation.exit_with_error:
                    raise FatalError('Error: %s' % message)
                else:
                    print_warning('Warning: %s' % message)

                self.deprecated = False  # disable Click's built-in deprecation handling

            # Print warnings for options
            check_deprecation(ctx)
            return super(Action, self).invoke(ctx)

    class Argument(click.Argument):
        """
        Positional argument

        names - alias of 'param_decls'
        """
        def __init__(self, **kwargs):
            names = kwargs.pop('names')
            super(Argument, self).__init__(names, **kwargs)

    class Scope(object):
        """
            Scope for sub-command option.
            possible values:
            - default - only available on defined level (global/action)
            - global - When defined for action, also available as global
            - shared - Opposite to 'global': when defined in global scope, also available for all actions
        """

        SCOPES = ('default', 'global', 'shared')

        def __init__(self, scope=None):
            if scope is None:
                self._scope = 'default'
            elif isinstance(scope, str) and scope in self.SCOPES:
                self._scope = scope
            elif isinstance(scope, Scope):
                self._scope = str(scope)
            else:
                raise FatalError('Unknown scope for option: %s' % scope)

        @property
        def is_global(self):
            return self._scope == 'global'

        @property
        def is_shared(self):
            return self._scope == 'shared'

        def __str__(self):
            return self._scope

    class Option(click.Option):
        """Option that knows whether it should be global"""
        def __init__(self, scope=None, deprecated=False, hidden=False, **kwargs):
            """
            Keyword arguments additional to Click's Option class:

            names - alias of 'param_decls'
            deprecated - marks option as deprecated. May be boolean, string (with custom deprecation message)
            or dict with optional keys:
                since: version of deprecation
                removed: version when option will be removed
                custom_message:  Additional text to deprecation warning
            """

            kwargs['param_decls'] = kwargs.pop('names')
            super(Option, self).__init__(**kwargs)

            self.deprecated = deprecated
            self.scope = Scope(scope)
            self.hidden = hidden

            if deprecated:
                deprecation = Deprecation(deprecated)
                self.help = deprecation.help(self.help)

            if self.envvar:
                self.help += ' The default value can be set with the %s environment variable.' % self.envvar

            if self.scope.is_global:
                self.help += ' This option can be used at most once either globally, or for one subcommand.'

        def get_help_record(self, ctx):
            # Backport "hidden" parameter to click 5.0
            if self.hidden:
                return

            return super(Option, self).get_help_record(ctx)

    class CLI(click.MultiCommand):
        """Action list contains all actions with options available for CLI"""
        def __init__(self, all_actions=None, verbose_output=None, help=None):
            super(CLI, self).__init__(
                chain=True,
                invoke_without_command=True,
                result_callback=self.execute_tasks,
                context_settings={'max_content_width': 140},
                help=help,
            )
            self._actions = {}
            self.global_action_callbacks = []
            self.commands_with_aliases = {}

            if verbose_output is None:
                verbose_output = []

            self.verbose_output = verbose_output

            if all_actions is None:
                all_actions = {}

            shared_options = []

            # Global options
            for option_args in all_actions.get('global_options', []):
                option = Option(**option_args)
                self.params.append(option)

                if option.scope.is_shared:
                    shared_options.append(option)

            # Global options validators
            self.global_action_callbacks = all_actions.get('global_action_callbacks', [])

            # Actions
            for name, action in all_actions.get('actions', {}).items():
                arguments = action.pop('arguments', [])
                options = action.pop('options', [])

                if arguments is None:
                    arguments = []

                if options is None:
                    options = []

                self._actions[name] = Action(name=name, **action)
                for alias in [name] + action.get('aliases', []):
                    self.commands_with_aliases[alias] = name

                for argument_args in arguments:
                    self._actions[name].params.append(Argument(**argument_args))

                # Add all shared options
                for option in shared_options:
                    self._actions[name].params.append(option)

                for option_args in options:
                    option = Option(**option_args)

                    if option.scope.is_shared:
                        raise FatalError(
                            '"%s" is defined for action "%s". '
                            ' "shared" options can be declared only on global level' % (option.name, name))

                    # Promote options to global if see for the first time
                    if option.scope.is_global and option.name not in [o.name for o in self.params]:
                        self.params.append(option)

                    self._actions[name].params.append(option)

        def list_commands(self, ctx):
            return sorted(filter(lambda name: not self._actions[name].hidden, self._actions))

        def get_command(self, ctx, name):
            if name in self.commands_with_aliases:
                return self._actions.get(self.commands_with_aliases.get(name))

            # Trying fallback to build target (from "all" action) if command is not known
            else:
                return Action(name=name, callback=self._actions.get('fallback').unwrapped_callback)

        def _print_closing_message(self, args, actions):
            # print a closing message of some kind
            #
            if any(t in str(actions) for t in ('flash', 'dfu', 'uf2', 'uf2-app')):
                print('Done')
                return

            if not os.path.exists(os.path.join(args.build_dir, 'flasher_args.json')):
                print('Done')
                return

            # Otherwise, if we built any binaries print a message about
            # how to flash them
            def print_flashing_message(title, key):
                with open(os.path.join(args.build_dir, 'flasher_args.json')) as f:
                    flasher_args = json.load(f)

                def flasher_path(f):
                    return _safe_relpath(os.path.join(args.build_dir, f))

                if key != 'project':  # flashing a single item
                    if key not in flasher_args:
                        # This is the case for 'idf.py bootloader' if Secure Boot is on, need to follow manual flashing steps
                        print('\n%s build complete.' % title)
                        return
                    cmd = ''
                    if (key == 'bootloader'):  # bootloader needs --flash-mode, etc to be passed in
                        cmd = ' '.join(flasher_args['write_flash_args']) + ' '

                    cmd += flasher_args[key]['offset'] + ' '
                    cmd += flasher_path(flasher_args[key]['file'])
                else:  # flashing the whole project
                    cmd = ' '.join(flasher_args['write_flash_args']) + ' '
                    flash_items = sorted(
                        ((o, f) for (o, f) in flasher_args['flash_files'].items() if len(o) > 0),
                        key=lambda x: int(x[0], 0),
                    )
                    for o, f in flash_items:
                        cmd += o + ' ' + flasher_path(f) + ' '

                print('\n%s build complete. To flash, run this command:' % title)

                print(
                    '%s %s -p %s -b %s --before %s --after %s --chip %s %s write_flash %s' % (
                        PYTHON,
                        _safe_relpath('%s/components/esptool_py/esptool/esptool.py' % os.environ['IDF_PATH']),
                        args.port or '(PORT)',
                        args.baud,
                        flasher_args['extra_esptool_args']['before'],
                        flasher_args['extra_esptool_args']['after'],
                        flasher_args['extra_esptool_args']['chip'],
                        '--no-stub' if not flasher_args['extra_esptool_args']['stub'] else '',
                        cmd.strip(),
                    ))
                print(
                    "or run 'idf.py -p %s %s'" % (
                        args.port or '(PORT)',
                        key + '-flash' if key != 'project' else 'flash',
                    ))

            if 'all' in actions or 'build' in actions:
                print_flashing_message('Project', 'project')
            else:
                if 'app' in actions:
                    print_flashing_message('App', 'app')
                if 'partition_table' in actions:
                    print_flashing_message('Partition Table', 'partition_table')
                if 'bootloader' in actions:
                    print_flashing_message('Bootloader', 'bootloader')

        def execute_tasks(self, tasks, **kwargs):
            ctx = click.get_current_context()
            global_args = PropertyDict(kwargs)

            def _help_and_exit():
                print(ctx.get_help())
                ctx.exit()

            # Show warning if some tasks are present several times in the list
            dupplicated_tasks = sorted(
                [item for item, count in Counter(task.name for task in tasks).items() if count > 1])
            if dupplicated_tasks:
                dupes = ', '.join('"%s"' % t for t in dupplicated_tasks)

                print_warning(
                    'WARNING: Command%s found in the list of commands more than once. ' %
                    ('s %s are' % dupes if len(dupplicated_tasks) > 1 else ' %s is' % dupes) +
                    'Only first occurrence will be executed.')

            for task in tasks:
                # Show help and exit if help is in the list of commands
                if task.name == 'help':
                    _help_and_exit()

                # Set propagated global options.
                # These options may be set on one subcommand, but available in the list of global arguments
                for key in list(task.action_args):
                    option = next((o for o in ctx.command.params if o.name == key), None)

                    if option and (option.scope.is_global or option.scope.is_shared):
                        local_value = task.action_args.pop(key)
                        global_value = global_args[key]
                        default = () if option.multiple else option.default

                        if global_value != default and local_value != default and global_value != local_value:
                            raise FatalError(
                                'Option "%s" provided for "%s" is already defined to a different value. '
                                'This option can appear at most once in the command line.' % (key, task.name))
                        if local_value != default:
                            global_args[key] = local_value

            # Show warnings about global arguments
            check_deprecation(ctx)

            # Make sure that define_cache_entry is mutable list and can be modified in callbacks
            global_args.define_cache_entry = list(global_args.define_cache_entry)

            # Execute all global action callback - first from idf.py itself, then from extensions
            for action_callback in ctx.command.global_action_callbacks:
                action_callback(ctx, global_args, tasks)

            # Always show help when command is not provided
            if not tasks:
                _help_and_exit()

            # Build full list of tasks to and deal with dependencies and order dependencies
            tasks_to_run = OrderedDict()
            while tasks:
                task = tasks[0]
                tasks_dict = dict([(t.name, t) for t in tasks])

                dependecies_processed = True

                # If task have some dependecies they have to be executed before the task.
                for dep in task.dependencies:
                    if dep not in tasks_to_run.keys():
                        # If dependent task is in the list of unprocessed tasks move to the front of the list
                        if dep in tasks_dict.keys():
                            dep_task = tasks.pop(tasks.index(tasks_dict[dep]))
                        # Otherwise invoke it with default set of options
                        # and put to the front of the list of unprocessed tasks
                        else:
                            print(
                                'Adding "%s"\'s dependency "%s" to list of commands with default set of options.' %
                                (task.name, dep))
                            dep_task = ctx.invoke(ctx.command.get_command(ctx, dep))

                            # Remove options with global scope from invoke tasks because they are already in global_args
                            for key in list(dep_task.action_args):
                                option = next((o for o in ctx.command.params if o.name == key), None)
                                if option and (option.scope.is_global or option.scope.is_shared):
                                    dep_task.action_args.pop(key)

                        tasks.insert(0, dep_task)
                        dependecies_processed = False

                # Order only dependencies are moved to the front of the queue if they present in command list
                for dep in task.order_dependencies:
                    if dep in tasks_dict.keys() and dep not in tasks_to_run.keys():
                        tasks.insert(0, tasks.pop(tasks.index(tasks_dict[dep])))
                        dependecies_processed = False

                if dependecies_processed:
                    # Remove task from list of unprocessed tasks
                    tasks.pop(0)

                    # And add to the queue
                    if task.name not in tasks_to_run.keys():
                        tasks_to_run.update([(task.name, task)])

            # Run all tasks in the queue
            # when global_args.dry_run is true idf.py works in idle mode and skips actual task execution
            if not global_args.dry_run:
                for task in tasks_to_run.values():
                    name_with_aliases = task.name
                    if task.aliases:
                        name_with_aliases += ' (aliases: %s)' % ', '.join(task.aliases)

                    print('Executing action: %s' % name_with_aliases)
                    task(ctx, global_args, task.action_args)

                self._print_closing_message(global_args, tasks_to_run.keys())

            return tasks_to_run

    # That's a tiny parser that parse project-dir even before constructing
    # fully featured click parser to be sure that extensions are loaded from the right place
    @click.command(
        add_help_option=False,
        context_settings={
            'allow_extra_args': True,
            'ignore_unknown_options': True
        },
    )
    @click.option('-C', '--project-dir', default=os.getcwd(), type=click.Path())
    def parse_project_dir(project_dir):
        return realpath(project_dir)
    # Set `complete_var` to not existing environment variable name to prevent early cmd completion
    project_dir = parse_project_dir(standalone_mode=False, complete_var='_IDF.PY_COMPLETE_NOT_EXISTING')

    all_actions = {}
    # Load extensions from components dir
    idf_py_extensions_path = os.path.join(os.environ['IDF_PATH'], 'tools', 'idf_py_actions')
    extension_dirs = [realpath(idf_py_extensions_path)]
    extra_paths = os.environ.get('IDF_EXTRA_ACTIONS_PATH')
    if extra_paths is not None:
        for path in extra_paths.split(';'):
            path = realpath(path)
            if path not in extension_dirs:
                extension_dirs.append(path)

    extensions = {}
    for directory in extension_dirs:
        if directory and not os.path.exists(directory):
            print_warning('WARNING: Directory with idf.py extensions doesn\'t exist:\n    %s' % directory)
            continue

        sys.path.append(directory)
        for _finder, name, _ispkg in sorted(iter_modules([directory])):
            if name.endswith('_ext'):
                extensions[name] = import_module(name)

    # Load component manager if available and not explicitly disabled
    if os.getenv('IDF_COMPONENT_MANAGER', None) != '0':
        try:
            from idf_component_manager import idf_extensions

            extensions['component_manager_ext'] = idf_extensions
            os.environ['IDF_COMPONENT_MANAGER'] = '1'

        except ImportError:
            pass

    for name, extension in extensions.items():
        try:
            all_actions = merge_action_lists(all_actions, extension.action_extensions(all_actions, project_dir))
        except AttributeError:
            print_warning('WARNING: Cannot load idf.py extension "%s"' % name)

    # Load extensions from project dir
    if os.path.exists(os.path.join(project_dir, 'idf_ext.py')):
        sys.path.append(project_dir)
        try:
            from idf_ext import action_extensions
        except ImportError:
            print_warning('Error importing extension file idf_ext.py. Skipping.')
            print_warning("Please make sure that it contains implementation (even if it's empty) of add_action_extensions")

        try:
            all_actions = merge_action_lists(all_actions, action_extensions(all_actions, project_dir))
        except NameError:
            pass

    cli_help = (
        'ESP-IDF CLI build management tool. '
        'For commands that are not known to idf.py an attempt to execute it as a build system target will be made.')

    return CLI(help=cli_help, verbose_output=verbose_output, all_actions=all_actions)
Exemple #2
0
def action_extensions(base_actions: Dict, project_path: str) -> Any:
    def build_target(target_name: str, ctx: Context, args: PropertyDict) -> None:
        """
        Execute the target build system to build target 'target_name'

        Calls ensure_build_directory() which will run cmake to generate a build
        directory (with the specified generator) as needed.
        """
        hints = not args.no_hints
        ensure_build_directory(args, ctx.info_name)
        run_target(target_name, args, force_progression=GENERATORS[args.generator].get('force_progression', False), hints=hints)

    def size_target(target_name: str, ctx: Context, args: PropertyDict) -> None:
        """
        Builds the app and then executes a size-related target passed in 'target_name'.
        `tool_error_handler` handler is used to suppress errors during the build,
        so size action can run even in case of overflow.

        """

        def tool_error_handler(e: int, stdout: str, stderr: str) -> None:
            print_hints(stdout, stderr)

        hints = not args.no_hints
        ensure_build_directory(args, ctx.info_name)
        run_target('all', args, force_progression=GENERATORS[args.generator].get('force_progression', False),
                   custom_error_handler=tool_error_handler, hints=hints)
        run_target(target_name, args)

    def list_build_system_targets(target_name: str, ctx: Context, args: PropertyDict) -> None:
        """Shows list of targets known to build sytem (make/ninja)"""
        build_target('help', ctx, args)

    def menuconfig(target_name: str, ctx: Context, args: PropertyDict, style: str) -> None:
        """
        Menuconfig target is build_target extended with the style argument for setting the value for the environment
        variable.
        """
        if sys.version_info[0] < 3:
            # The subprocess lib cannot accept environment variables as "unicode".
            # This encoding step is required only in Python 2.
            style = style.encode(sys.getfilesystemencoding() or 'utf-8')
        os.environ['MENUCONFIG_STYLE'] = style
        args.no_hints = True
        build_target(target_name, ctx, args)

    def fallback_target(target_name: str, ctx: Context, args: PropertyDict) -> None:
        """
        Execute targets that are not explicitly known to idf.py
        """
        ensure_build_directory(args, ctx.info_name)

        try:
            subprocess.check_output(GENERATORS[args.generator]['dry_run'] + [target_name], cwd=args.build_dir)

        except Exception:
            if target_name in ['clang-check', 'clang-html-report']:
                raise FatalError('command "{}" requires an additional plugin "pyclang". '
                                 'Please install it via "pip install --upgrade pyclang"'.format(target_name))

            raise FatalError(
                'command "%s" is not known to idf.py and is not a %s target' % (target_name, args.generator))

        run_target(target_name, args)

    def verbose_callback(ctx: Context, param: List, value: str) -> Optional[str]:
        if not value or ctx.resilient_parsing:
            return None

        for line in ctx.command.verbose_output:
            print(line)

        return value

    def clean(action: str, ctx: Context, args: PropertyDict) -> None:
        if not os.path.isdir(args.build_dir):
            print("Build directory '%s' not found. Nothing to clean." % args.build_dir)
            return
        build_target('clean', ctx, args)

    def fullclean(action: str, ctx: Context, args: PropertyDict) -> None:
        build_dir = args.build_dir
        if not os.path.isdir(build_dir):
            print("Build directory '%s' not found. Nothing to clean." % build_dir)
            return
        if len(os.listdir(build_dir)) == 0:
            print("Build directory '%s' is empty. Nothing to clean." % build_dir)
            return

        if not os.path.exists(os.path.join(build_dir, 'CMakeCache.txt')):
            raise FatalError(
                "Directory '%s' doesn't seem to be a CMake build directory. Refusing to automatically "
                "delete files in this directory. Delete the directory manually to 'clean' it." % build_dir)
        red_flags = ['CMakeLists.txt', '.git', '.svn']
        for red in red_flags:
            red = os.path.join(build_dir, red)
            if os.path.exists(red):
                raise FatalError(
                    "Refusing to automatically delete files in directory containing '%s'. Delete files manually if you're sure."
                    % red)
        if args.verbose and len(build_dir) > 1:
            print('The following symlinks were identified and removed:\n%s' % '\n'.join(build_dir))
        for f in os.listdir(build_dir):  # TODO: once we are Python 3 only, this can be os.scandir()
            f = os.path.join(build_dir, f)
            if args.verbose:
                print('Removing: %s' % f)
            if os.path.isdir(f):
                shutil.rmtree(f)
            else:
                os.remove(f)

    def python_clean(action: str, ctx: Context, args: PropertyDict) -> None:
        for root, dirnames, filenames in os.walk(os.environ['IDF_PATH']):
            for d in dirnames:
                if d == '__pycache__':
                    dir_to_delete = os.path.join(root, d)
                    if args.verbose:
                        print('Removing: %s' % dir_to_delete)
                    shutil.rmtree(dir_to_delete)
            for filename in fnmatch.filter(filenames, '*.py[co]'):
                file_to_delete = os.path.join(root, filename)
                if args.verbose:
                    print('Removing: %s' % file_to_delete)
                os.remove(file_to_delete)

    def set_target(action: str, ctx: Context, args: PropertyDict, idf_target: str) -> None:
        if (not args['preview'] and idf_target in PREVIEW_TARGETS):
            raise FatalError(
                "%s is still in preview. You have to append '--preview' option after idf.py to use any preview feature."
                % idf_target)
        args.define_cache_entry.append('IDF_TARGET=' + idf_target)
        sdkconfig_path = os.path.join(args.project_dir, 'sdkconfig')
        sdkconfig_old = sdkconfig_path + '.old'
        if os.path.exists(sdkconfig_old):
            os.remove(sdkconfig_old)
        if os.path.exists(sdkconfig_path):
            os.rename(sdkconfig_path, sdkconfig_old)
        print('Set Target to: %s, new sdkconfig created. Existing sdkconfig renamed to sdkconfig.old.' % idf_target)
        ensure_build_directory(args, ctx.info_name, True)

    def reconfigure(action: str, ctx: Context, args: PropertyDict) -> None:
        ensure_build_directory(args, ctx.info_name, True)

    def validate_root_options(ctx: Context, args: PropertyDict, tasks: List) -> None:
        args.project_dir = realpath(args.project_dir)
        if args.build_dir is not None and args.project_dir == realpath(args.build_dir):
            raise FatalError(
                'Setting the build directory to the project directory is not supported. Suggest dropping '
                "--build-dir option, the default is a 'build' subdirectory inside the project directory.")
        if args.build_dir is None:
            args.build_dir = os.path.join(args.project_dir, 'build')
        args.build_dir = realpath(args.build_dir)

    def idf_version_callback(ctx: Context, param: str, value: str) -> None:
        if not value or ctx.resilient_parsing:
            return

        version = idf_version()

        if not version:
            raise FatalError('ESP-IDF version cannot be determined')

        print('ESP-IDF %s' % version)
        sys.exit(0)

    def list_targets_callback(ctx: Context, param: List, value: int) -> None:
        if not value or ctx.resilient_parsing:
            return

        for target in SUPPORTED_TARGETS:
            print(target)

        if 'preview' in ctx.params:
            for target in PREVIEW_TARGETS:
                print(target)

        sys.exit(0)

    def show_docs(action: str, ctx: Context, args: PropertyDict, no_browser: bool, language: str, starting_page: str, version: str, target: str) -> None:
        if language == 'cn':
            language = 'zh_CN'
        if not version:
            # '0.0-dev' here because if 'dev' in version it will transform in to 'latest'
            version_search = re.search(r'v\d+\.\d+\.?\d*(-dev|-beta\d|-rc)?', idf_version() or '0.0-dev')
            version = version_search.group() if version_search else 'latest'
            if 'dev' in version:
                version = 'latest'
        elif version[0] != 'v':
            version = 'v' + version
        target = target or get_target(args.project_dir) or 'esp32'
        link = '/'.join([URL_TO_DOC, language, version, target, starting_page or ''])
        redirect_link = False
        try:
            req = Request(link)
            webpage = urlopen(req)
            redirect_link = webpage.geturl().endswith('404.html')
        except URLError:
            print("We can't check the link's functionality because you don't have an internet connection")
        if redirect_link:
            print('Target', target, 'doesn\'t exist for version', version)
            link = '/'.join([URL_TO_DOC, language, version, starting_page or ''])
        if not no_browser:
            print('Opening documentation in the default browser:')
            print(link)
            open_new_tab(link)
        else:
            print('Please open the documentation link in the browser:')
            print(link)
        sys.exit(0)

    def get_default_language() -> str:
        try:
            language = 'zh_CN' if locale.getdefaultlocale()[0] == 'zh_CN' else 'en'
        except ValueError:
            language = 'en'
        return language

    root_options = {
        'global_options': [
            {
                'names': ['--version'],
                'help': 'Show IDF version and exit.',
                'is_flag': True,
                'expose_value': False,
                'callback': idf_version_callback,
            },
            {
                'names': ['--list-targets'],
                'help': 'Print list of supported targets and exit.',
                'is_flag': True,
                'expose_value': False,
                'callback': list_targets_callback,
            },
            {
                'names': ['-C', '--project-dir'],
                'scope': 'shared',
                'help': 'Project directory.',
                'type': click.Path(),
                'default': os.getcwd(),
            },
            {
                'names': ['-B', '--build-dir'],
                'help': 'Build directory.',
                'type': click.Path(),
                'default': None,
            },
            {
                'names': ['-w/-n', '--cmake-warn-uninitialized/--no-warnings'],
                'help': ('Enable CMake uninitialized variable warnings for CMake files inside the project directory. '
                         "(--no-warnings is now the default, and doesn't need to be specified.)"),
                'envvar': 'IDF_CMAKE_WARN_UNINITIALIZED',
                'is_flag': True,
                'default': False,
            },
            {
                'names': ['-v', '--verbose'],
                'help': 'Verbose build output.',
                'is_flag': True,
                'is_eager': True,
                'default': False,
                'callback': verbose_callback,
            },
            {
                'names': ['--preview'],
                'help': 'Enable IDF features that are still in preview.',
                'is_flag': True,
                'default': False,
            },
            {
                'names': ['--ccache/--no-ccache'],
                'help': 'Use ccache in build. Disabled by default.',
                'is_flag': True,
                'envvar': 'IDF_CCACHE_ENABLE',
                'default': False,
            },
            {
                'names': ['-G', '--generator'],
                'help': 'CMake generator.',
                'type': click.Choice(GENERATORS.keys()),
            },
            {
                'names': ['--dry-run'],
                'help': "Only process arguments, but don't execute actions.",
                'is_flag': True,
                'hidden': True,
                'default': False,
            },
            {
                'names': ['--no-hints'],
                'help': 'Disable hints on how to resolve errors and logging.',
                'is_flag': True,
                'default': False
            }
        ],
        'global_action_callbacks': [validate_root_options],
    }

    build_actions = {
        'actions': {
            'all': {
                'aliases': ['build'],
                'callback': build_target,
                'short_help': 'Build the project.',
                'help': (
                    'Build the project. This can involve multiple steps:\n\n'
                    '1. Create the build directory if needed. '
                    "The sub-directory 'build' is used to hold build output, "
                    'although this can be changed with the -B option.\n\n'
                    '2. Run CMake as necessary to configure the project '
                    'and generate build files for the main build tool.\n\n'
                    '3. Run the main build tool (Ninja or GNU Make). '
                    'By default, the build tool is automatically detected '
                    'but it can be explicitly set by passing the -G option to idf.py.\n\n'),
                'options': global_options,
                'order_dependencies': [
                    'reconfigure',
                    'menuconfig',
                    'clean',
                    'fullclean',
                ],
            },
            'menuconfig': {
                'callback': menuconfig,
                'help': 'Run "menuconfig" project configuration tool.',
                'options': global_options + [
                    {
                        'names': ['--style', '--color-scheme', 'style'],
                        'help': (
                            'Menuconfig style.\n'
                            'The built-in styles include:\n\n'
                            '- default - a yellowish theme,\n\n'
                            '- monochrome -  a black and white theme, or\n\n'
                            '- aquatic - a blue theme.\n\n'
                            'It is possible to customize these themes further'
                            ' as it is described in the Color schemes section of the kconfiglib documentation.\n'
                            'The default value is \"aquatic\".'),
                        'envvar': 'MENUCONFIG_STYLE',
                        'default': 'aquatic',
                    }
                ],
            },
            'confserver': {
                'callback': build_target,
                'help': 'Run JSON configuration server.',
                'options': global_options,
            },
            'size': {
                'callback': size_target,
                'help': 'Print basic size information about the app.',
                'options': global_options,
            },
            'size-components': {
                'callback': size_target,
                'help': 'Print per-component size information.',
                'options': global_options,
            },
            'size-files': {
                'callback': size_target,
                'help': 'Print per-source-file size information.',
                'options': global_options,
            },
            'bootloader': {
                'callback': build_target,
                'help': 'Build only bootloader.',
                'options': global_options,
            },
            'app': {
                'callback': build_target,
                'help': 'Build only the app.',
                'order_dependencies': ['clean', 'fullclean', 'reconfigure'],
                'options': global_options,
            },
            'efuse-common-table': {
                'callback': build_target,
                'help': 'Generate C-source for IDF\'s eFuse fields.',
                'order_dependencies': ['reconfigure'],
                'options': global_options,
            },
            'efuse-custom-table': {
                'callback': build_target,
                'help': 'Generate C-source for user\'s eFuse fields.',
                'order_dependencies': ['reconfigure'],
                'options': global_options,
            },
            'show-efuse-table': {
                'callback': build_target,
                'help': 'Print eFuse table.',
                'order_dependencies': ['reconfigure'],
                'options': global_options,
            },
            'partition-table': {
                'callback': build_target,
                'help': 'Build only partition table.',
                'order_dependencies': ['reconfigure'],
                'options': global_options,
            },
            'build-system-targets': {
                'callback': list_build_system_targets,
                'help': 'Print list of build system targets.',
            },
            'fallback': {
                'callback': fallback_target,
                'help': 'Handle for targets not known for idf.py.',
                'hidden': True,
            },
            'docs': {
                'callback': show_docs,
                'help': 'Open web browser with documentation for ESP-IDF',
                'options': [
                    {
                        'names': ['--no-browser', '-nb'],
                        'is_flag': True,
                        'help': 'Don\'t open browser.'
                    },
                    {
                        'names': ['--language', '-l'],
                        'default': get_default_language(),
                        'type': click.Choice(['en', 'zh_CN', 'cn']),
                        'help': 'Documentation language. Your system language by default (en or cn)'
                    },
                    {
                        'names': ['--starting-page', '-sp'],
                        'help': 'Documentation page (get-started, api-reference etc).'
                    },
                    {
                        'names': ['--version', '-v'],
                        'help': 'Version of ESP-IDF.'
                    },
                    {
                        'names': ['--target', '-t'],
                        'type': TargetChoice(SUPPORTED_TARGETS + PREVIEW_TARGETS + ['']),
                        'help': 'Chip target.'
                    }
                ]
            },
            'save-defconfig': {
                'callback': build_target,
                'help': 'Generate a sdkconfig.defaults with options different from the default ones',
                'options': global_options
            }
        }
    }

    clean_actions = {
        'actions': {
            'reconfigure': {
                'callback': reconfigure,
                'short_help': 'Re-run CMake.',
                'help': (
                    "Re-run CMake even if it doesn't seem to need re-running. "
                    "This isn't necessary during normal usage, "
                    'but can be useful after adding/removing files from the source tree, '
                    'or when modifying CMake cache variables. '
                    "For example, \"idf.py -DNAME='VALUE' reconfigure\" "
                    'can be used to set variable "NAME" in CMake cache to value "VALUE".'),
                'options': global_options,
                'order_dependencies': ['menuconfig', 'fullclean'],
            },
            'set-target': {
                'callback': set_target,
                'short_help': 'Set the chip target to build.',
                'help': (
                    'Set the chip target to build. This will remove the '
                    'existing sdkconfig file and corresponding CMakeCache and '
                    'create new ones according to the new target.\nFor example, '
                    "\"idf.py set-target esp32\" will select esp32 as the new chip "
                    'target.'),
                'arguments': [
                    {
                        'names': ['idf-target'],
                        'nargs': 1,
                        'type': TargetChoice(SUPPORTED_TARGETS + PREVIEW_TARGETS),
                    },
                ],
                'dependencies': ['fullclean'],
            },
            'clean': {
                'callback': clean,
                'short_help': 'Delete build output files from the build directory.',
                'help': (
                    'Delete build output files from the build directory, '
                    "forcing a 'full rebuild' the next time "
                    "the project is built. Cleaning doesn't delete "
                    'CMake configuration output and some other files'),
                'order_dependencies': ['fullclean'],
            },
            'fullclean': {
                'callback': fullclean,
                'short_help': 'Delete the entire build directory contents.',
                'help': (
                    'Delete the entire build directory contents. '
                    'This includes all CMake configuration output.'
                    'The next time the project is built, '
                    'CMake will configure it from scratch. '
                    'Note that this option recursively deletes all files '
                    'in the build directory, so use with care.'
                    'Project configuration is not deleted.')
            },
            'python-clean': {
                'callback': python_clean,
                'short_help': 'Delete generated Python byte code from the IDF directory',
                'help': (
                    'Delete generated Python byte code from the IDF directory '
                    'which may cause issues when switching between IDF and Python versions. '
                    'It is advised to run this target after switching versions.')
            },
        }
    }

    return merge_action_lists(root_options, build_actions, clean_actions)
Exemple #3
0
def action_extensions(base_actions, project_path):
    def build_target(target_name, ctx, args):
        """
        Execute the target build system to build target 'target_name'

        Calls ensure_build_directory() which will run cmake to generate a build
        directory (with the specified generator) as needed.
        """
        ensure_build_directory(args, ctx.info_name)
        run_target(target_name, args)

    def menuconfig(target_name, ctx, args, style):
        """
        Menuconfig target is build_target extended with the style argument for setting the value for the environment
        variable.
        """
        if sys.version_info[0] < 3:
            # The subprocess lib cannot accept environment variables as "unicode".
            # This encoding step is required only in Python 2.
            style = style.encode(sys.getfilesystemencoding() or 'utf-8')
        os.environ['MENUCONFIG_STYLE'] = style
        build_target(target_name, ctx, args)

    def fallback_target(target_name, ctx, args):
        """
        Execute targets that are not explicitly known to idf.py
        """
        ensure_build_directory(args, ctx.info_name)

        try:
            subprocess.check_output(GENERATORS[args.generator]["dry_run"] +
                                    [target_name],
                                    cwd=args.build_dir)

        except Exception:
            raise FatalError(
                'command "%s" is not known to idf.py and is not a %s target' %
                (target_name, args.generator))

        run_target(target_name, args)

    def verbose_callback(ctx, param, value):
        if not value or ctx.resilient_parsing:
            return

        for line in ctx.command.verbose_output:
            print(line)

        return value

    def clean(action, ctx, args):
        if not os.path.isdir(args.build_dir):
            print("Build directory '%s' not found. Nothing to clean." %
                  args.build_dir)
            return
        build_target("clean", ctx, args)

    def _delete_windows_symlinks(directory):
        """
        It deletes symlinks recursively on Windows. It is useful for Python 2 which doesn't detect symlinks on Windows.
        """
        deleted_paths = []
        if os.name == "nt":
            import ctypes

            for root, dirnames, _filenames in os.walk(directory):
                for d in dirnames:
                    full_path = os.path.join(root, d)
                    try:
                        full_path = full_path.decode("utf-8")
                    except Exception:
                        pass
                    if ctypes.windll.kernel32.GetFileAttributesW(
                            full_path) & 0x0400:
                        os.rmdir(full_path)
                        deleted_paths.append(full_path)
        return deleted_paths

    def fullclean(action, ctx, args):
        build_dir = args.build_dir
        if not os.path.isdir(build_dir):
            print("Build directory '%s' not found. Nothing to clean." %
                  build_dir)
            return
        if len(os.listdir(build_dir)) == 0:
            print("Build directory '%s' is empty. Nothing to clean." %
                  build_dir)
            return

        if not os.path.exists(os.path.join(build_dir, "CMakeCache.txt")):
            raise FatalError(
                "Directory '%s' doesn't seem to be a CMake build directory. Refusing to automatically "
                "delete files in this directory. Delete the directory manually to 'clean' it."
                % build_dir)
        red_flags = ["CMakeLists.txt", ".git", ".svn"]
        for red in red_flags:
            red = os.path.join(build_dir, red)
            if os.path.exists(red):
                raise FatalError(
                    "Refusing to automatically delete files in directory containing '%s'. Delete files manually if you're sure."
                    % red)
        # OK, delete everything in the build directory...
        # Note: Python 2.7 doesn't detect symlinks on Windows (it is supported form 3.2). Tools promising to not
        # follow symlinks will actually follow them. Deleting the build directory with symlinks deletes also items
        # outside of this directory.
        deleted_symlinks = _delete_windows_symlinks(build_dir)
        if args.verbose and len(deleted_symlinks) > 1:
            print("The following symlinks were identified and removed:\n%s" %
                  "\n".join(deleted_symlinks))
        for f in os.listdir(
                build_dir
        ):  # TODO: once we are Python 3 only, this can be os.scandir()
            f = os.path.join(build_dir, f)
            if args.verbose:
                print("Removing: %s" % f)
            if os.path.isdir(f):
                shutil.rmtree(f)
            else:
                os.remove(f)

    def python_clean(action, ctx, args):
        for root, dirnames, filenames in os.walk(os.environ["IDF_PATH"]):
            for d in dirnames:
                if d == "__pycache__":
                    dir_to_delete = os.path.join(root, d)
                    if args.verbose:
                        print("Removing: %s" % dir_to_delete)
                    shutil.rmtree(dir_to_delete)
            for filename in fnmatch.filter(filenames, '*.py[co]'):
                file_to_delete = os.path.join(root, filename)
                if args.verbose:
                    print("Removing: %s" % file_to_delete)
                os.remove(file_to_delete)

    def set_target(action, ctx, args, idf_target):
        if (not args["preview"] and idf_target in PREVIEW_TARGETS):
            raise FatalError(
                "%s is still in preview. You have to append '--preview' option after idf.py to use any preview feature."
                % idf_target)
        args.define_cache_entry.append("IDF_TARGET=" + idf_target)
        sdkconfig_path = os.path.join(args.project_dir, 'sdkconfig')
        sdkconfig_old = sdkconfig_path + ".old"
        if os.path.exists(sdkconfig_old):
            os.remove(sdkconfig_old)
        if os.path.exists(sdkconfig_path):
            os.rename(sdkconfig_path, sdkconfig_old)
        print(
            "Set Target to: %s, new sdkconfig created. Existing sdkconfig renamed to sdkconfig.old."
            % idf_target)
        ensure_build_directory(args, ctx.info_name, True)

    def reconfigure(action, ctx, args):
        ensure_build_directory(args, ctx.info_name, True)

    def validate_root_options(ctx, args, tasks):
        args.project_dir = realpath(args.project_dir)
        if args.build_dir is not None and args.project_dir == realpath(
                args.build_dir):
            raise FatalError(
                "Setting the build directory to the project directory is not supported. Suggest dropping "
                "--build-dir option, the default is a 'build' subdirectory inside the project directory."
            )
        if args.build_dir is None:
            args.build_dir = os.path.join(args.project_dir, "build")
        args.build_dir = realpath(args.build_dir)

    def idf_version_callback(ctx, param, value):
        if not value or ctx.resilient_parsing:
            return

        version = idf_version()

        if not version:
            raise FatalError("ESP-IDF version cannot be determined")

        print("ESP-IDF %s" % version)
        sys.exit(0)

    def list_targets_callback(ctx, param, value):
        if not value or ctx.resilient_parsing:
            return

        for target in SUPPORTED_TARGETS:
            print(target)

        if "preview" in ctx.params:
            for target in PREVIEW_TARGETS:
                print(target)

        sys.exit(0)

    root_options = {
        "global_options": [
            {
                "names": ["--version"],
                "help": "Show IDF version and exit.",
                "is_flag": True,
                "expose_value": False,
                "callback": idf_version_callback
            },
            {
                "names": ["--list-targets"],
                "help": "Print list of supported targets and exit.",
                "is_flag": True,
                "expose_value": False,
                "callback": list_targets_callback
            },
            {
                "names": ["-C", "--project-dir"],
                "help": "Project directory.",
                "type": click.Path(),
                "default": os.getcwd(),
            },
            {
                "names": ["-B", "--build-dir"],
                "help": "Build directory.",
                "type": click.Path(),
                "default": None,
            },
            {
                "names": ["-n", "--no-warnings"],
                "help": "Disable Cmake warnings.",
                "is_flag": True,
                "default": False,
            },
            {
                "names": ["-v", "--verbose"],
                "help": "Verbose build output.",
                "is_flag": True,
                "is_eager": True,
                "default": False,
                "callback": verbose_callback
            },
            {
                "names": ["--preview"],
                "help": "Enable IDF features that are still in preview.",
                "is_flag": True,
                "default": False,
            },
            {
                "names": ["--ccache/--no-ccache"],
                "help":
                ("Use ccache in build. Disabled by default, unless "
                 "IDF_CCACHE_ENABLE environment variable is set to a non-zero value."
                 ),
                "is_flag":
                True,
                "default":
                os.getenv("IDF_CCACHE_ENABLE") not in [None, "", "0"],
            },
            {
                "names": ["-G", "--generator"],
                "help": "CMake generator.",
                "type": click.Choice(GENERATORS.keys()),
            },
            {
                "names": ["--dry-run"],
                "help": "Only process arguments, but don't execute actions.",
                "is_flag": True,
                "hidden": True,
                "default": False
            },
        ],
        "global_action_callbacks": [validate_root_options],
    }

    build_actions = {
        "actions": {
            "all": {
                "aliases": ["build"],
                "callback":
                build_target,
                "short_help":
                "Build the project.",
                "help":
                ("Build the project. This can involve multiple steps:\n\n"
                 "1. Create the build directory if needed. "
                 "The sub-directory 'build' is used to hold build output, "
                 "although this can be changed with the -B option.\n\n"
                 "2. Run CMake as necessary to configure the project "
                 "and generate build files for the main build tool.\n\n"
                 "3. Run the main build tool (Ninja or GNU Make). "
                 "By default, the build tool is automatically detected "
                 "but it can be explicitly set by passing the -G option to idf.py.\n\n"
                 ),
                "options":
                global_options,
                "order_dependencies": [
                    "reconfigure",
                    "menuconfig",
                    "clean",
                    "fullclean",
                ],
            },
            "menuconfig": {
                "callback":
                menuconfig,
                "help":
                'Run "menuconfig" project configuration tool.',
                "options":
                global_options + [{
                    "names": ["--style", "--color-scheme", "style"],
                    "help":
                    ("Menuconfig style.\n"
                     "Is it possible to customize the menuconfig style by either setting the MENUCONFIG_STYLE "
                     "environment variable or through this option. The built-in styles include:\n\n"
                     "- default - a yellowish theme,\n\n"
                     "- monochrome -  a black and white theme, or\n"
                     "- aquatic - a blue theme.\n\n"
                     "The default value is \"aquatic\". It is possible to customize these themes further "
                     "as it is described in the Color schemes section of the kconfiglib documentation."
                     ),
                    "default":
                    os.environ.get('MENUCONFIG_STYLE', 'aquatic'),
                }],
            },
            "confserver": {
                "callback": build_target,
                "help": "Run JSON configuration server.",
                "options": global_options,
            },
            "size": {
                "callback": build_target,
                "help": "Print basic size information about the app.",
                "options": global_options,
                "dependencies": ["app"],
            },
            "size-components": {
                "callback": build_target,
                "help": "Print per-component size information.",
                "options": global_options,
                "dependencies": ["app"],
            },
            "size-files": {
                "callback": build_target,
                "help": "Print per-source-file size information.",
                "options": global_options,
                "dependencies": ["app"],
            },
            "bootloader": {
                "callback": build_target,
                "help": "Build only bootloader.",
                "options": global_options,
            },
            "app": {
                "callback": build_target,
                "help": "Build only the app.",
                "order_dependencies": ["clean", "fullclean", "reconfigure"],
                "options": global_options,
            },
            "efuse_common_table": {
                "callback": build_target,
                "help": "Generate C-source for IDF's eFuse fields.",
                "order_dependencies": ["reconfigure"],
                "options": global_options,
            },
            "efuse_custom_table": {
                "callback": build_target,
                "help": "Generate C-source for user's eFuse fields.",
                "order_dependencies": ["reconfigure"],
                "options": global_options,
            },
            "show_efuse_table": {
                "callback": build_target,
                "help": "Print eFuse table.",
                "order_dependencies": ["reconfigure"],
                "options": global_options,
            },
            "partition_table": {
                "callback": build_target,
                "help": "Build only partition table.",
                "order_dependencies": ["reconfigure"],
                "options": global_options,
            },
            "erase_otadata": {
                "callback": build_target,
                "help": "Erase otadata partition.",
                "options": global_options,
            },
            "read_otadata": {
                "callback": build_target,
                "help": "Read otadata partition.",
                "options": global_options,
            },
            "fallback": {
                "callback": fallback_target,
                "help": "Handle for targets not known for idf.py.",
                "hidden": True
            }
        }
    }

    clean_actions = {
        "actions": {
            "reconfigure": {
                "callback":
                reconfigure,
                "short_help":
                "Re-run CMake.",
                "help":
                ("Re-run CMake even if it doesn't seem to need re-running. "
                 "This isn't necessary during normal usage, "
                 "but can be useful after adding/removing files from the source tree, "
                 "or when modifying CMake cache variables. "
                 "For example, \"idf.py -DNAME='VALUE' reconfigure\" "
                 'can be used to set variable "NAME" in CMake cache to value "VALUE".'
                 ),
                "options":
                global_options,
                "order_dependencies": ["menuconfig", "fullclean"],
            },
            "set-target": {
                "callback":
                set_target,
                "short_help":
                "Set the chip target to build.",
                "help":
                ("Set the chip target to build. This will remove the "
                 "existing sdkconfig file and corresponding CMakeCache and "
                 "create new ones according to the new target.\nFor example, "
                 "\"idf.py set-target esp32\" will select esp32 as the new chip "
                 "target."),
                "arguments": [
                    {
                        "names": ["idf-target"],
                        "nargs": 1,
                        "type":
                        TargetChoice(SUPPORTED_TARGETS + PREVIEW_TARGETS),
                    },
                ],
                "dependencies": ["fullclean"],
            },
            "clean": {
                "callback":
                clean,
                "short_help":
                "Delete build output files from the build directory.",
                "help": ("Delete build output files from the build directory, "
                         "forcing a 'full rebuild' the next time "
                         "the project is built. Cleaning doesn't delete "
                         "CMake configuration output and some other files"),
                "order_dependencies": ["fullclean"],
            },
            "fullclean": {
                "callback":
                fullclean,
                "short_help":
                "Delete the entire build directory contents.",
                "help": ("Delete the entire build directory contents. "
                         "This includes all CMake configuration output."
                         "The next time the project is built, "
                         "CMake will configure it from scratch. "
                         "Note that this option recursively deletes all files "
                         "in the build directory, so use with care."
                         "Project configuration is not deleted.")
            },
            "python-clean": {
                "callback":
                python_clean,
                "short_help":
                "Delete generated Python byte code from the IDF directory",
                "help":
                ("Delete generated Python byte code from the IDF directory "
                 "which may cause issues when switching between IDF and Python versions. "
                 "It is advised to run this target after switching versions.")
            },
        }
    }

    return merge_action_lists(root_options, build_actions, clean_actions)
Exemple #4
0
def init_cli(verbose_output=None):
    # Click is imported here to run it after check_environment()
    import click

    class DeprecationMessage(object):
        """Construct deprecation notice for help messages"""
        def __init__(self, deprecated=False):
            self.deprecated = deprecated
            self.since = None
            self.removed = None
            self.custom_message = ""

            if isinstance(deprecated, dict):
                self.custom_message = deprecated.get("message", "")
                self.since = deprecated.get("since", None)
                self.removed = deprecated.get("removed", None)
            elif isinstance(deprecated, str):
                self.custom_message = deprecated

        def full_message(self, type="Option"):
            return "%s is deprecated %sand will be removed in%s.%s" % (
                type, "since %s " % self.since if self.since else "",
                " %s" % self.removed if self.removed else " future versions",
                " %s" % self.custom_message if self.custom_message else "")

        def help(self, text, type="Option", separator=" "):
            text = text or ""
            return self.full_message(
                type) + separator + text if self.deprecated else text

        def short_help(self, text):
            text = text or ""
            return ("Deprecated! " + text) if self.deprecated else text

    def print_deprecation_warning(ctx):
        """Prints deprectation warnings for arguments in given context"""
        for option in ctx.command.params:
            default = () if option.multiple else option.default
            if isinstance(option, Option) and option.deprecated and ctx.params[
                    option.name] != default:
                print("Warning: %s" %
                      DeprecationMessage(option.deprecated).full_message(
                          'Option "%s"' % option.name))

    class Task(object):
        def __init__(self, callback, name, aliases, dependencies,
                     order_dependencies, action_args):
            self.callback = callback
            self.name = name
            self.dependencies = dependencies
            self.order_dependencies = order_dependencies
            self.action_args = action_args
            self.aliases = aliases

        def run(self, context, global_args, action_args=None):
            if action_args is None:
                action_args = self.action_args

            self.callback(self.name, context, global_args, **action_args)

    class Action(click.Command):
        def __init__(self,
                     name=None,
                     aliases=None,
                     deprecated=False,
                     dependencies=None,
                     order_dependencies=None,
                     **kwargs):
            super(Action, self).__init__(name, **kwargs)

            self.name = self.name or self.callback.__name__
            self.deprecated = deprecated

            if aliases is None:
                aliases = []
            self.aliases = aliases

            self.help = self.help or self.callback.__doc__
            if self.help is None:
                self.help = ""

            if dependencies is None:
                dependencies = []

            if order_dependencies is None:
                order_dependencies = []

            # Show first line of help if short help is missing
            self.short_help = self.short_help or self.help.split("\n")[0]

            if deprecated:
                deprecation = DeprecationMessage(deprecated)
                self.short_help = deprecation.short_help(self.short_help)
                self.help = deprecation.help(self.help,
                                             type="Command",
                                             separator="\n")

            # Add aliases to help string
            if aliases:
                aliases_help = "Aliases: %s." % ", ".join(aliases)

                self.help = "\n".join([self.help, aliases_help])
                self.short_help = " ".join([aliases_help, self.short_help])

            if self.callback is not None:
                callback = self.callback

                def wrapped_callback(**action_args):
                    return Task(
                        callback=callback,
                        name=self.name,
                        dependencies=dependencies,
                        order_dependencies=order_dependencies,
                        action_args=action_args,
                        aliases=self.aliases,
                    )

                self.callback = wrapped_callback

        def invoke(self, ctx):
            if self.deprecated:
                print("Warning: %s" % DeprecationMessage(
                    self.deprecated).full_message('Command "%s"' % self.name))
                self.deprecated = False  # disable Click's built-in deprecation handling

            # Print warnings for options
            print_deprecation_warning(ctx)
            return super(Action, self).invoke(ctx)

    class Argument(click.Argument):
        """
        Positional argument

        names - alias of 'param_decls'
        """
        def __init__(self, **kwargs):
            names = kwargs.pop("names")
            super(Argument, self).__init__(names, **kwargs)

    class Scope(object):
        """
            Scope for sub-command option.
            possible values:
            - default - only available on defined level (global/action)
            - global - When defined for action, also available as global
            - shared - Opposite to 'global': when defined in global scope, also available for all actions
        """

        SCOPES = ("default", "global", "shared")

        def __init__(self, scope=None):
            if scope is None:
                self._scope = "default"
            elif isinstance(scope, str) and scope in self.SCOPES:
                self._scope = scope
            elif isinstance(scope, Scope):
                self._scope = str(scope)
            else:
                raise FatalError("Unknown scope for option: %s" % scope)

        @property
        def is_global(self):
            return self._scope == "global"

        @property
        def is_shared(self):
            return self._scope == "shared"

        def __str__(self):
            return self._scope

    class Option(click.Option):
        """Option that knows whether it should be global"""
        def __init__(self,
                     scope=None,
                     deprecated=False,
                     hidden=False,
                     **kwargs):
            """
            Keyword arguments additional to Click's Option class:

            names - alias of 'param_decls'
            deprecated - marks option as deprecated. May be boolean, string (with custom deprecation message)
            or dict with optional keys:
                since: version of deprecation
                removed: version when option will be removed
                custom_message:  Additional text to deprecation warning
            """

            kwargs["param_decls"] = kwargs.pop("names")
            super(Option, self).__init__(**kwargs)

            self.deprecated = deprecated
            self.scope = Scope(scope)
            self.hidden = hidden

            if deprecated:
                deprecation = DeprecationMessage(deprecated)
                self.help = deprecation.help(self.help)

            if self.scope.is_global:
                self.help += " This option can be used at most once either globally, or for one subcommand."

        def get_help_record(self, ctx):
            # Backport "hidden" parameter to click 5.0
            if self.hidden:
                return

            return super(Option, self).get_help_record(ctx)

    class CLI(click.MultiCommand):
        """Action list contains all actions with options available for CLI"""
        def __init__(self, all_actions=None, verbose_output=None, help=None):
            super(CLI, self).__init__(
                chain=True,
                invoke_without_command=True,
                result_callback=self.execute_tasks,
                context_settings={"max_content_width": 140},
                help=help,
            )
            self._actions = {}
            self.global_action_callbacks = []
            self.commands_with_aliases = {}

            if verbose_output is None:
                verbose_output = []

            self.verbose_output = verbose_output

            if all_actions is None:
                all_actions = {}

            shared_options = []

            # Global options
            for option_args in all_actions.get("global_options", []):
                option = Option(**option_args)
                self.params.append(option)

                if option.scope.is_shared:
                    shared_options.append(option)

            # Global options validators
            self.global_action_callbacks = all_actions.get(
                "global_action_callbacks", [])

            # Actions
            for name, action in all_actions.get("actions", {}).items():
                arguments = action.pop("arguments", [])
                options = action.pop("options", [])

                if arguments is None:
                    arguments = []

                if options is None:
                    options = []

                self._actions[name] = Action(name=name, **action)
                for alias in [name] + action.get("aliases", []):
                    self.commands_with_aliases[alias] = name

                for argument_args in arguments:
                    self._actions[name].params.append(
                        Argument(**argument_args))

                # Add all shared options
                for option in shared_options:
                    self._actions[name].params.append(option)

                for option_args in options:
                    option = Option(**option_args)

                    if option.scope.is_shared:
                        raise FatalError(
                            '"%s" is defined for action "%s". '
                            ' "shared" options can be declared only on global level'
                            % (option.name, name))

                    # Promote options to global if see for the first time
                    if option.scope.is_global and option.name not in [
                            o.name for o in self.params
                    ]:
                        self.params.append(option)

                    self._actions[name].params.append(option)

        def list_commands(self, ctx):
            return sorted(self._actions)

        def get_command(self, ctx, name):
            return self._actions.get(self.commands_with_aliases.get(name))

        def _print_closing_message(self, args, actions):
            # print a closing message of some kind
            #
            if "flash" in str(actions):
                print("Done")
                return

            if not os.path.exists(
                    os.path.join(args.build_dir, "flasher_args.json")):
                print("Done")
                return

            # Otherwise, if we built any binaries print a message about
            # how to flash them
            def print_flashing_message(title, key):
                print("\n%s build complete. To flash, run this command:" %
                      title)

                with open(os.path.join(args.build_dir,
                                       "flasher_args.json")) as f:
                    flasher_args = json.load(f)

                def flasher_path(f):
                    return _safe_relpath(os.path.join(args.build_dir, f))

                if key != "project":  # flashing a single item
                    cmd = ""
                    if (
                            key == "bootloader"
                    ):  # bootloader needs --flash-mode, etc to be passed in
                        cmd = " ".join(flasher_args["write_flash_args"]) + " "

                    cmd += flasher_args[key]["offset"] + " "
                    cmd += flasher_path(flasher_args[key]["file"])
                else:  # flashing the whole project
                    cmd = " ".join(flasher_args["write_flash_args"]) + " "
                    flash_items = sorted(
                        ((o, f)
                         for (o, f) in flasher_args["flash_files"].items()
                         if len(o) > 0),
                        key=lambda x: int(x[0], 0),
                    )
                    for o, f in flash_items:
                        cmd += o + " " + flasher_path(f) + " "

                print("%s -p %s -b %s --after %s write_flash %s" % (
                    _safe_relpath("%s/components/esptool_py/esptool/esptool.py"
                                  % os.environ["IDF_PATH"]),
                    args.port or "(PORT)",
                    args.baud,
                    flasher_args["extra_esptool_args"]["after"],
                    cmd.strip(),
                ))
                print("or run 'idf.py -p %s %s'" % (
                    args.port or "(PORT)",
                    key + "-flash" if key != "project" else "flash",
                ))

            if "all" in actions or "build" in actions:
                print_flashing_message("Project", "project")
            else:
                if "app" in actions:
                    print_flashing_message("App", "app")
                if "partition_table" in actions:
                    print_flashing_message("Partition Table",
                                           "partition_table")
                if "bootloader" in actions:
                    print_flashing_message("Bootloader", "bootloader")

        def execute_tasks(self, tasks, **kwargs):
            ctx = click.get_current_context()
            global_args = PropertyDict(kwargs)

            # Show warning if some tasks are present several times in the list
            dupplicated_tasks = sorted([
                item for item, count in Counter(
                    task.name for task in tasks).items() if count > 1
            ])
            if dupplicated_tasks:
                dupes = ", ".join('"%s"' % t for t in dupplicated_tasks)
                print(
                    "WARNING: Command%s found in the list of commands more than once. "
                    % ("s %s are" % dupes
                       if len(dupplicated_tasks) > 1 else " %s is" % dupes) +
                    "Only first occurence will be executed.")

            # Set propagated global options.
            # These options may be set on one subcommand, but available in the list of global arguments
            for task in tasks:
                for key in list(task.action_args):
                    option = next(
                        (o for o in ctx.command.params if o.name == key), None)

                    if option and (option.scope.is_global
                                   or option.scope.is_shared):
                        local_value = task.action_args.pop(key)
                        global_value = global_args[key]
                        default = () if option.multiple else option.default

                        if global_value != default and local_value != default and global_value != local_value:
                            raise FatalError(
                                'Option "%s" provided for "%s" is already defined to a different value. '
                                "This option can appear at most once in the command line."
                                % (key, task.name))
                        if local_value != default:
                            global_args[key] = local_value

            # Show warnings about global arguments
            print_deprecation_warning(ctx)

            # Make sure that define_cache_entry is mutable list and can be modified in callbacks
            global_args.define_cache_entry = list(
                global_args.define_cache_entry)

            # Execute all global action callback - first from idf.py itself, then from extensions
            for action_callback in ctx.command.global_action_callbacks:
                action_callback(ctx, global_args, tasks)

            # Always show help when command is not provided
            if not tasks:
                print(ctx.get_help())
                ctx.exit()

            # Build full list of tasks to and deal with dependencies and order dependencies
            tasks_to_run = OrderedDict()
            while tasks:
                task = tasks[0]
                tasks_dict = dict([(t.name, t) for t in tasks])

                dependecies_processed = True

                # If task have some dependecies they have to be executed before the task.
                for dep in task.dependencies:
                    if dep not in tasks_to_run.keys():
                        # If dependent task is in the list of unprocessed tasks move to the front of the list
                        if dep in tasks_dict.keys():
                            dep_task = tasks.pop(tasks.index(tasks_dict[dep]))
                        # Otherwise invoke it with default set of options
                        # and put to the front of the list of unprocessed tasks
                        else:
                            print(
                                'Adding "%s"\'s dependency "%s" to list of commands with default set of options.'
                                % (task.name, dep))
                            dep_task = ctx.invoke(
                                ctx.command.get_command(ctx, dep))

                            # Remove options with global scope from invoke tasks because they are alread in global_args
                            for key in list(dep_task.action_args):
                                option = next((o for o in ctx.command.params
                                               if o.name == key), None)
                                if option and (option.scope.is_global
                                               or option.scope.is_shared):
                                    dep_task.action_args.pop(key)

                        tasks.insert(0, dep_task)
                        dependecies_processed = False

                # Order only dependencies are moved to the front of the queue if they present in command list
                for dep in task.order_dependencies:
                    if dep in tasks_dict.keys(
                    ) and dep not in tasks_to_run.keys():
                        tasks.insert(0,
                                     tasks.pop(tasks.index(tasks_dict[dep])))
                        dependecies_processed = False

                if dependecies_processed:
                    # Remove task from list of unprocessed tasks
                    tasks.pop(0)

                    # And add to the queue
                    if task.name not in tasks_to_run.keys():
                        tasks_to_run.update([(task.name, task)])

            # Run all tasks in the queue
            # when global_args.dry_run is true idf.py works in idle mode and skips actual task execution
            if not global_args.dry_run:
                for task in tasks_to_run.values():
                    name_with_aliases = task.name
                    if task.aliases:
                        name_with_aliases += " (aliases: %s)" % ", ".join(
                            task.aliases)

                    print("Executing action: %s" % name_with_aliases)
                    task.run(ctx, global_args, task.action_args)

                self._print_closing_message(global_args, tasks_to_run.keys())

            return tasks_to_run

    # That's a tiny parser that parse project-dir even before constructing
    # fully featured click parser to be sure that extensions are loaded from the right place
    @click.command(
        add_help_option=False,
        context_settings={
            "allow_extra_args": True,
            "ignore_unknown_options": True
        },
    )
    @click.option("-C", "--project-dir", default=os.getcwd())
    def parse_project_dir(project_dir):
        return realpath(project_dir)

    project_dir = parse_project_dir(standalone_mode=False)

    all_actions = {}
    # Load extensions from components dir
    idf_py_extensions_path = os.path.join(os.environ["IDF_PATH"], "tools",
                                          "idf_py_actions")
    extra_paths = os.environ.get("IDF_EXTRA_ACTIONS_PATH", "").split(';')
    extension_dirs = [idf_py_extensions_path] + extra_paths
    extensions = {}

    for directory in extension_dirs:
        if directory and not os.path.exists(directory):
            print(
                'WARNING: Directroy with idf.py extensions doesn\'t exist:\n    %s'
                % directory)
            continue

        sys.path.append(directory)
        for _finder, name, _ispkg in sorted(iter_modules([directory])):
            if name.endswith('_ext'):
                extensions[name] = import_module(name)

    for name, extension in extensions.items():
        try:
            all_actions = merge_action_lists(
                all_actions,
                extension.action_extensions(all_actions, project_dir))
        except AttributeError:
            print('WARNING: Cannot load idf.py extension "%s"' % name)

    # Load extensions from project dir
    if os.path.exists(os.path.join(project_dir, "idf_ext.py")):
        sys.path.append(project_dir)
        try:
            from idf_ext import action_extensions
        except ImportError:
            print("Error importing extension file idf_ext.py. Skipping.")
            print(
                "Please make sure that it contains implementation (even if it's empty) of add_action_extensions"
            )

        try:
            all_actions = merge_action_lists(
                all_actions, action_extensions(all_actions, project_dir))
        except NameError:
            pass

    return CLI(help="ESP-IDF build management",
               verbose_output=verbose_output,
               all_actions=all_actions)