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 <frantisek.hrbata@espressif.com>
This commit is contained in:
Frantisek Hrbata
2026-03-20 08:13:26 +01:00
parent 512dacab6a
commit 2aacc2ad0c
4 changed files with 85 additions and 1 deletions
+35 -1
View File
@@ -68,6 +68,32 @@ function(__init_kconfig)
idf_build_set_property(CONFIG_DIR "${config_dir}") idf_build_set_property(CONFIG_DIR "${config_dir}")
endfunction() 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 .. cmakev2:function:: __should_generate_sdkconfig
@@ -671,12 +697,20 @@ function(__create_base_kconfgen_command sdkconfig sdkconfig_defaults)
endforeach() endforeach()
endif() 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 # Create base kconfgen command
set(base_kconfgen_cmd ${python} -m kconfgen set(base_kconfgen_cmd ${python} -m kconfgen
--list-separator=semicolon --list-separator=semicolon
--kconfig "${root_kconfig}" --kconfig "${root_kconfig}"
--sdkconfig-rename "${root_sdkconfig_rename}" --sdkconfig-rename "${root_sdkconfig_rename}"
--config "${sdkconfig}" --config "${sdkconfig_orig}"
${defaults_args} ${defaults_args}
--env "IDF_BUILD_V2=y") --env "IDF_BUILD_V2=y")
+9
View File
@@ -83,6 +83,15 @@ function(__fetch_components_from_registry)
idf_die("IDF Component Manager error: ${cmgr_result}") idf_die("IDF Component Manager error: ${cmgr_result}")
endif() endif()
endwhile() 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() endfunction()
#[[ #[[
+3
View File
@@ -581,6 +581,9 @@ macro(idf_project_init)
# Discover and initialize components # Discover and initialize components
__init_components() __init_components()
# Save original sdkconfig before kconfgen may drop unknown options
__create_sdkconfig_orig_copy()
# Generate initial sdkconfig with discovered components # Generate initial sdkconfig with discovered components
__generate_sdkconfig() __generate_sdkconfig()
@@ -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')