From 0f349ee4044d2c7f4ab01500e69f86509f6b396a Mon Sep 17 00:00:00 2001 From: Frantisek Hrbata Date: Wed, 18 Feb 2026 08:52:37 +0100 Subject: [PATCH 1/2] fix(cmakev2/kconfig): preserve sdkconfig options from managed components The sdkconfig file may contain configuration options defined in Kconfig files of managed components. Since kconfgen runs before the component manager fetches these components, the Kconfig definitions for managed component options are not yet available. The kconfgen --output config flag regenerates sdkconfig from kconfiglib's internal state, which only knows about options with loaded Kconfig definitions. This causes unknown options (i.e., those from managed components) to be silently dropped from sdkconfig during intermediate regeneration rounds. Note that kconfgen's --config flag (used for reading sdkconfig) only performs deprecated option name replacement and does NOT drop unknown options. The problem is exclusively in --output config, which writes a fresh sdkconfig from the parsed Kconfig tree state. Fix this by introducing a __SDKCONFIG_ORIG build property that provides an indirection layer for the --config input path: - Before the component manager runs: __SDKCONFIG_ORIG points to a copy of the original sdkconfig (build/sdkconfig.orig), created by the new __create_sdkconfig_orig_copy() function. This copy preserves all original options, including those from managed components. - During intermediate kconfgen runs: --config reads from the preserved copy (so unknown options survive as input), while --output config writes to the real sdkconfig (unknown options may be dropped there, but this is harmless since kconfgen always reads from the copy). - After the component manager completes: __SDKCONFIG_ORIG is reset to point to the real sdkconfig and __BASE_KCONFGEN_CMD is rebuilt, so that subsequent operations (menuconfig, save-defconfig, confserver) read and write the actual sdkconfig file directly. The flow is: __create_sdkconfig_orig_copy() -> __SDKCONFIG_ORIG = build/sdkconfig.orig __generate_sdkconfig() -> --config build/sdkconfig.orig --output config project/sdkconfig __fetch_components_from_registry(): loop: download_components() __generate_sdkconfig() -> --config build/sdkconfig.orig --output config project/sdkconfig if success: break endloop -> __SDKCONFIG_ORIG = project/sdkconfig -> rebuild __BASE_KCONFGEN_CMD idf_create_menuconfig() / save-defconfig / confserver -> uses --config project/sdkconfig (the real file) Signed-off-by: Frantisek Hrbata --- tools/cmakev2/kconfig.cmake | 36 +++++++++++++++++- tools/cmakev2/manager.cmake | 9 +++++ tools/cmakev2/project.cmake | 3 ++ .../buildv2/test_sdkconfig.py | 38 +++++++++++++++++++ 4 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 tools/test_build_system/buildv2/test_sdkconfig.py diff --git a/tools/cmakev2/kconfig.cmake b/tools/cmakev2/kconfig.cmake index 786397b4dab..aabfda66b8a 100644 --- a/tools/cmakev2/kconfig.cmake +++ b/tools/cmakev2/kconfig.cmake @@ -68,6 +68,32 @@ function(__init_kconfig) idf_build_set_property(CONFIG_DIR "${config_dir}") endfunction() +#[[ + __create_sdkconfig_orig_copy() + + Create a copy of the sdkconfig file in the build directory to preserve + all original options, including those from managed components that are + not yet known to kconfgen. The copy is referenced via __SDKCONFIG_ORIG + and used as the --config input for kconfgen, so that unknown options + are not dropped during intermediate sdkconfig regeneration rounds. + + After the component manager has fetched all components (and their Kconfig + definitions are available), __SDKCONFIG_ORIG is reset to point to the + real sdkconfig so that subsequent operations (menuconfig, etc.) read + and write the actual file. +#]] +function(__create_sdkconfig_orig_copy) + idf_build_get_property(sdkconfig SDKCONFIG) + idf_build_get_property(build_dir BUILD_DIR) + set(sdkconfig_orig "${build_dir}/sdkconfig.orig") + if(EXISTS "${sdkconfig}") + file(COPY_FILE "${sdkconfig}" "${sdkconfig_orig}" ONLY_IF_DIFFERENT) + else() + set(sdkconfig_orig "${sdkconfig}") + endif() + idf_build_set_property(__SDKCONFIG_ORIG "${sdkconfig_orig}") +endfunction() + #[[ .. cmakev2:function:: __should_generate_sdkconfig @@ -671,12 +697,20 @@ function(__create_base_kconfgen_command sdkconfig sdkconfig_defaults) endforeach() endif() + # Use __SDKCONFIG_ORIG for --config so that unknown options from managed + # components are preserved during intermediate kconfgen runs. Falls back + # to the real sdkconfig when __SDKCONFIG_ORIG is not yet set. + idf_build_get_property(sdkconfig_orig __SDKCONFIG_ORIG) + if(NOT sdkconfig_orig) + set(sdkconfig_orig "${sdkconfig}") + endif() + # Create base kconfgen command set(base_kconfgen_cmd ${python} -m kconfgen --list-separator=semicolon --kconfig "${root_kconfig}" --sdkconfig-rename "${root_sdkconfig_rename}" - --config "${sdkconfig}" + --config "${sdkconfig_orig}" ${defaults_args} --env "IDF_BUILD_V2=y") diff --git a/tools/cmakev2/manager.cmake b/tools/cmakev2/manager.cmake index 7bf8d6e6582..c0488e6ec01 100644 --- a/tools/cmakev2/manager.cmake +++ b/tools/cmakev2/manager.cmake @@ -83,6 +83,15 @@ function(__fetch_components_from_registry) idf_die("IDF Component Manager error: ${cmgr_result}") endif() endwhile() + + # All managed components are now fetched and their Kconfig definitions + # are available. Point __SDKCONFIG_ORIG back to the real sdkconfig so + # that subsequent operations (menuconfig, save-defconfig, confserver) + # read and write the actual file, not the preserved copy. + idf_build_get_property(sdkconfig SDKCONFIG) + idf_build_set_property(__SDKCONFIG_ORIG "${sdkconfig}") + idf_build_get_property(sdkconfig_defaults SDKCONFIG_DEFAULTS) + __create_base_kconfgen_command("${sdkconfig}" "${sdkconfig_defaults}") endfunction() #[[ diff --git a/tools/cmakev2/project.cmake b/tools/cmakev2/project.cmake index cd0392a9908..cf8c1060f64 100644 --- a/tools/cmakev2/project.cmake +++ b/tools/cmakev2/project.cmake @@ -581,6 +581,9 @@ macro(idf_project_init) # Discover and initialize components __init_components() + # Save original sdkconfig before kconfgen may drop unknown options + __create_sdkconfig_orig_copy() + # Generate initial sdkconfig with discovered components __generate_sdkconfig() diff --git a/tools/test_build_system/buildv2/test_sdkconfig.py b/tools/test_build_system/buildv2/test_sdkconfig.py new file mode 100644 index 00000000000..33a0488c65d --- /dev/null +++ b/tools/test_build_system/buildv2/test_sdkconfig.py @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +import logging +from pathlib import Path + +from test_build_system_helpers import IdfPyFunc +from test_build_system_helpers import file_contains + + +def test_sdkconfig_preserves_managed_component_options(idf_py: IdfPyFunc, test_app_copy: Path) -> None: + logging.info('sdkconfig options from managed components should be preserved during reconfigure') + + # Create a local managed component outside the project directory. + # Since it is referenced via path in idf_component.yml, the component + # manager processes it as a managed component whose Kconfig is only + # available after the CM run -- i.e. after the initial kconfgen round. + managed_comp_dir = test_app_copy / '..' / 'managed_comp' + managed_comp_dir.mkdir(parents=True) + (managed_comp_dir / 'CMakeLists.txt').write_text('idf_component_register()\n') + (managed_comp_dir / 'Kconfig').write_text( + 'config MANAGED_COMP_TEST_OPTION\n int "Managed component test option"\n default 10\n' + ) + + # Reference the local component as a managed dependency + (test_app_copy / 'main' / 'idf_component.yml').write_text( + 'dependencies:\n managed_comp:\n path: "../../managed_comp"\n' + ) + + # Write a non-default value directly into sdkconfig (not sdkconfig.defaults). + # Before the fix, kconfgen would drop this unknown option via --output config + # during the first generation round (before the component manager makes the + # managed component's Kconfig available), causing the value to revert to the + # default (10) in the final sdkconfig. + (test_app_copy / 'sdkconfig').write_text('CONFIG_MANAGED_COMP_TEST_OPTION=30\n') + + idf_py('reconfigure') + + assert file_contains(test_app_copy / 'sdkconfig', 'CONFIG_MANAGED_COMP_TEST_OPTION=30') From 7bc635bd9ee23df962a1c14bd43ae6ca64057d4c Mon Sep 17 00:00:00 2001 From: Frantisek Hrbata Date: Wed, 18 Feb 2026 09:31:05 +0100 Subject: [PATCH 2/2] fix(test_build_system): add __init__.py to buildv2 test directory Pytest's default import mode (prepend/rootdir) imports test modules as top-level modules keyed by filename. When two directories contain test files with the same basename (e.g. test_sdkconfig.py in both test_build_system/ and test_build_system/buildv2/), pytest tries to register both as the module name "test_sdkconfig". The second collection fails with "import file mismatch" because the module object already cached in sys.modules points to the first file. Adding __init__.py to the buildv2/ directory makes it a proper Python package. Pytest then imports its test modules under the package namespace (buildv2.test_sdkconfig), which is distinct from the top-level test_sdkconfig, resolving the collision. Signed-off-by: Frantisek Hrbata --- tools/test_build_system/buildv2/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tools/test_build_system/buildv2/__init__.py diff --git a/tools/test_build_system/buildv2/__init__.py b/tools/test_build_system/buildv2/__init__.py new file mode 100644 index 00000000000..e69de29bb2d