From 32e1303c3e0a6ec70aba2518830e5d912ea5f7ee Mon Sep 17 00:00:00 2001 From: Tomas Rezucha Date: Wed, 27 May 2026 11:37:45 +0200 Subject: [PATCH] feat(tools): Add 'check' field to idf.py actions --- tools/idf.py | 16 ++++++++++++++++ tools/idf_py_actions/dfu_ext.py | 22 +++++++++++++++------- tools/idf_py_actions/tools.py | 9 +-------- tools/test_build_system/test_build.py | 12 +++++++++--- 4 files changed, 41 insertions(+), 18 deletions(-) diff --git a/tools/idf.py b/tools/idf.py index 33c50ab6921..8f111f0a591 100755 --- a/tools/idf.py +++ b/tools/idf.py @@ -255,6 +255,7 @@ def init_cli(verbose_output: list | None = None) -> Any: def __init__( self, callback: Callable, + check: Callable | None, name: str, aliases: list, dependencies: list | None, @@ -262,6 +263,7 @@ def init_cli(verbose_output: list | None = None) -> Any: action_args: dict, ) -> None: self.callback = callback + self.check = check self.name = name self.dependencies = dependencies self.order_dependencies = order_dependencies @@ -274,6 +276,15 @@ def init_cli(verbose_output: list | None = None) -> Any: self.callback(self.name, context, global_args, **action_args) + def check_requirements( + self, context: click.core.Context, global_args: PropertyDict, action_args: dict | None = None + ) -> bool: + if self.check is None: + return True + if action_args is None: + action_args = self.action_args + return bool(self.check(self.name, context, global_args, **action_args)) + class Action(click.RichCommand): callback: Callable @@ -284,6 +295,7 @@ def init_cli(verbose_output: list | None = None) -> Any: deprecated: dict | str | bool = False, dependencies: list | None = None, order_dependencies: list | None = None, + check: Callable | None = None, hidden: bool = False, **kwargs: Any, ) -> None: @@ -326,6 +338,7 @@ def init_cli(verbose_output: list | None = None) -> Any: def wrapped_callback(**action_args: Any) -> Task: return Task( callback=self.unwrapped_callback, + check=check, name=self.name, dependencies=dependencies, order_dependencies=order_dependencies, @@ -796,6 +809,9 @@ def init_cli(verbose_output: list | None = None) -> Any: dependecies_processed = True + if not task.check_requirements(ctx, global_args, task.action_args): + raise FatalError(f'Action "{task.name}" cannot run in the current project configuration.') + # If task have some dependencies they have to be executed before the task. for dep in task.dependencies: if dep not in tasks_to_run.keys(): diff --git a/tools/idf_py_actions/dfu_ext.py b/tools/idf_py_actions/dfu_ext.py index 044346d5d2f..58a4565c170 100644 --- a/tools/idf_py_actions/dfu_ext.py +++ b/tools/idf_py_actions/dfu_ext.py @@ -1,24 +1,29 @@ # SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 -import os - from rich_click import Context 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 get_sdkconfig_filename from idf_py_actions.tools import get_sdkconfig_value +from idf_py_actions.tools import red_print from idf_py_actions.tools import run_target SOC_USB_DFU_SUPPORTED = 'CONFIG_SOC_USB_DFU_SUPPORTED' -def _soc_usb_dfu_supported(project_path: str) -> bool: - return bool(get_sdkconfig_value(os.path.join(project_path, 'sdkconfig'), SOC_USB_DFU_SUPPORTED) == 'y') - - def action_extensions(base_actions: dict, project_path: str) -> dict: + def check_dfu_supported(target_name: str, ctx: Context, args: PropertyDict, **kwargs: str) -> bool: + sdkconfig_path = get_sdkconfig_filename(args) + if get_sdkconfig_value(sdkconfig_path, SOC_USB_DFU_SUPPORTED) == 'y': + return True + + target = get_sdkconfig_value(sdkconfig_path, 'CONFIG_IDF_TARGET') or 'unknown' + red_print(f'DFU is not supported for this target: {target}') + return False + def dfu_target(target_name: str, ctx: Context, args: PropertyDict, part_size: str) -> None: ensure_build_directory(args, ctx.info_name) run_target(target_name, args, {'ESP_DFU_PART_SIZE': part_size} if part_size else {}) @@ -44,6 +49,7 @@ def action_extensions(base_actions: dict, project_path: str) -> dict: 'actions': { 'dfu': { 'callback': dfu_target, + 'check': check_dfu_supported, 'short_help': 'Build the DFU binary', 'dependencies': ['all'], 'options': [ @@ -57,11 +63,13 @@ def action_extensions(base_actions: dict, project_path: str) -> dict: }, 'dfu-list': { 'callback': dfu_list_target, + 'check': check_dfu_supported, 'short_help': 'List DFU capable devices', 'dependencies': [], }, 'dfu-flash': { 'callback': dfu_flash_target, + 'check': check_dfu_supported, 'short_help': 'Flash the DFU binary', 'order_dependencies': ['dfu'], 'options': [ @@ -77,4 +85,4 @@ def action_extensions(base_actions: dict, project_path: str) -> dict: } } - return dfu_actions if _soc_usb_dfu_supported(project_path) else {} + return dfu_actions diff --git a/tools/idf_py_actions/tools.py b/tools/idf_py_actions/tools.py index 3e4e2648154..61f993e1e5e 100644 --- a/tools/idf_py_actions/tools.py +++ b/tools/idf_py_actions/tools.py @@ -988,19 +988,12 @@ def get_sdkconfig_value(sdkconfig_file: str, key: str) -> str | None: pattern = re.compile(rf'^{key}=\"?([^\"]*)\"?$') with open(sdkconfig_file, encoding='utf-8') as f: for line in f: - match = re.match(pattern, line) + match = re.match(pattern, line.strip()) if match: value = match.group(1) return value -def is_target_supported(project_path: str, supported_targets: list) -> bool: - """ - Returns True if the active target is supported, or False otherwise. - """ - return get_target(project_path) in supported_targets - - def _check_idf_target( args: 'PropertyDict', prog_name: str, cache: dict, cache_cmdl: dict, env: dict | None = None ) -> None: diff --git a/tools/test_build_system/test_build.py b/tools/test_build_system/test_build.py index 38aaba37c6a..1fc1a20f674 100644 --- a/tools/test_build_system/test_build.py +++ b/tools/test_build_system/test_build.py @@ -175,11 +175,17 @@ def test_build_fail_on_build_time(idf_py: IdfPyFunc, test_app_copy: Path) -> Non @pytest.mark.usefixtures('test_app_copy') def test_build_dfu(idf_py: IdfPyFunc) -> None: - logging.info('DFU build works') + logging.info('DFU is rejected for targets without USB DFU support') + idf_py('set-target', 'esp32') ret = idf_py('dfu', check=False) - assert 'command "dfu" is not known to idf.py and is not a Ninja target' in ret.stderr, ( - 'DFU build should fail for default chip target' + assert ret.returncode != 0 + assert 'DFU is not supported for this target: esp32' in ret.stderr + assert 'Action "dfu" cannot run in the current project configuration.' in ret.stderr + assert 'Adding "dfu"\'s dependency "all"' not in ret.stdout, ( + 'DFU check must fail before the "all" dependency is scheduled' ) + + logging.info('DFU build works for esp32s2') idf_py('set-target', 'esp32s2') ret = idf_py('dfu') assert 'build/dfu.bin" has been written. You may proceed with DFU flashing.' in ret.stdout, (