feat(tools): Add 'check' field to idf.py actions

This commit is contained in:
Tomas Rezucha
2026-05-27 11:37:45 +02:00
committed by BOT
parent f7e24571ea
commit 32e1303c3e
4 changed files with 41 additions and 18 deletions

View File

@@ -255,6 +255,7 @@ def init_cli(verbose_output: list | None = None) -> Any:
def __init__( def __init__(
self, self,
callback: Callable, callback: Callable,
check: Callable | None,
name: str, name: str,
aliases: list, aliases: list,
dependencies: list | None, dependencies: list | None,
@@ -262,6 +263,7 @@ def init_cli(verbose_output: list | None = None) -> Any:
action_args: dict, action_args: dict,
) -> None: ) -> None:
self.callback = callback self.callback = callback
self.check = check
self.name = name self.name = name
self.dependencies = dependencies self.dependencies = dependencies
self.order_dependencies = order_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) 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): class Action(click.RichCommand):
callback: Callable callback: Callable
@@ -284,6 +295,7 @@ def init_cli(verbose_output: list | None = None) -> Any:
deprecated: dict | str | bool = False, deprecated: dict | str | bool = False,
dependencies: list | None = None, dependencies: list | None = None,
order_dependencies: list | None = None, order_dependencies: list | None = None,
check: Callable | None = None,
hidden: bool = False, hidden: bool = False,
**kwargs: Any, **kwargs: Any,
) -> None: ) -> None:
@@ -326,6 +338,7 @@ def init_cli(verbose_output: list | None = None) -> Any:
def wrapped_callback(**action_args: Any) -> Task: def wrapped_callback(**action_args: Any) -> Task:
return Task( return Task(
callback=self.unwrapped_callback, callback=self.unwrapped_callback,
check=check,
name=self.name, name=self.name,
dependencies=dependencies, dependencies=dependencies,
order_dependencies=order_dependencies, order_dependencies=order_dependencies,
@@ -796,6 +809,9 @@ def init_cli(verbose_output: list | None = None) -> Any:
dependecies_processed = True 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. # If task have some dependencies they have to be executed before the task.
for dep in task.dependencies: for dep in task.dependencies:
if dep not in tasks_to_run.keys(): if dep not in tasks_to_run.keys():

View File

@@ -1,24 +1,29 @@
# SPDX-FileCopyrightText: 2022-2026 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
from rich_click 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
from idf_py_actions.tools import ensure_build_directory 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 get_sdkconfig_value
from idf_py_actions.tools import red_print
from idf_py_actions.tools import run_target from idf_py_actions.tools import run_target
SOC_USB_DFU_SUPPORTED = 'CONFIG_SOC_USB_DFU_SUPPORTED' 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 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: def dfu_target(target_name: str, ctx: Context, args: PropertyDict, part_size: str) -> None:
ensure_build_directory(args, ctx.info_name) ensure_build_directory(args, ctx.info_name)
run_target(target_name, args, {'ESP_DFU_PART_SIZE': part_size} if part_size else {}) 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': { 'actions': {
'dfu': { 'dfu': {
'callback': dfu_target, 'callback': dfu_target,
'check': check_dfu_supported,
'short_help': 'Build the DFU binary', 'short_help': 'Build the DFU binary',
'dependencies': ['all'], 'dependencies': ['all'],
'options': [ 'options': [
@@ -57,11 +63,13 @@ def action_extensions(base_actions: dict, project_path: str) -> dict:
}, },
'dfu-list': { 'dfu-list': {
'callback': dfu_list_target, 'callback': dfu_list_target,
'check': check_dfu_supported,
'short_help': 'List DFU capable devices', 'short_help': 'List DFU capable devices',
'dependencies': [], 'dependencies': [],
}, },
'dfu-flash': { 'dfu-flash': {
'callback': dfu_flash_target, 'callback': dfu_flash_target,
'check': check_dfu_supported,
'short_help': 'Flash the DFU binary', 'short_help': 'Flash the DFU binary',
'order_dependencies': ['dfu'], 'order_dependencies': ['dfu'],
'options': [ '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

View File

@@ -988,19 +988,12 @@ def get_sdkconfig_value(sdkconfig_file: str, key: str) -> str | None:
pattern = re.compile(rf'^{key}=\"?([^\"]*)\"?$') pattern = re.compile(rf'^{key}=\"?([^\"]*)\"?$')
with open(sdkconfig_file, encoding='utf-8') as f: with open(sdkconfig_file, encoding='utf-8') as f:
for line in f: for line in f:
match = re.match(pattern, line) match = re.match(pattern, line.strip())
if match: if match:
value = match.group(1) value = match.group(1)
return value 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( def _check_idf_target(
args: 'PropertyDict', prog_name: str, cache: dict, cache_cmdl: dict, env: dict | None = None args: 'PropertyDict', prog_name: str, cache: dict, cache_cmdl: dict, env: dict | None = None
) -> None: ) -> None:

View File

@@ -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') @pytest.mark.usefixtures('test_app_copy')
def test_build_dfu(idf_py: IdfPyFunc) -> None: 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) ret = idf_py('dfu', check=False)
assert 'command "dfu" is not known to idf.py and is not a Ninja target' in ret.stderr, ( assert ret.returncode != 0
'DFU build should fail for default chip target' 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') idf_py('set-target', 'esp32s2')
ret = idf_py('dfu') ret = idf_py('dfu')
assert 'build/dfu.bin" has been written. You may proceed with DFU flashing.' in ret.stdout, ( assert 'build/dfu.bin" has been written. You may proceed with DFU flashing.' in ret.stdout, (