mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-02 11:10:54 +03:00
Merge branch 'feat/replace_click_witch_rich_click_v6.0' into 'release/v6.0'
feat(tools): Replaced click with rich_click (v6.0) See merge request espressif/esp-idf!49021
This commit is contained in:
@@ -12,7 +12,7 @@ Usage:
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import click
|
import rich_click as click
|
||||||
from src.app import BLELogApp
|
from src.app import BLELogApp
|
||||||
from src.backend.models import format_bytes
|
from src.backend.models import format_bytes
|
||||||
from src.backend.uart_transport import validate_uart_port
|
from src.backend.uart_transport import validate_uart_port
|
||||||
|
|||||||
+133
-30
@@ -156,9 +156,48 @@ def _safe_relpath(path: str, start: str | None = None) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def init_cli(verbose_output: list | None = None) -> Any:
|
def init_cli(verbose_output: list | None = None) -> Any:
|
||||||
# Click is imported here to run it after check_environment()
|
# rich-click is imported here to run it after check_environment()
|
||||||
import click
|
import rich_click as click
|
||||||
from click.shell_completion import CompletionItem
|
from click.shell_completion import CompletionItem
|
||||||
|
from rich_click import Context
|
||||||
|
from rich_click import RichHelpConfiguration
|
||||||
|
from rich_click.rich_click import MAX_WIDTH
|
||||||
|
|
||||||
|
# ``RichHelpFormatter`` was promoted to the top-level namespace in
|
||||||
|
# rich-click 1.9; the submodule path is stable across 1.8.x/1.9.x. For
|
||||||
|
# positional/option base classes we deliberately use plain ``click.Argument``
|
||||||
|
# / ``click.Option`` rather than the rich-click 1.9 ``RichArgument`` /
|
||||||
|
# ``RichOption`` subclasses: those subclasses are empty wrappers on 1.9.x
|
||||||
|
# (the only observable difference is the ``isinstance(obj, RichArgument)``
|
||||||
|
# gate that auto-populates an Arguments panel when ``obj.help is not None``
|
||||||
|
# -- idf.py never declares ``help=`` on a positional argument), and the
|
||||||
|
# symbols don't exist on 1.8.x at all. Using the click base classes keeps
|
||||||
|
# one code path for both rich-click lines.
|
||||||
|
from rich_click.rich_help_formatter import RichHelpFormatter
|
||||||
|
|
||||||
|
# click 8.2 made ``Parameter.make_metavar(ctx)`` mandatory. rich-click
|
||||||
|
# versions before 1.8.6 still call ``param.make_metavar()`` with no ctx
|
||||||
|
# (see ``rich_click/rich_help_rendering.py``), which crashes with
|
||||||
|
# ``TypeError`` on click >= 8.2. The crash hits *every* parameter rich-click
|
||||||
|
# iterates -- including click's built-in ``--help`` option, which is a
|
||||||
|
# plain ``click.Option`` instance not under our control. So patch the
|
||||||
|
# ``Parameter.make_metavar`` method itself to accept ctx as optional,
|
||||||
|
# fishing the running context from ``click.get_current_context`` when
|
||||||
|
# rich-click forgets to pass it. The patch is a no-op on click < 8.2
|
||||||
|
# (signature already takes only ``self``) and on click >= 8.2 it simply
|
||||||
|
# bridges the old rich-click call site.
|
||||||
|
if click.Parameter.make_metavar.__code__.co_argcount >= 2:
|
||||||
|
_orig_make_metavar = click.Parameter.make_metavar
|
||||||
|
|
||||||
|
def _make_metavar_with_optional_ctx(self: 'click.Parameter', ctx: 'Context | None' = None) -> str:
|
||||||
|
if ctx is None:
|
||||||
|
try:
|
||||||
|
ctx = click.get_current_context()
|
||||||
|
except RuntimeError:
|
||||||
|
ctx = click.Context(click.Command(self.name or '_'))
|
||||||
|
return _orig_make_metavar(self, ctx) # type: ignore[no-any-return]
|
||||||
|
|
||||||
|
click.Parameter.make_metavar = _make_metavar_with_optional_ctx # type: ignore[method-assign]
|
||||||
|
|
||||||
class Deprecation:
|
class Deprecation:
|
||||||
"""Construct deprecation notice for help messages"""
|
"""Construct deprecation notice for help messages"""
|
||||||
@@ -210,7 +249,7 @@ def init_cli(verbose_output: list | None = None) -> Any:
|
|||||||
text = text or ''
|
text = text or ''
|
||||||
return ('Deprecated! ' + text) if self.deprecated else text
|
return ('Deprecated! ' + text) if self.deprecated else text
|
||||||
|
|
||||||
def check_deprecation(ctx: click.core.Context) -> None:
|
def check_deprecation(ctx: Context) -> None:
|
||||||
"""Prints deprecation warnings for arguments in given context"""
|
"""Prints deprecation warnings for arguments in given context"""
|
||||||
for option in ctx.command.params:
|
for option in ctx.command.params:
|
||||||
default = () if option.multiple else option.default
|
default = () if option.multiple else option.default
|
||||||
@@ -240,15 +279,13 @@ def init_cli(verbose_output: list | None = None) -> Any:
|
|||||||
self.action_args = action_args
|
self.action_args = action_args
|
||||||
self.aliases = aliases
|
self.aliases = aliases
|
||||||
|
|
||||||
def __call__(
|
def __call__(self, context: Context, global_args: PropertyDict, action_args: dict | None = None) -> None:
|
||||||
self, context: click.core.Context, global_args: PropertyDict, action_args: dict | None = None
|
|
||||||
) -> None:
|
|
||||||
if action_args is None:
|
if action_args is None:
|
||||||
action_args = self.action_args
|
action_args = self.action_args
|
||||||
|
|
||||||
self.callback(self.name, context, global_args, **action_args)
|
self.callback(self.name, context, global_args, **action_args)
|
||||||
|
|
||||||
class Action(click.Command):
|
class Action(click.RichCommand):
|
||||||
callback: Callable
|
callback: Callable
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -309,7 +346,7 @@ def init_cli(verbose_output: list | None = None) -> Any:
|
|||||||
|
|
||||||
self.callback: Callable = wrapped_callback
|
self.callback: Callable = wrapped_callback
|
||||||
|
|
||||||
def invoke(self, ctx: click.core.Context) -> click.core.Context:
|
def invoke(self, ctx: Context) -> Context:
|
||||||
if self.deprecated:
|
if self.deprecated:
|
||||||
deprecation = Deprecation(self.deprecated)
|
deprecation = Deprecation(self.deprecated)
|
||||||
message = deprecation.full_message(f'Command "{self.name}"')
|
message = deprecation.full_message(f'Command "{self.name}"')
|
||||||
@@ -325,6 +362,26 @@ def init_cli(verbose_output: list | None = None) -> Any:
|
|||||||
check_deprecation(ctx)
|
check_deprecation(ctx)
|
||||||
return super().invoke(ctx)
|
return super().invoke(ctx)
|
||||||
|
|
||||||
|
def format_options(self, ctx: Context, formatter: RichHelpFormatter) -> None:
|
||||||
|
"""
|
||||||
|
default_panels_first=True causes the
|
||||||
|
renderer to drop `post_default_panels` for options on non-Group
|
||||||
|
commands, which is exactly where the subcommand "Options" panel
|
||||||
|
lives -- `idf.py <subcmd> --help` would otherwise show only
|
||||||
|
Usage + description. Temporarily flip the flag to False while
|
||||||
|
rendering options.
|
||||||
|
"""
|
||||||
|
# default_panels_first=True is introduced in rich-click 1.9.6
|
||||||
|
if not hasattr(formatter.config, 'default_panels_first'):
|
||||||
|
super().format_options(ctx, formatter)
|
||||||
|
return
|
||||||
|
prev_default_first = formatter.config.default_panels_first
|
||||||
|
try:
|
||||||
|
formatter.config.default_panels_first = False
|
||||||
|
super().format_options(ctx, formatter)
|
||||||
|
finally:
|
||||||
|
formatter.config.default_panels_first = prev_default_first
|
||||||
|
|
||||||
class Argument(click.Argument):
|
class Argument(click.Argument):
|
||||||
"""
|
"""
|
||||||
Positional argument
|
Positional argument
|
||||||
@@ -406,14 +463,14 @@ def init_cli(verbose_output: list | None = None) -> Any:
|
|||||||
if self.scope.is_global:
|
if self.scope.is_global:
|
||||||
self.help += ' This option can be used at most once either globally, or for one subcommand.'
|
self.help += ' This option can be used at most once either globally, or for one subcommand.'
|
||||||
|
|
||||||
def get_help_record(self, ctx: click.core.Context) -> Any:
|
def get_help_record(self, ctx: Context) -> Any:
|
||||||
# Backport "hidden" parameter to click 5.0
|
# Backport "hidden" parameter to click 5.0
|
||||||
if self.hidden:
|
if self.hidden:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return super().get_help_record(ctx)
|
return super().get_help_record(ctx)
|
||||||
|
|
||||||
class CLI(click.Group):
|
class CLI(click.RichGroup):
|
||||||
"""Action list contains all actions with options available for CLI"""
|
"""Action list contains all actions with options available for CLI"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -421,13 +478,26 @@ def init_cli(verbose_output: list | None = None) -> Any:
|
|||||||
all_actions: dict | None = None,
|
all_actions: dict | None = None,
|
||||||
verbose_output: list | None = None,
|
verbose_output: list | None = None,
|
||||||
cli_help: str | None = None,
|
cli_help: str | None = None,
|
||||||
|
command_groups: dict[str, list[dict[str, Any]]] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
rich_help_config_kwargs: dict[str, Any] = {
|
||||||
|
'max_width': MAX_WIDTH,
|
||||||
|
'command_groups': command_groups if command_groups is not None else {},
|
||||||
|
}
|
||||||
|
# ``default_panels_first`` was added in rich-click 1.9.6; on older
|
||||||
|
# versions passing it raises TypeError.
|
||||||
|
if hasattr(RichHelpConfiguration, 'default_panels_first'):
|
||||||
|
rich_help_config_kwargs['default_panels_first'] = True
|
||||||
super().__init__(
|
super().__init__(
|
||||||
|
PROG,
|
||||||
chain=True,
|
chain=True,
|
||||||
invoke_without_command=True,
|
invoke_without_command=True,
|
||||||
result_callback=self.execute_tasks,
|
result_callback=self.execute_tasks,
|
||||||
no_args_is_help=True,
|
no_args_is_help=True,
|
||||||
context_settings={'max_content_width': 140},
|
context_settings={
|
||||||
|
'help_option_names': ['-h', '--help'],
|
||||||
|
'rich_help_config': RichHelpConfiguration(**rich_help_config_kwargs),
|
||||||
|
},
|
||||||
help=cli_help,
|
help=cli_help,
|
||||||
)
|
)
|
||||||
self._actions = {}
|
self._actions = {}
|
||||||
@@ -467,6 +537,7 @@ def init_cli(verbose_output: list | None = None) -> Any:
|
|||||||
options = []
|
options = []
|
||||||
|
|
||||||
self._actions[name] = Action(name=name, **action)
|
self._actions[name] = Action(name=name, **action)
|
||||||
|
self.commands[name] = self._actions[name]
|
||||||
for alias in [name] + action.get('aliases', []):
|
for alias in [name] + action.get('aliases', []):
|
||||||
self.commands_with_aliases[alias] = name
|
self.commands_with_aliases[alias] = name
|
||||||
|
|
||||||
@@ -492,10 +563,10 @@ def init_cli(verbose_output: list | None = None) -> Any:
|
|||||||
|
|
||||||
self._actions[name].params.append(option)
|
self._actions[name].params.append(option)
|
||||||
|
|
||||||
def list_commands(self, ctx: click.core.Context) -> list:
|
def list_commands(self, ctx: Context) -> list:
|
||||||
return sorted(filter(lambda name: not self._actions[name].hidden, self._actions))
|
return sorted(filter(lambda name: not self._actions[name].hidden, self._actions))
|
||||||
|
|
||||||
def get_command(self, ctx: click.core.Context, name: str) -> Action | None:
|
def get_command(self, ctx: Context, name: str) -> Action | None:
|
||||||
if name in self.commands_with_aliases:
|
if name in self.commands_with_aliases:
|
||||||
return self._actions.get(self.commands_with_aliases.get(name))
|
return self._actions.get(self.commands_with_aliases.get(name))
|
||||||
|
|
||||||
@@ -506,7 +577,7 @@ def init_cli(verbose_output: list | None = None) -> Any:
|
|||||||
return Action(name=name, callback=callback.unwrapped_callback)
|
return Action(name=name, callback=callback.unwrapped_callback)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def shell_complete(self, ctx: click.core.Context, incomplete: str) -> list[CompletionItem]:
|
def shell_complete(self, ctx: Context, incomplete: str) -> list[CompletionItem]:
|
||||||
# Enable @-argument completion in bash only if @ is not present in
|
# Enable @-argument completion in bash only if @ is not present in
|
||||||
# COMP_WORDBREAKS. When @ is included, the @-argument is not considered
|
# COMP_WORDBREAKS. When @ is included, the @-argument is not considered
|
||||||
# part of the completion word, causing @-argument completion to function
|
# part of the completion word, causing @-argument completion to function
|
||||||
@@ -799,15 +870,14 @@ def init_cli(verbose_output: list | None = None) -> Any:
|
|||||||
build_dir: str = args.build_dir
|
build_dir: str = args.build_dir
|
||||||
return os.path.abspath(build_dir)
|
return os.path.abspath(build_dir)
|
||||||
|
|
||||||
def _extract_relevant_path(path: str) -> str:
|
def _path_relative_to_project(path: str, project_dir: str) -> str:
|
||||||
"""
|
"""If ``path`` is under ``project_dir``, return its path relative to the project; else ``path`` unchanged."""
|
||||||
Returns part of the path starting from 'components' or 'managed_components'.
|
path_abs = os.path.abspath(os.path.normpath(path))
|
||||||
If neither is found, returns the full path.
|
project_abs = os.path.abspath(os.path.normpath(project_dir))
|
||||||
"""
|
parent_prefix = project_abs.rstrip(os.sep) + os.sep
|
||||||
for keyword in ('components', 'managed_components'):
|
if path_abs == project_abs or path_abs.startswith(parent_prefix):
|
||||||
# arg path is loaded from project_description.json, where paths are always defined with '/'
|
return _safe_relpath(path_abs, project_abs)
|
||||||
if keyword in path.split('/'):
|
|
||||||
return keyword + path.split(keyword, 1)[1]
|
|
||||||
return path
|
return path
|
||||||
|
|
||||||
# Mutable dict used as a cache keyed by lock path
|
# Mutable dict used as a cache keyed by lock path
|
||||||
@@ -879,6 +949,20 @@ def init_cli(verbose_output: list | None = None) -> Any:
|
|||||||
return lock_key in _get_trusted_names_from_lock(lock_path)
|
return lock_key in _get_trusted_names_from_lock(lock_path)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _build_rich_help_command_groups(
|
||||||
|
external_panels: list[tuple[str, list[str]]],
|
||||||
|
) -> dict[str, list[dict[str, Any]]]:
|
||||||
|
"""Build ``command_groups`` for rich-click's ``RichHelpConfiguration``.
|
||||||
|
``external_panels`` is a list of ``(title, command_names)`` from ``idf_ext.py`` extension
|
||||||
|
modules and from Python entry-point extensions. Those panels appear on
|
||||||
|
the root ``idf.py --help`` after the default Commands section.
|
||||||
|
"""
|
||||||
|
panels: list[dict[str, Any]] = []
|
||||||
|
for title, cmds in external_panels:
|
||||||
|
if cmds:
|
||||||
|
panels.append({'name': title, 'commands': cmds})
|
||||||
|
return {PROG: panels} if panels else {}
|
||||||
|
|
||||||
# That's a tiny parser that parse project-dir even before constructing
|
# 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
|
# fully featured click parser to be sure that extensions are loaded from the right place
|
||||||
@click.command(
|
@click.command(
|
||||||
@@ -978,31 +1062,44 @@ def init_cli(verbose_output: list | None = None) -> Any:
|
|||||||
else:
|
else:
|
||||||
print_warning(
|
print_warning(
|
||||||
f'WARNING: Not loading component extension from untrusted source '
|
f'WARNING: Not loading component extension from untrusted source '
|
||||||
f'"{_extract_relevant_path(comp_dir)}". '
|
f'"{_path_relative_to_project(comp_dir, project_dir)}". '
|
||||||
'Only extensions from trusted sources are loaded. Run '
|
'Only extensions from trusted sources are loaded. Run '
|
||||||
'"idf.py docs -sp api-guides/tools/idf-py.html#extending-idf-py" '
|
'"idf.py docs -sp api-guides/tools/idf-py.html#extending-idf-py" '
|
||||||
'for the list of trusted sources. Set IDF_EXTENSION_ALLOW_UNTRUSTED=1 to load all.'
|
'for the list of trusted sources. Set IDF_EXTENSION_ALLOW_UNTRUSTED=1 to load all.'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Load extensions from directories that participate in the build (components and project)
|
# Load extensions from directories that participate in the build (components and project)
|
||||||
|
external_help_panels: list[tuple[str, list[str]]] = []
|
||||||
for ext_dir in component_idf_ext_dirs + [project_dir]:
|
for ext_dir in component_idf_ext_dirs + [project_dir]:
|
||||||
extension_func = load_cli_extension_from_dir(ext_dir)
|
extension_func = load_cli_extension_from_dir(ext_dir)
|
||||||
if extension_func:
|
if extension_func:
|
||||||
try:
|
try:
|
||||||
all_actions = merge_action_lists(all_actions, custom_actions=extension_func(all_actions, project_dir))
|
custom_actions = extension_func(all_actions, project_dir)
|
||||||
|
all_actions = merge_action_lists(all_actions, custom_actions=custom_actions)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print_warning(f'WARNING: Cannot load directory extension from "{ext_dir}": {e}')
|
print_warning(f'WARNING: Cannot load directory extension from "{ext_dir}": {e}')
|
||||||
else:
|
else:
|
||||||
|
panel_cmds = sorted(n for n in custom_actions.get('actions') or {} if n != 'fallback')
|
||||||
|
if panel_cmds:
|
||||||
|
panel_title = (
|
||||||
|
'Project' if ext_dir == project_dir else _path_relative_to_project(ext_dir, project_dir)
|
||||||
|
)
|
||||||
|
external_help_panels.append((panel_title, panel_cmds))
|
||||||
if ext_dir != project_dir:
|
if ext_dir != project_dir:
|
||||||
print(f'INFO: Loaded component extension from "{_extract_relevant_path(ext_dir)}"')
|
print(f'INFO: Loaded component extension from "{_path_relative_to_project(ext_dir, project_dir)}"')
|
||||||
|
|
||||||
# Load extensions from Python entry points
|
# Load extensions from Python entry points
|
||||||
entry_point_extensions = load_cli_extensions_from_entry_points()
|
entry_point_extensions = load_cli_extensions_from_entry_points()
|
||||||
for name, extension_func in entry_point_extensions:
|
for ep_name, extension_func in entry_point_extensions:
|
||||||
try:
|
try:
|
||||||
all_actions = merge_action_lists(all_actions, custom_actions=extension_func(all_actions, project_dir))
|
custom_actions = extension_func(all_actions, project_dir)
|
||||||
|
all_actions = merge_action_lists(all_actions, custom_actions=custom_actions)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print_warning(f'WARNING: Cannot load entry point extension "{name}": {e}')
|
print_warning(f'WARNING: Cannot load entry point extension "{ep_name}": {e}')
|
||||||
|
else:
|
||||||
|
panel_cmds = sorted(n for n in (custom_actions.get('actions') or {}) if n != 'fallback')
|
||||||
|
if panel_cmds:
|
||||||
|
external_help_panels.append((ep_name, panel_cmds))
|
||||||
|
|
||||||
cli_help = (
|
cli_help = (
|
||||||
'ESP-IDF CLI build management tool. '
|
'ESP-IDF CLI build management tool. '
|
||||||
@@ -1010,7 +1107,13 @@ def init_cli(verbose_output: list | None = None) -> Any:
|
|||||||
f'Selected target: {get_target(project_dir)}'
|
f'Selected target: {get_target(project_dir)}'
|
||||||
)
|
)
|
||||||
|
|
||||||
return CLI(cli_help=cli_help, verbose_output=verbose_output, all_actions=all_actions)
|
help_command_groups = _build_rich_help_command_groups(external_help_panels)
|
||||||
|
return CLI(
|
||||||
|
cli_help=cli_help,
|
||||||
|
verbose_output=verbose_output,
|
||||||
|
all_actions=all_actions,
|
||||||
|
command_groups=help_command_groups,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main(argv: list[Any] | None = None) -> None:
|
def main(argv: list[Any] | None = None) -> None:
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ from urllib.request import Request
|
|||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
from webbrowser import open_new_tab
|
from webbrowser import open_new_tab
|
||||||
|
|
||||||
import click
|
import rich_click as click
|
||||||
from click.core import Context
|
from rich_click import Context
|
||||||
|
|
||||||
from idf_py_actions.constants import GENERATORS
|
from idf_py_actions.constants import GENERATORS
|
||||||
from idf_py_actions.constants import PREVIEW_TARGETS
|
from idf_py_actions.constants import PREVIEW_TARGETS
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -6,9 +6,8 @@ import stat
|
|||||||
import sys
|
import sys
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
from shutil import copytree
|
from shutil import copytree
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
import click
|
from rich_click import Context
|
||||||
|
|
||||||
from idf_py_actions.tools import PropertyDict
|
from idf_py_actions.tools import PropertyDict
|
||||||
|
|
||||||
@@ -100,8 +99,8 @@ def create_component(target_path: str, name: str) -> None:
|
|||||||
replace_in_file(os.path.join(target_path, 'CMakeLists.txt'), 'main', name)
|
replace_in_file(os.path.join(target_path, 'CMakeLists.txt'), 'main', name)
|
||||||
|
|
||||||
|
|
||||||
def action_extensions(base_actions: Dict, project_path: str) -> Dict:
|
def action_extensions(base_actions: dict, project_path: str) -> dict:
|
||||||
def create_new(action: str, ctx: click.core.Context, global_args: PropertyDict, **action_args: str) -> Dict:
|
def create_new(action: str, ctx: Context, global_args: PropertyDict, **action_args: str) -> dict:
|
||||||
target_path = action_args.get('path') or os.path.join(project_path, action_args['name'])
|
target_path = action_args.get('path') or os.path.join(project_path, action_args['name'])
|
||||||
|
|
||||||
is_empty_and_create(target_path, action)
|
is_empty_and_create(target_path, action)
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import time
|
|||||||
from threading import Thread
|
from threading import Thread
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from click import INT
|
|
||||||
from click.core import Context
|
|
||||||
from esp_coredump import CoreDump
|
from esp_coredump import CoreDump
|
||||||
|
from rich_click import INT
|
||||||
|
from rich_click import Context
|
||||||
|
|
||||||
from idf_py_actions.errors import FatalError
|
from idf_py_actions.errors import FatalError
|
||||||
from idf_py_actions.serial_ext import BAUD_RATE
|
from idf_py_actions.serial_ext import BAUD_RATE
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
# SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from click.core import Context
|
from rich_click import Context
|
||||||
|
|
||||||
from idf_py_actions.errors import FatalError
|
from idf_py_actions.errors import FatalError
|
||||||
|
from idf_py_actions.tools import PropertyDict
|
||||||
from idf_py_actions.tools import ensure_build_directory
|
from idf_py_actions.tools import ensure_build_directory
|
||||||
from idf_py_actions.tools import is_target_supported
|
from idf_py_actions.tools import is_target_supported
|
||||||
from idf_py_actions.tools import PropertyDict
|
|
||||||
from idf_py_actions.tools import run_target
|
from idf_py_actions.tools import run_target
|
||||||
|
|
||||||
|
|
||||||
def action_extensions(base_actions: Dict, project_path: str) -> Dict:
|
def action_extensions(base_actions: dict, project_path: str) -> dict:
|
||||||
|
|
||||||
SUPPORTED_TARGETS = ['esp32s2', 'esp32s3', 'esp32p4']
|
SUPPORTED_TARGETS = ['esp32s2', 'esp32s3', 'esp32p4']
|
||||||
|
|
||||||
def dfu_target(target_name: str, ctx: Context, args: PropertyDict, part_size: str) -> None:
|
def dfu_target(target_name: str, ctx: Context, args: PropertyDict, part_size: str) -> None:
|
||||||
@@ -29,8 +28,10 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
|
|||||||
run_target(target_name, args, {'ESP_DFU_PATH': path})
|
run_target(target_name, args, {'ESP_DFU_PATH': path})
|
||||||
except FatalError:
|
except FatalError:
|
||||||
# Cannot capture the error from dfu-util here so the best advise is:
|
# Cannot capture the error from dfu-util here so the best advise is:
|
||||||
print('Please have a look at the "Device Firmware Upgrade through USB" chapter in API Guides of the '
|
print(
|
||||||
'ESP-IDF documentation for solving common dfu-util issues.')
|
'Please have a look at the "Device Firmware Upgrade through USB" chapter in API Guides of the '
|
||||||
|
'ESP-IDF documentation for solving common dfu-util issues.'
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
dfu_actions = {
|
dfu_actions = {
|
||||||
@@ -43,8 +44,8 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
|
|||||||
{
|
{
|
||||||
'names': ['--part-size'],
|
'names': ['--part-size'],
|
||||||
'help': 'Large files are split up into smaller partitions in order to avoid timeout during '
|
'help': 'Large files are split up into smaller partitions in order to avoid timeout during '
|
||||||
'erasing flash. This option allows to overwrite the default partition size of '
|
'erasing flash. This option allows to overwrite the default partition size of '
|
||||||
'mkdfu.py.'
|
'mkdfu.py.',
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -62,8 +63,8 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
|
|||||||
'names': ['--path'],
|
'names': ['--path'],
|
||||||
'default': '',
|
'default': '',
|
||||||
'help': 'Specify path to DFU device. The default empty path works if there is just one '
|
'help': 'Specify path to DFU device. The default empty path works if there is just one '
|
||||||
'ESP device with the same product identifier. See the device list for paths '
|
'ESP device with the same product identifier. See the device list for paths '
|
||||||
'of available devices.'
|
'of available devices.',
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
# SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Dict
|
|
||||||
from typing import Optional
|
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
import click
|
from rich_click import Context
|
||||||
|
|
||||||
from idf_py_actions.tools import PropertyDict
|
from idf_py_actions.tools import PropertyDict
|
||||||
from idf_py_actions.tools import RunTool
|
from idf_py_actions.tools import RunTool
|
||||||
@@ -16,20 +13,20 @@ from idf_py_actions.tools import yellow_print
|
|||||||
|
|
||||||
def diag(
|
def diag(
|
||||||
action: str,
|
action: str,
|
||||||
ctx: click.core.Context,
|
ctx: Context,
|
||||||
args: PropertyDict,
|
args: PropertyDict,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
log_prefix: bool,
|
log_prefix: bool,
|
||||||
force: bool,
|
force: bool,
|
||||||
no_color: bool,
|
no_color: bool,
|
||||||
zip_directory: Optional[str],
|
zip_directory: str | None,
|
||||||
list_recipes: bool,
|
list_recipes: bool,
|
||||||
check_recipes: bool,
|
check_recipes: bool,
|
||||||
cmdl_recipes: Tuple,
|
cmdl_recipes: tuple,
|
||||||
cmdl_tags: Tuple,
|
cmdl_tags: tuple,
|
||||||
purge_file: Optional[str],
|
purge_file: str | None,
|
||||||
append: bool,
|
append: bool,
|
||||||
output: Optional[str],
|
output: str | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
diag_args: list = [sys.executable, '-m', 'esp_idf_diag']
|
diag_args: list = [sys.executable, '-m', 'esp_idf_diag']
|
||||||
|
|
||||||
@@ -106,12 +103,10 @@ def diag(
|
|||||||
diag_args += ['--port', args.port]
|
diag_args += ['--port', args.port]
|
||||||
else:
|
else:
|
||||||
yellow_print(
|
yellow_print(
|
||||||
(
|
'The target serial port is not specified, so '
|
||||||
'The target serial port is not specified, so '
|
'autodetection will be used. To set it manually, use '
|
||||||
'autodetection will be used. To set it manually, use '
|
'the "--port" option. Example: "idf.py --port '
|
||||||
'the "--port" option. Example: "idf.py --port '
|
'/dev/ttyUSB0 diag".'
|
||||||
'/dev/ttyUSB0 diag".'
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -121,18 +116,16 @@ def diag(
|
|||||||
|
|
||||||
if command == 'create':
|
if command == 'create':
|
||||||
yellow_print(
|
yellow_print(
|
||||||
(
|
f'Please make sure to thoroughly check it for any sensitive '
|
||||||
f'Please make sure to thoroughly check it for any sensitive '
|
f'information before sharing and remove files you do not want '
|
||||||
f'information before sharing and remove files you do not want '
|
f'to share. Kindly include any additional files you find '
|
||||||
f'to share. Kindly include any additional files you find '
|
f'relevant that were not automatically added. Please archive '
|
||||||
f'relevant that were not automatically added. Please archive '
|
f'the contents of the final report directory using the command:\n'
|
||||||
f'the contents of the final report directory using the command:\n'
|
f'"idf.py diag --zip {output}".'
|
||||||
f'"idf.py diag --zip {output}".'
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def action_extensions(base_actions: Dict, project_path: str) -> Any:
|
def action_extensions(base_actions: dict, project_path: str) -> Any:
|
||||||
return {
|
return {
|
||||||
'actions': {
|
'actions': {
|
||||||
'diag': {
|
'diag': {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
from click.core import Context
|
from rich_click import Context
|
||||||
|
|
||||||
|
|
||||||
class FatalError(RuntimeError):
|
class FatalError(RuntimeError):
|
||||||
@@ -8,7 +8,7 @@ class FatalError(RuntimeError):
|
|||||||
Wrapper class for runtime errors that aren't caused by bugs in idf.py or the build process.
|
Wrapper class for runtime errors that aren't caused by bugs in idf.py or the build process.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, message: str, ctx: Context=None):
|
def __init__(self, message: str, ctx: Context = None):
|
||||||
super(RuntimeError, self).__init__(message)
|
super(RuntimeError, self).__init__(message)
|
||||||
# if context is defined, check for the cleanup tasks
|
# if context is defined, check for the cleanup tasks
|
||||||
if ctx is not None and 'cleanup' in ctx.meta:
|
if ctx is not None and 'cleanup' in ctx.meta:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from click.core import Context
|
from rich_click import Context
|
||||||
|
|
||||||
from idf_py_actions.errors import FatalError
|
from idf_py_actions.errors import FatalError
|
||||||
from idf_py_actions.tools import PropertyDict
|
from idf_py_actions.tools import PropertyDict
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import time
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from click.core import Context
|
from rich_click import Context
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from idf_py_actions.tools import PropertyDict
|
from idf_py_actions.tools import PropertyDict
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import click
|
import rich_click as click
|
||||||
|
from click.core import ParameterSource
|
||||||
|
from rich_click import Context
|
||||||
|
|
||||||
from idf_py_actions.errors import FatalError
|
from idf_py_actions.errors import FatalError
|
||||||
from idf_py_actions.global_options import global_options
|
from idf_py_actions.global_options import global_options
|
||||||
@@ -50,7 +52,7 @@ def yellow_print(message: str, newline: str | None = '\n') -> None:
|
|||||||
|
|
||||||
|
|
||||||
def action_extensions(base_actions: dict, project_path: str) -> dict:
|
def action_extensions(base_actions: dict, project_path: str) -> dict:
|
||||||
def _get_project_desc(ctx: click.core.Context, args: PropertyDict) -> Any:
|
def _get_project_desc(ctx: Context, args: PropertyDict) -> Any:
|
||||||
desc_path = os.path.join(args.build_dir, 'project_description.json')
|
desc_path = os.path.join(args.build_dir, 'project_description.json')
|
||||||
if not os.path.exists(desc_path):
|
if not os.path.exists(desc_path):
|
||||||
ensure_build_directory(args, ctx.info_name)
|
ensure_build_directory(args, ctx.info_name)
|
||||||
@@ -83,7 +85,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
|
|||||||
result += ['--no-stub']
|
result += ['--no-stub']
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _get_commandline_options(ctx: click.core.Context) -> list:
|
def _get_commandline_options(ctx: Context) -> list:
|
||||||
"""Return all the command line options up to first action"""
|
"""Return all the command line options up to first action"""
|
||||||
# This approach ignores argument parsing done Click
|
# This approach ignores argument parsing done Click
|
||||||
result = []
|
result = []
|
||||||
@@ -98,7 +100,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
|
|||||||
|
|
||||||
def monitor(
|
def monitor(
|
||||||
action: str,
|
action: str,
|
||||||
ctx: click.core.Context,
|
ctx: Context,
|
||||||
args: PropertyDict,
|
args: PropertyDict,
|
||||||
print_filter: str,
|
print_filter: str,
|
||||||
monitor_baud: str,
|
monitor_baud: str,
|
||||||
@@ -136,7 +138,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
|
|||||||
# Use the global baud rate if it has been changed by the command line.
|
# Use the global baud rate if it has been changed by the command line.
|
||||||
# Use project_desc['monitor_baud'] as the last option.
|
# Use project_desc['monitor_baud'] as the last option.
|
||||||
|
|
||||||
global_baud_defined = ctx._parameter_source['baud'] == click.core.ParameterSource.COMMANDLINE
|
global_baud_defined = ctx._parameter_source['baud'] == ParameterSource.COMMANDLINE
|
||||||
baud = args.baud if global_baud_defined else project_desc['monitor_baud']
|
baud = args.baud if global_baud_defined else project_desc['monitor_baud']
|
||||||
|
|
||||||
monitor_args += ['-b', baud]
|
monitor_args += ['-b', baud]
|
||||||
@@ -205,7 +207,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
|
|||||||
|
|
||||||
def flash(
|
def flash(
|
||||||
action: str,
|
action: str,
|
||||||
ctx: click.core.Context,
|
ctx: Context,
|
||||||
args: PropertyDict,
|
args: PropertyDict,
|
||||||
force: bool,
|
force: bool,
|
||||||
extra_args: str,
|
extra_args: str,
|
||||||
@@ -240,13 +242,13 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
|
|||||||
}
|
}
|
||||||
run_target(action, args, env, force_progression=True, interactive=True)
|
run_target(action, args, env, force_progression=True, interactive=True)
|
||||||
|
|
||||||
def erase_flash(action: str, ctx: click.core.Context, args: PropertyDict) -> None:
|
def erase_flash(action: str, ctx: Context, args: PropertyDict) -> None:
|
||||||
ensure_build_directory(args, ctx.info_name)
|
ensure_build_directory(args, ctx.info_name)
|
||||||
esptool_args = _get_esptool_args(args)
|
esptool_args = _get_esptool_args(args)
|
||||||
esptool_args += ['erase-flash']
|
esptool_args += ['erase-flash']
|
||||||
RunTool('esptool', esptool_args, args.build_dir, hints=not args.no_hints, interactive=True)()
|
RunTool('esptool', esptool_args, args.build_dir, hints=not args.no_hints, interactive=True)()
|
||||||
|
|
||||||
def global_callback(ctx: click.core.Context, global_args: dict, tasks: PropertyDict) -> None:
|
def global_callback(ctx: Context, global_args: dict, tasks: PropertyDict) -> None:
|
||||||
encryption = any([task.name in ('encrypted-flash', 'encrypted-app-flash') for task in tasks])
|
encryption = any([task.name in ('encrypted-flash', 'encrypted-app-flash') for task in tasks])
|
||||||
if encryption:
|
if encryption:
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
@@ -254,7 +256,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
|
|||||||
task.action_args['encrypted'] = True
|
task.action_args['encrypted'] = True
|
||||||
break
|
break
|
||||||
|
|
||||||
def ota_targets(target_name: str, ctx: click.core.Context, args: PropertyDict) -> None:
|
def ota_targets(target_name: str, ctx: Context, args: PropertyDict) -> None:
|
||||||
"""
|
"""
|
||||||
Execute the target build system to build target 'target_name'.
|
Execute the target build system to build target 'target_name'.
|
||||||
Additionally set global variables for baud and port.
|
Additionally set global variables for baud and port.
|
||||||
@@ -267,7 +269,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
|
|||||||
|
|
||||||
def merge_bin(
|
def merge_bin(
|
||||||
action: str,
|
action: str,
|
||||||
ctx: click.core.Context,
|
ctx: Context,
|
||||||
args: PropertyDict,
|
args: PropertyDict,
|
||||||
output: str,
|
output: str,
|
||||||
format: str, # noqa: A002
|
format: str, # noqa: A002
|
||||||
@@ -315,7 +317,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
|
|||||||
|
|
||||||
def secure_decrypt_flash_data(
|
def secure_decrypt_flash_data(
|
||||||
action: str,
|
action: str,
|
||||||
ctx: click.core.Context,
|
ctx: Context,
|
||||||
args: PropertyDict,
|
args: PropertyDict,
|
||||||
aes_xts: bool,
|
aes_xts: bool,
|
||||||
keyfile: str,
|
keyfile: str,
|
||||||
@@ -341,7 +343,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
|
|||||||
RunTool('espsecure', decrypt_flash_data_args, args.build_dir)()
|
RunTool('espsecure', decrypt_flash_data_args, args.build_dir)()
|
||||||
|
|
||||||
def secure_digest_secure_bootloader(
|
def secure_digest_secure_bootloader(
|
||||||
action: str, ctx: click.core.Context, args: PropertyDict, keyfile: str, output: str, iv: str, **extra_args: str
|
action: str, ctx: Context, args: PropertyDict, keyfile: str, output: str, iv: str, **extra_args: str
|
||||||
) -> None:
|
) -> None:
|
||||||
ensure_build_directory(args, ctx.info_name)
|
ensure_build_directory(args, ctx.info_name)
|
||||||
digest_secure_bootloader_args = [PYTHON, '-m', 'espsecure', 'digest-secure-bootloader']
|
digest_secure_bootloader_args = [PYTHON, '-m', 'espsecure', 'digest-secure-bootloader']
|
||||||
@@ -357,7 +359,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
|
|||||||
|
|
||||||
def secure_encrypt_flash_data(
|
def secure_encrypt_flash_data(
|
||||||
action: str,
|
action: str,
|
||||||
ctx: click.core.Context,
|
ctx: Context,
|
||||||
args: PropertyDict,
|
args: PropertyDict,
|
||||||
aes_xts: bool,
|
aes_xts: bool,
|
||||||
keyfile: str,
|
keyfile: str,
|
||||||
@@ -383,7 +385,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
|
|||||||
RunTool('espsecure', encrypt_flash_data_args, args.build_dir)()
|
RunTool('espsecure', encrypt_flash_data_args, args.build_dir)()
|
||||||
|
|
||||||
def secure_generate_flash_encryption_key(
|
def secure_generate_flash_encryption_key(
|
||||||
action: str, ctx: click.core.Context, args: PropertyDict, keylen: str, **extra_args: str
|
action: str, ctx: Context, args: PropertyDict, keylen: str, **extra_args: str
|
||||||
) -> None:
|
) -> None:
|
||||||
ensure_build_directory(args, ctx.info_name)
|
ensure_build_directory(args, ctx.info_name)
|
||||||
generate_flash_encryption_key_args = [PYTHON, '-m', 'espsecure', 'generate-flash-encryption-key']
|
generate_flash_encryption_key_args = [PYTHON, '-m', 'espsecure', 'generate-flash-encryption-key']
|
||||||
@@ -394,7 +396,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
|
|||||||
RunTool('espsecure', generate_flash_encryption_key_args, args.project_dir)()
|
RunTool('espsecure', generate_flash_encryption_key_args, args.project_dir)()
|
||||||
|
|
||||||
def secure_generate_signing_key(
|
def secure_generate_signing_key(
|
||||||
action: str, ctx: click.core.Context, args: PropertyDict, version: str, scheme: str, **extra_args: str
|
action: str, ctx: Context, args: PropertyDict, version: str, scheme: str, **extra_args: str
|
||||||
) -> None:
|
) -> None:
|
||||||
ensure_build_directory(args, ctx.info_name)
|
ensure_build_directory(args, ctx.info_name)
|
||||||
generate_signing_key_args = [PYTHON, '-m', 'espsecure', 'generate-signing-key']
|
generate_signing_key_args = [PYTHON, '-m', 'espsecure', 'generate-signing-key']
|
||||||
@@ -415,7 +417,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
|
|||||||
RunTool('espsecure', generate_signing_key_args, args.project_dir)()
|
RunTool('espsecure', generate_signing_key_args, args.project_dir)()
|
||||||
|
|
||||||
def secure_generate_key_digest(
|
def secure_generate_key_digest(
|
||||||
action: str, ctx: click.core.Context, args: PropertyDict, keyfile: str, output: str, **extra_args: str
|
action: str, ctx: Context, args: PropertyDict, keyfile: str, output: str, **extra_args: str
|
||||||
) -> None:
|
) -> None:
|
||||||
ensure_build_directory(args, ctx.info_name)
|
ensure_build_directory(args, ctx.info_name)
|
||||||
generate_key_digest_args = [PYTHON, '-m', 'espsecure', 'digest-sbv2-public-key']
|
generate_key_digest_args = [PYTHON, '-m', 'espsecure', 'digest-sbv2-public-key']
|
||||||
@@ -427,7 +429,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
|
|||||||
|
|
||||||
def secure_sign_data(
|
def secure_sign_data(
|
||||||
action: str,
|
action: str,
|
||||||
ctx: click.core.Context,
|
ctx: Context,
|
||||||
args: PropertyDict,
|
args: PropertyDict,
|
||||||
version: str,
|
version: str,
|
||||||
keyfile: str,
|
keyfile: str,
|
||||||
@@ -456,7 +458,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
|
|||||||
RunTool('espsecure', sign_data_args, args.build_dir)()
|
RunTool('espsecure', sign_data_args, args.build_dir)()
|
||||||
|
|
||||||
def secure_verify_signature(
|
def secure_verify_signature(
|
||||||
action: str, ctx: click.core.Context, args: PropertyDict, version: str, keyfile: str, **extra_args: str
|
action: str, ctx: Context, args: PropertyDict, version: str, keyfile: str, **extra_args: str
|
||||||
) -> None:
|
) -> None:
|
||||||
ensure_build_directory(args, ctx.info_name)
|
ensure_build_directory(args, ctx.info_name)
|
||||||
verify_signature_args = [PYTHON, '-m', 'espsecure', 'verify-signature']
|
verify_signature_args = [PYTHON, '-m', 'espsecure', 'verify-signature']
|
||||||
@@ -470,7 +472,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
|
|||||||
|
|
||||||
def secure_generate_nvs_partition_key(
|
def secure_generate_nvs_partition_key(
|
||||||
action: str,
|
action: str,
|
||||||
ctx: click.core.Context,
|
ctx: Context,
|
||||||
args: PropertyDict,
|
args: PropertyDict,
|
||||||
encryption_scheme: str,
|
encryption_scheme: str,
|
||||||
keyfile: str,
|
keyfile: str,
|
||||||
@@ -488,7 +490,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
|
|||||||
RunTool('espsecure', generate_nvs_partition_key_args, args.project_dir)()
|
RunTool('espsecure', generate_nvs_partition_key_args, args.project_dir)()
|
||||||
|
|
||||||
def secure_encrypt_nvs_partition(
|
def secure_encrypt_nvs_partition(
|
||||||
action: str, ctx: click.core.Context, args: PropertyDict, keyfile: str, **extra_args: str
|
action: str, ctx: Context, args: PropertyDict, keyfile: str, **extra_args: str
|
||||||
) -> None:
|
) -> None:
|
||||||
ensure_build_directory(args, ctx.info_name)
|
ensure_build_directory(args, ctx.info_name)
|
||||||
encrypt_nvs_partition_args = [PYTHON, '-m', 'esp_idf_nvs_partition_gen', 'encrypt']
|
encrypt_nvs_partition_args = [PYTHON, '-m', 'esp_idf_nvs_partition_gen', 'encrypt']
|
||||||
@@ -501,7 +503,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
|
|||||||
encrypt_nvs_partition_args += [extra_args['partition_size']]
|
encrypt_nvs_partition_args += [extra_args['partition_size']]
|
||||||
RunTool('espsecure', encrypt_nvs_partition_args, args.project_dir)()
|
RunTool('espsecure', encrypt_nvs_partition_args, args.project_dir)()
|
||||||
|
|
||||||
def _parse_efuse_args(ctx: click.core.Context, args: PropertyDict, extra_args: dict) -> list:
|
def _parse_efuse_args(ctx: Context, args: PropertyDict, extra_args: dict) -> list:
|
||||||
efuse_args = []
|
efuse_args = []
|
||||||
if args.port:
|
if args.port:
|
||||||
efuse_args += ['-p', args.port]
|
efuse_args += ['-p', args.port]
|
||||||
@@ -518,7 +520,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
|
|||||||
efuse_args += ['--do-not-confirm']
|
efuse_args += ['--do-not-confirm']
|
||||||
return efuse_args
|
return efuse_args
|
||||||
|
|
||||||
def efuse_burn(action: str, ctx: click.core.Context, args: PropertyDict, **extra_args: dict) -> None:
|
def efuse_burn(action: str, ctx: Context, args: PropertyDict, **extra_args: dict) -> None:
|
||||||
ensure_build_directory(args, ctx.info_name)
|
ensure_build_directory(args, ctx.info_name)
|
||||||
burn_efuse_args = [PYTHON, '-m', 'espefuse']
|
burn_efuse_args = [PYTHON, '-m', 'espefuse']
|
||||||
burn_efuse_args += _parse_efuse_args(ctx, args, extra_args)
|
burn_efuse_args += _parse_efuse_args(ctx, args, extra_args)
|
||||||
@@ -527,7 +529,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
|
|||||||
burn_efuse_args += list(extra_args['efuse_positional_args'])
|
burn_efuse_args += list(extra_args['efuse_positional_args'])
|
||||||
RunTool('espefuse', burn_efuse_args, args.build_dir)()
|
RunTool('espefuse', burn_efuse_args, args.build_dir)()
|
||||||
|
|
||||||
def efuse_burn_key(action: str, ctx: click.core.Context, args: PropertyDict, **extra_args: str) -> None:
|
def efuse_burn_key(action: str, ctx: Context, args: PropertyDict, **extra_args: str) -> None:
|
||||||
ensure_build_directory(args, ctx.info_name)
|
ensure_build_directory(args, ctx.info_name)
|
||||||
burn_key_args = [PYTHON, '-m', 'espefuse']
|
burn_key_args = [PYTHON, '-m', 'espefuse']
|
||||||
burn_key_args += _parse_efuse_args(ctx, args, extra_args)
|
burn_key_args += _parse_efuse_args(ctx, args, extra_args)
|
||||||
@@ -542,9 +544,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
|
|||||||
burn_key_args += extra_args['efuse_positional_args']
|
burn_key_args += extra_args['efuse_positional_args']
|
||||||
RunTool('espefuse', burn_key_args, args.project_dir, build_dir=args.build_dir)()
|
RunTool('espefuse', burn_key_args, args.project_dir, build_dir=args.build_dir)()
|
||||||
|
|
||||||
def efuse_dump(
|
def efuse_dump(action: str, ctx: Context, args: PropertyDict, file_name: str, **extra_args: dict) -> None:
|
||||||
action: str, ctx: click.core.Context, args: PropertyDict, file_name: str, **extra_args: dict
|
|
||||||
) -> None:
|
|
||||||
ensure_build_directory(args, ctx.info_name)
|
ensure_build_directory(args, ctx.info_name)
|
||||||
dump_args = [PYTHON, '-m', 'espefuse']
|
dump_args = [PYTHON, '-m', 'espefuse']
|
||||||
dump_args += _parse_efuse_args(ctx, args, extra_args)
|
dump_args += _parse_efuse_args(ctx, args, extra_args)
|
||||||
@@ -553,7 +553,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
|
|||||||
dump_args += ['--file-name', file_name]
|
dump_args += ['--file-name', file_name]
|
||||||
RunTool('espefuse', dump_args, args.build_dir)()
|
RunTool('espefuse', dump_args, args.build_dir)()
|
||||||
|
|
||||||
def efuse_read_protect(action: str, ctx: click.core.Context, args: PropertyDict, **extra_args: dict) -> None:
|
def efuse_read_protect(action: str, ctx: Context, args: PropertyDict, **extra_args: dict) -> None:
|
||||||
ensure_build_directory(args, ctx.info_name)
|
ensure_build_directory(args, ctx.info_name)
|
||||||
read_protect_args = [PYTHON, '-m', 'espefuse']
|
read_protect_args = [PYTHON, '-m', 'espefuse']
|
||||||
read_protect_args += _parse_efuse_args(ctx, args, extra_args)
|
read_protect_args += _parse_efuse_args(ctx, args, extra_args)
|
||||||
@@ -564,7 +564,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
|
|||||||
|
|
||||||
def efuse_summary(
|
def efuse_summary(
|
||||||
action: str,
|
action: str,
|
||||||
ctx: click.core.Context,
|
ctx: Context,
|
||||||
args: PropertyDict,
|
args: PropertyDict,
|
||||||
format: str, # noqa: A002
|
format: str, # noqa: A002
|
||||||
**extra_args: dict,
|
**extra_args: dict,
|
||||||
@@ -579,7 +579,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
|
|||||||
summary_args += [str(extra_args['efuse_name'])]
|
summary_args += [str(extra_args['efuse_name'])]
|
||||||
RunTool('espefuse', summary_args, args.build_dir)()
|
RunTool('espefuse', summary_args, args.build_dir)()
|
||||||
|
|
||||||
def efuse_write_protect(action: str, ctx: click.core.Context, args: PropertyDict, **extra_args: dict) -> None:
|
def efuse_write_protect(action: str, ctx: Context, args: PropertyDict, **extra_args: dict) -> None:
|
||||||
ensure_build_directory(args, ctx.info_name)
|
ensure_build_directory(args, ctx.info_name)
|
||||||
write_protect_args = [PYTHON, '-m', 'espefuse']
|
write_protect_args = [PYTHON, '-m', 'espefuse']
|
||||||
write_protect_args += _parse_efuse_args(ctx, args, extra_args)
|
write_protect_args += _parse_efuse_args(ctx, args, extra_args)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from typing import Any
|
|||||||
from typing import TextIO
|
from typing import TextIO
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
import click
|
import rich_click as click
|
||||||
import yaml
|
import yaml
|
||||||
from esp_idf_monitor import get_ansi_converter
|
from esp_idf_monitor import get_ansi_converter
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
# SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
from typing import Dict, List
|
|
||||||
|
|
||||||
from click.core import Context
|
from rich_click import Context
|
||||||
from idf_py_actions.tools import PropertyDict, ensure_build_directory, run_target
|
|
||||||
|
from idf_py_actions.tools import PropertyDict
|
||||||
|
from idf_py_actions.tools import ensure_build_directory
|
||||||
|
from idf_py_actions.tools import run_target
|
||||||
|
|
||||||
|
|
||||||
def action_extensions(base_actions: Dict, project_path: List) -> Dict:
|
def action_extensions(base_actions: dict, project_path: list) -> dict:
|
||||||
def uf2_target(target_name: str, ctx: Context, args: PropertyDict, md5_disable: bool) -> None:
|
def uf2_target(target_name: str, ctx: Context, args: PropertyDict, md5_disable: bool) -> None:
|
||||||
ensure_build_directory(args, ctx.info_name)
|
ensure_build_directory(args, ctx.info_name)
|
||||||
extra = list()
|
extra = list()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
setuptools
|
setuptools
|
||||||
packaging
|
packaging
|
||||||
click
|
click
|
||||||
|
rich-click
|
||||||
pyserial
|
pyserial
|
||||||
cryptography
|
cryptography
|
||||||
pyparsing
|
pyparsing
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from test_build_system_helpers import EnvDict
|
|||||||
from test_build_system_helpers import IdfPyFunc
|
from test_build_system_helpers import IdfPyFunc
|
||||||
from test_build_system_helpers import find_python
|
from test_build_system_helpers import find_python
|
||||||
from test_build_system_helpers import replace_in_file
|
from test_build_system_helpers import replace_in_file
|
||||||
|
from test_build_system_helpers import run_idf_py
|
||||||
|
|
||||||
from conftest import should_clean_test_dir
|
from conftest import should_clean_test_dir
|
||||||
|
|
||||||
@@ -185,10 +186,11 @@ def test_extension_from_component(idf_py: IdfPyFunc, test_app_copy: Path) -> Non
|
|||||||
idf_py('reconfigure')
|
idf_py('reconfigure')
|
||||||
ret = idf_py('--help')
|
ret = idf_py('--help')
|
||||||
assert 'test-component-action' in ret.stdout
|
assert 'test-component-action' in ret.stdout
|
||||||
assert 'INFO: Loaded component extension from "components/test_component"' in ret.stdout
|
expected_info = f'INFO: Loaded component extension from "{os.path.join("components", "test_component")}"'
|
||||||
|
assert expected_info in ret.stdout
|
||||||
ret = idf_py('test-component-action')
|
ret = idf_py('test-component-action')
|
||||||
assert 'Test extension action executed - component extension' in ret.stdout
|
assert 'Test extension action executed - component extension' in ret.stdout
|
||||||
assert 'INFO: Loaded component extension from "components/test_component"' in ret.stdout
|
assert expected_info in ret.stdout
|
||||||
|
|
||||||
|
|
||||||
def test_extension_from_component_invalid_syntax(idf_py: IdfPyFunc, test_app_copy: Path) -> None:
|
def test_extension_from_component_invalid_syntax(idf_py: IdfPyFunc, test_app_copy: Path) -> None:
|
||||||
@@ -239,6 +241,47 @@ def test_extension_from_component_invalid_syntax(idf_py: IdfPyFunc, test_app_cop
|
|||||||
assert 'Attribute "version" is required in custom extension.' in ret.stderr
|
assert 'Attribute "version" is required in custom extension.' in ret.stderr
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures('test_app_copy')
|
||||||
|
def test_idf_py_help_rich_click_component_extension_panel(
|
||||||
|
idf_py: IdfPyFunc,
|
||||||
|
default_idf_env: dict[str, str],
|
||||||
|
) -> None:
|
||||||
|
"""Default Commands panel first; component extension gets its own panel after."""
|
||||||
|
idf_py('create-component', '-C', 'components', 'help_group_comp')
|
||||||
|
comp_dir = Path('components') / 'help_group_comp'
|
||||||
|
(comp_dir / 'idf_ext.py').write_text(
|
||||||
|
textwrap.dedent(
|
||||||
|
TEST_EXT_TEMPLATE.format(
|
||||||
|
suffix='help panel comp',
|
||||||
|
global_options='',
|
||||||
|
actions="""'help-group-comp-cmd': {
|
||||||
|
'callback': test_extension_action,
|
||||||
|
'help': 'Component extension command for help panel test'
|
||||||
|
}""",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
replace_in_file(
|
||||||
|
Path('main') / 'CMakeLists.txt',
|
||||||
|
'# placeholder_inside_idf_component_register',
|
||||||
|
'\n'.join(['INCLUDE_DIRS "." ', 'REQUIRES "help_group_comp" ']),
|
||||||
|
)
|
||||||
|
idf_py('reconfigure')
|
||||||
|
|
||||||
|
env = {**default_idf_env, 'COLUMNS': '120', 'NO_COLOR': '1'}
|
||||||
|
ret = run_idf_py('--help', env=env, workdir=os.getcwd(), check=True)
|
||||||
|
|
||||||
|
idx_commands = ret.stdout.find('Commands')
|
||||||
|
assert idx_commands != -1, 'Root idf.py --help should list the default Commands group.'
|
||||||
|
idx_comp_panel = ret.stdout.find('help_group_comp', idx_commands)
|
||||||
|
assert idx_comp_panel != -1, 'Expected help group for the help_group_comp component extension.'
|
||||||
|
assert idx_comp_panel > idx_commands, 'Component extension group should appear after the default Commands group.'
|
||||||
|
|
||||||
|
# ret.stdout[i:j]: substring from index i (inclusive) to j (exclusive); ':' separates the two bounds.
|
||||||
|
default_block = ret.stdout[idx_commands:idx_comp_panel]
|
||||||
|
assert 'build' in default_block, 'Built-in `build` should still appear under the default Commands panel.'
|
||||||
|
|
||||||
|
|
||||||
# ----------- Test cases for entry point extension -----------
|
# ----------- Test cases for entry point extension -----------
|
||||||
|
|
||||||
|
|
||||||
@@ -370,6 +413,44 @@ def test_extension_entrypoint_conflicting_names(
|
|||||||
assert 'This global option conflicts with existing one' not in ret.stdout
|
assert 'This global option conflicts with existing one' not in ret.stdout
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures('test_app_copy')
|
||||||
|
def test_idf_py_help_rich_click_entrypoint_extension_panel(
|
||||||
|
idf_py: IdfPyFunc,
|
||||||
|
default_idf_env: dict[str, str],
|
||||||
|
extension_package_manager: ExtensionPackageManager,
|
||||||
|
) -> None:
|
||||||
|
"""Default Commands panel first; entry-point extension gets its own panel after."""
|
||||||
|
extension_package_manager.create_package('helpgroup')
|
||||||
|
|
||||||
|
env = {**default_idf_env, 'COLUMNS': '120', 'NO_COLOR': '1'}
|
||||||
|
ret = run_idf_py('--help', env=env, workdir=os.getcwd(), check=True)
|
||||||
|
|
||||||
|
idx_commands = ret.stdout.find('Commands')
|
||||||
|
assert idx_commands != -1, 'Root idf.py --help should list the default Commands group.'
|
||||||
|
idx_ep_panel = ret.stdout.find('test_extension_helpgroup', idx_commands)
|
||||||
|
assert idx_ep_panel != -1, 'Expected help group for the helpgroup entry-point extension.'
|
||||||
|
assert idx_ep_panel > idx_commands, 'Entry-point extension group should appear after the default Commands group.'
|
||||||
|
|
||||||
|
# ret.stdout[i:j]: substring from index i (inclusive) to j (exclusive); ':' separates the two bounds.
|
||||||
|
default_block = ret.stdout[idx_commands:idx_ep_panel]
|
||||||
|
assert 'build' in default_block, 'Built-in `build` should still appear under the default Commands panel.'
|
||||||
|
|
||||||
|
|
||||||
|
# ----------- General extension tests -----------
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures('test_app_copy')
|
||||||
|
def test_idf_py_subcommand_help_shows_options(
|
||||||
|
idf_py: IdfPyFunc,
|
||||||
|
default_idf_env: dict[str, str],
|
||||||
|
) -> None:
|
||||||
|
"""Subcommand --help must list global/action options (rich-click + default_panels_first)."""
|
||||||
|
idf_py('reconfigure')
|
||||||
|
env = {**default_idf_env, 'NO_COLOR': '1', 'COLUMNS': '120'}
|
||||||
|
ret = run_idf_py('flash', '--help', env=env, workdir=os.getcwd(), check=True)
|
||||||
|
assert '--project-dir' in ret.stdout or '-C ' in ret.stdout
|
||||||
|
|
||||||
|
|
||||||
# ----------- Regression test: idf.py recursion via idf_version clause -----------
|
# ----------- Regression test: idf.py recursion via idf_version clause -----------
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,28 @@ py_actions_path = os.path.join(current_dir, '..', 'idf_py_actions')
|
|||||||
link_path = os.path.join(py_actions_path, 'test_ext')
|
link_path = os.path.join(py_actions_path, 'test_ext')
|
||||||
|
|
||||||
|
|
||||||
|
# As idf.py uses rich-click, unite modification variables to ensure constant results on various CI terminals
|
||||||
|
_idf_py_test_env_saved: dict[str, str | None] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def setUpModule() -> None:
|
||||||
|
for key in ('COLUMNS', 'LINES', 'NO_COLOR', 'FORCE_COLOR', 'PY_COLORS', 'TERM'):
|
||||||
|
_idf_py_test_env_saved[key] = os.environ.get(key)
|
||||||
|
os.environ['COLUMNS'] = '200'
|
||||||
|
os.environ['LINES'] = '40'
|
||||||
|
os.environ['NO_COLOR'] = '1'
|
||||||
|
for unset in ('FORCE_COLOR', 'PY_COLORS'):
|
||||||
|
os.environ.pop(unset, None)
|
||||||
|
|
||||||
|
|
||||||
|
def tearDownModule() -> None:
|
||||||
|
for key, previous in _idf_py_test_env_saved.items():
|
||||||
|
if previous is None:
|
||||||
|
os.environ.pop(key, None)
|
||||||
|
else:
|
||||||
|
os.environ[key] = previous
|
||||||
|
|
||||||
|
|
||||||
class TestWithoutExtensions(TestCase):
|
class TestWithoutExtensions(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
|
|||||||
Reference in New Issue
Block a user