diff --git a/tools/cmakev2/kconfig.cmake b/tools/cmakev2/kconfig.cmake index bc944116b85..a4b08c5a53b 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 770d535b0d2..fed486e1575 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/__init__.py b/tools/test_build_system/buildv2/__init__.py new file mode 100644 index 00000000000..e69de29bb2d 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')