Ejemplo n.º 1
0
def init_cli():
    # Click is imported here to run it after check_environment()
    import click

    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,
                     dependencies=None,
                     order_dependencies=None,
                     **kwargs):
            super(Action, self).__init__(name, **kwargs)

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

            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]

            # 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

    class Argument(click.Argument):
        """Positional argument"""
        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, **kwargs):
            kwargs["param_decls"] = kwargs.pop("names")
            super(Option, self).__init__(**kwargs)

            self.scope = Scope(scope)

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

    class CLI(click.MultiCommand):
        """Action list contains all actions with options available for CLI"""
        def __init__(self, action_lists=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 action_lists is None:
                action_lists = []

            shared_options = []

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

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

            for action_list in action_lists:
                # Global options validators
                self.global_action_callbacks.extend(
                    action_list.get("global_action_callbacks", []))

            for action_list in action_lists:
                # Actions
                for name, action in action_list.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

            # 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(ctx.params)

            # Set propagated global options
            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

            # Validate global arguments
            for action_callback in ctx.command.global_action_callbacks:
                action_callback(ctx, global_args, tasks)

            # very simple dependency management
            completed_tasks = set()

            if not tasks:
                print(ctx.get_help())
                ctx.exit()

            while tasks:
                task = tasks[0]
                tasks_dict = dict([(t.name, t) for t in tasks])

                name_with_aliases = task.name
                if task.aliases:
                    name_with_aliases += " (aliases: %s)" % ", ".join(
                        task.aliases)

                ready_to_run = True
                for dep in task.dependencies:
                    if dep not in completed_tasks:
                        print(
                            'Adding %s\'s dependency "%s" to list of actions' %
                            (task.name, dep))
                        dep_task = ctx.invoke(ctx.command.get_command(
                            ctx, dep))

                        # Remove global options from dependent tasks
                        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)
                        ready_to_run = False

                for dep in task.order_dependencies:
                    if dep in tasks_dict.keys() and dep not in completed_tasks:
                        tasks.insert(0,
                                     tasks.pop(tasks.index(tasks_dict[dep])))
                        ready_to_run = False

                if ready_to_run:
                    tasks.pop(0)

                    if task.name in completed_tasks:
                        print("Skipping action that is already done: %s" %
                              name_with_aliases)
                    else:
                        print("Executing action: %s" % name_with_aliases)
                        task.run(ctx, global_args, task.action_args)

                    completed_tasks.add(task.name)

            self._print_closing_message(global_args, completed_tasks)

        @staticmethod
        def merge_action_lists(*action_lists):
            merged_actions = {
                "global_options": [],
                "actions": {},
                "global_action_callbacks": [],
            }
            for action_list in action_lists:
                merged_actions["global_options"].extend(
                    action_list.get("global_options", []))
                merged_actions["actions"].update(action_list.get(
                    "actions", {}))
                merged_actions["global_action_callbacks"].extend(
                    action_list.get("global_action_callbacks", []))
            return merged_actions

    # 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)

    # Load base idf commands
    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)

    # Possible keys for action dict are: global_options, actions and global_action_callbacks
    global_options = [{
        "names": ["-D", "--define-cache-entry"],
        "help": "Create a cmake cache entry.",
        "scope": "global",
        "multiple": True,
    }]

    root_options = {
        "global_options": [
            {
                "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,
                "default": False,
            },
            {
                "names": ["--ccache"],
                "help": "Use ccache in build",
                "is_flag": True,
                "default": False,
            },
            {
                # This is unused/ignored argument, as ccache use was originally opt-out.
                # Use of ccache has been made opt-in using --cache arg.
                "names": ["--no-ccache"],
                "default": True,
                "is_flag": True,
                "hidden": True,
            },
            {
                "names": ["-G", "--generator"],
                "help": "CMake generator.",
                "type": click.Choice(GENERATOR_CMDS.keys()),
            },
        ],
        "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": build_target,
                "help": 'Run "menuconfig" project configuration tool.',
                "options": global_options,
            },
            "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": "Genereate C-source for IDF's eFuse fields.",
                "order_dependencies": ["reconfigure"],
                "options": global_options,
            },
            "efuse_custom_table": {
                "callback": build_target,
                "help": "Genereate 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,
            },
        }
    }

    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"],
            },
            "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.",
            },
        }
    }

    baud_rate = {
        "names": ["-b", "--baud"],
        "help": "Baud rate.",
        "scope": "global",
        "envvar": "ESPBAUD",
        "default": 460800,
    }

    port = {
        "names": ["-p", "--port"],
        "help": "Serial port.",
        "scope": "global",
        "envvar": "ESPPORT",
        "default": None,
    }

    serial_actions = {
        "actions": {
            "flash": {
                "callback": flash,
                "help": "Flash the project.",
                "options": global_options + [baud_rate, port],
                "dependencies": ["all"],
                "order_dependencies": ["erase_flash"],
            },
            "erase_flash": {
                "callback": erase_flash,
                "help": "Erase entire flash chip.",
                "options": [baud_rate, port],
            },
            "monitor": {
                "callback":
                monitor,
                "help":
                "Display serial output.",
                "options": [
                    port,
                    {
                        "names": ["--print-filter", "--print_filter"],
                        "help":
                        ("Filter monitor output.\n"
                         "Restrictions on what to print can be specified as a series of <tag>:<log_level> items "
                         "where <tag> is the tag string and <log_level> is a character from the set "
                         "{N, E, W, I, D, V, *} referring to a level. "
                         'For example, "tag1:W" matches and prints only the outputs written with '
                         'ESP_LOGW("tag1", ...) or at lower verbosity level, i.e. ESP_LOGE("tag1", ...). '
                         'Not specifying a <log_level> or using "*" defaults to Verbose level.\n'
                         'Please see the IDF Monitor section of the ESP-IDF documentation '
                         'for a more detailed description and further examples.'
                         ),
                        "default":
                        None,
                    },
                ],
                "order_dependencies": [
                    "flash",
                    "partition_table-flash",
                    "bootloader-flash",
                    "app-flash",
                ],
            },
            "partition_table-flash": {
                "callback": flash,
                "help": "Flash partition table only.",
                "options": [baud_rate, port],
                "dependencies": ["partition_table"],
                "order_dependencies": ["erase_flash"],
            },
            "bootloader-flash": {
                "callback": flash,
                "help": "Flash bootloader only.",
                "options": [baud_rate, port],
                "dependencies": ["bootloader"],
                "order_dependencies": ["erase_flash"],
            },
            "app-flash": {
                "callback": flash,
                "help": "Flash the app only.",
                "options": [baud_rate, port],
                "dependencies": ["app"],
                "order_dependencies": ["erase_flash"],
            },
            "encrypted-app-flash": {
                "callback": flash,
                "help": "Flash the encrypted app only.",
                "dependencies": ["app"],
                "order_dependencies": ["erase_flash"],
            },
            "encrypted-flash": {
                "callback": flash,
                "help": "Flash the encrypted project.",
                "dependencies": ["all"],
                "order_dependencies": ["erase_flash"],
            },
        },
    }

    base_actions = CLI.merge_action_lists(root_options, build_actions,
                                          clean_actions, serial_actions)
    all_actions = [base_actions]

    # Load extensions
    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"
            )

    # Add actions extensions
    try:
        all_actions.append(action_extensions(base_actions, project_dir))
    except NameError:
        pass

    return CLI(help="ESP-IDF build management", action_lists=all_actions)
Ejemplo n.º 2
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)
Ejemplo n.º 3
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)