feat: Adopt rebuild optimization tool (configdep) in cmakev2

This commit is contained in:
Jan Beran
2026-07-28 13:56:16 +02:00
parent 2ed759adcd
commit 8b213b517d
3 changed files with 115 additions and 34 deletions

View File

@@ -21,11 +21,6 @@ set(CMAKE_MODULE_PATH
# for both cmakev1 and cmakev2.
include(${CMAKE_CURRENT_LIST_DIR}/../cmake/version.cmake)
# Suppress CMake warning: "Manually-specified variables were not used by the project: CONFIGDEP_ENABLE"
# (CONFIGDEP_ENABLE is passed by idf.py but only used in the cmake v1 project() flow in project.cmake)
# FIXME: When cmakev2 will start supporting configdep, this can be removed.
set(_idf_ignore_configdep_enable "${CONFIGDEP_ENABLE}")
# The gdbinit.cmake file from cmakev1 contains a single function,
# __generate_gdbinit, which is used in the generation of
# project_description.json.
@@ -376,22 +371,62 @@ function(__init_toolchain)
endfunction()
#[[
__init_ccache()
__init_compiler_launchers()
Enable ccache if requested through CCACHE_ENABLE.
Build the compiler launcher chain and apply it to
CMAKE_{C,CXX,ASM}_COMPILER_LAUNCHER. The chain runs esp-idf-configdep first
(when configdep is enabled) followed by ccache (when enabled).
#]]
function(__init_ccache)
if(NOT CCACHE_ENABLE)
function(__init_compiler_launchers)
set(launcher_chain "")
# esp-idf-configdep goes first in the chain.
# Gate on the same esp-idf-kconfig version check as --output cdep_tree in
# kconfig.cmake so the launcher is never enabled without the .cdep tree.
if(CONFIGDEP_ENABLE)
idf_build_get_property(python PYTHON)
__check_python_package_min_version(
${python} esp-idf-kconfig "${CONFIGDEP_MIN_KCONFIG_VERSION}" _kconfig_version_ok)
if(_kconfig_version_ok)
find_program(CONFIGDEP_FOUND esp-idf-configdep)
if(CONFIGDEP_FOUND)
idf_msg("esp-idf-configdep will be used for faster recompilation")
list(APPEND launcher_chain "esp-idf-configdep")
else()
idf_warn("esp-idf-configdep enabled but not found. "
"Run 'eim fix' to update esp-idf-kconfig.")
endif()
else()
idf_warn("esp-idf-configdep not supported by esp-idf-kconfig "
"(>= ${CONFIGDEP_MIN_KCONFIG_VERSION} required). "
"Run 'eim fix' to update esp-idf-kconfig.")
endif()
endif()
# ccache goes second in the chain.
if(CCACHE_ENABLE)
find_program(CCACHE_FOUND ccache)
if(CCACHE_FOUND)
idf_msg("ccache will be used for faster recompilation")
list(APPEND launcher_chain "ccache")
else()
idf_warn("enabled ccache in build but ccache program not found")
endif()
endif()
if(NOT launcher_chain)
idf_msg("No compiler launcher chain will be used.")
return()
endif()
find_program(CCACHE_FOUND ccache)
if(CCACHE_FOUND)
idf_msg("ccache will be used for faster recompilation")
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache)
else()
idf_warn("enabled ccache in build but ccache program not found")
endif()
# Apply to all languages. Set in the caller's (project top-level) directory
# scope so component targets created later in idf_project_init() inherit it.
set(CMAKE_C_COMPILER_LAUNCHER "${launcher_chain}" PARENT_SCOPE)
set(CMAKE_CXX_COMPILER_LAUNCHER "${launcher_chain}" PARENT_SCOPE)
set(CMAKE_ASM_COMPILER_LAUNCHER "${launcher_chain}" PARENT_SCOPE)
string(REPLACE ";" " -> " launcher_display "${launcher_chain}")
idf_msg("Compiler launcher chain: ${launcher_display}")
endfunction()
#[[
@@ -761,8 +796,8 @@ __init_idf_target()
# Set IDF_TOOLCHAIN, IDF_TOOLCHAIN_FILE and CMAKE_TOOLCHAIN_FILE.
__init_toolchain()
# Enable ccache if requested.
__init_ccache()
# Set up the compiler launcher chain (esp-idf-configdep, ccache) if requested.
__init_compiler_launchers()
#[[

View File

@@ -18,6 +18,9 @@ set(ESP_MENUCONFIG_MIN_KCONFIG_VERSION "3.1.0")
# (fused menuconfig + deprecated-options post-processing in a single invocation).
set(MENUCONFIG_INLINE_MIN_KCONFIG_VERSION "3.9.0")
# Minimum esp-idf-kconfig version required for cdep_tree / configdep (--output cdep_tree)
set(CONFIGDEP_MIN_KCONFIG_VERSION "3.6.0")
#[[
__init_kconfig()
@@ -707,6 +710,17 @@ function(__generate_kconfig_outputs)
list(APPEND kconfgen_outputs_cmd --output config "${sdkconfig}")
endif()
# If configdep is enabled and esp-idf-kconfig supports cdep_tree, emit one
# .cdep stub per config option into config_dir (next to sdkconfig.h) so that
# esp-idf-configdep can rewrite compiler depfiles for selective rebuilds.
# Requires esp-idf-kconfig >= CONFIGDEP_MIN_KCONFIG_VERSION.
idf_build_get_property(python PYTHON)
__check_python_package_min_version(
${python} esp-idf-kconfig "${CONFIGDEP_MIN_KCONFIG_VERSION}" CONFIGDEP_SUPPORTED)
if(CONFIGDEP_SUPPORTED AND CONFIGDEP_ENABLE)
list(APPEND kconfgen_outputs_cmd --output cdep_tree "${config_dir}")
endif()
idf_build_set_property(__KCONFGEN_OUTPUTS_CMD "${kconfgen_outputs_cmd}")
# Generate Kconfig outputs using kconfgen

View File

@@ -13,15 +13,49 @@ from test_build_system_helpers import replace_in_file
CONFIGDEP_TEST_APP = 'tools/test_build_system/configdep_test_app'
_MAIN_OBJ_DIR = ('build', 'esp-idf', 'main', 'CMakeFiles', '__idf_main.dir')
MAIN_OBJ = os.path.join(*_MAIN_OBJ_DIR, 'configdep_main.c.obj')
UNRELATED_OBJ = os.path.join(*_MAIN_OBJ_DIR, 'unrelated.c.obj')
CONFIGDEP_ELF = Path(os.path.join('build', 'configdep_test_app.elf'))
def _main_obj_dir(request: pytest.FixtureRequest) -> str:
# cmakev2 names the main component target ``_idf_main``; cmakev1 uses ``__idf_main``.
target_dir = '_idf_main.dir' if request.config.getoption('buildv2', False) else '__idf_main.dir'
return os.path.join('build', 'esp-idf', 'main', 'CMakeFiles', target_dir)
@pytest.fixture
def main_obj(request: pytest.FixtureRequest) -> str:
return os.path.join(_main_obj_dir(request), 'configdep_main.c.obj')
@pytest.fixture
def unrelated_obj(request: pytest.FixtureRequest) -> str:
return os.path.join(_main_obj_dir(request), 'unrelated.c.obj')
@pytest.fixture(autouse=True)
def _select_build_system(test_app_copy: Path, request: pytest.FixtureRequest) -> None:
"""Build the shared configdep app with the requested build system.
The app sources are build-system agnostic; only the top-level
``CMakeLists.txt`` differs. When running with ``--buildv2``, switch it to the
cmakev2 entry point so the same test exercises configdep on both build systems.
"""
if not request.config.getoption('buildv2', False):
return
replace_in_file(
'CMakeLists.txt',
'include($ENV{IDF_PATH}/tools/cmake/project.cmake)',
'include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake)',
)
replace_in_file(
'CMakeLists.txt',
'project(configdep_test_app)',
'project(configdep_test_app C CXX ASM)\nidf_project_default()',
)
@pytest.mark.test_app_copy(CONFIGDEP_TEST_APP)
@pytest.mark.usefixtures('test_app_copy')
def test_configdep_selective_rebuild(idf_py: IdfPyFunc) -> None:
def test_configdep_selective_rebuild(idf_py: IdfPyFunc, main_obj: str, unrelated_obj: str) -> None:
"""Verify selective rebuild when a Kconfig option toggles.
``esp-idf-configdep`` scans each translation unit's source for ``CONFIG_*``
@@ -35,38 +69,36 @@ def test_configdep_selective_rebuild(idf_py: IdfPyFunc) -> None:
logging.info('toggle CONFIG_TEST_CONFIGDEP_OPTION from y to n')
replace_in_file('sdkconfig', 'CONFIG_TEST_CONFIGDEP_OPTION=y', '# CONFIG_TEST_CONFIGDEP_OPTION is not set')
snapshot_main = get_snapshot(MAIN_OBJ)
snapshot_unrelated = get_snapshot(UNRELATED_OBJ)
snapshot_main = get_snapshot(main_obj)
snapshot_unrelated = get_snapshot(unrelated_obj)
logging.info('rebuild after config change')
idf_py('build')
logging.info('configdep_main.c.obj must be rebuilt (references changed option)')
snapshot_main.assert_different(get_snapshot(MAIN_OBJ))
snapshot_main.assert_different(get_snapshot(main_obj))
logging.info('unrelated.c.obj must not be rebuilt (no CONFIG_* token for that option in source)')
snapshot_unrelated.assert_same(get_snapshot(UNRELATED_OBJ))
snapshot_unrelated.assert_same(get_snapshot(unrelated_obj))
@pytest.mark.test_app_copy(CONFIGDEP_TEST_APP)
@pytest.mark.usefixtures('test_app_copy')
def test_configdep_no_rebuild_without_change(idf_py: IdfPyFunc) -> None:
def test_configdep_no_rebuild_without_change(idf_py: IdfPyFunc, main_obj: str, unrelated_obj: str) -> None:
logging.info('initial build with configdep enabled (default)')
idf_py('build')
snapshot_main = get_snapshot(MAIN_OBJ)
snapshot_unrelated = get_snapshot(UNRELATED_OBJ)
snapshot_main = get_snapshot(main_obj)
snapshot_unrelated = get_snapshot(unrelated_obj)
logging.info('rebuild with no config changes')
idf_py('build')
logging.info('neither object file should be rebuilt')
snapshot_main.assert_same(get_snapshot(MAIN_OBJ))
snapshot_unrelated.assert_same(get_snapshot(UNRELATED_OBJ))
snapshot_main.assert_same(get_snapshot(main_obj))
snapshot_unrelated.assert_same(get_snapshot(unrelated_obj))
@pytest.mark.test_app_copy(CONFIGDEP_TEST_APP)
@pytest.mark.usefixtures('test_app_copy')
def test_configdep_elf_strings_track_sdkconfig_toggle(idf_py: IdfPyFunc) -> None:
"""After rebuild, the firmware string matches the final Kconfig (host-side ELF check)."""
idf_py('build')