From 8b213b517db2f9f566f9eab3a4ffa5fba1f6eb5c Mon Sep 17 00:00:00 2001 From: Jan Beran Date: Tue, 28 Jul 2026 13:56:16 +0200 Subject: [PATCH] feat: Adopt rebuild optimization tool (configdep) in cmakev2 --- tools/cmakev2/idf.cmake | 71 +++++++++++++++++------ tools/cmakev2/kconfig.cmake | 14 +++++ tools/test_build_system/test_configdep.py | 64 +++++++++++++++----- 3 files changed, 115 insertions(+), 34 deletions(-) diff --git a/tools/cmakev2/idf.cmake b/tools/cmakev2/idf.cmake index f4fc09ec99b..d3578b67f5f 100644 --- a/tools/cmakev2/idf.cmake +++ b/tools/cmakev2/idf.cmake @@ -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() #[[ diff --git a/tools/cmakev2/kconfig.cmake b/tools/cmakev2/kconfig.cmake index 1beaddc4f30..d5a5682308a 100644 --- a/tools/cmakev2/kconfig.cmake +++ b/tools/cmakev2/kconfig.cmake @@ -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 diff --git a/tools/test_build_system/test_configdep.py b/tools/test_build_system/test_configdep.py index 85a4c119936..c7aebaef93e 100644 --- a/tools/test_build_system/test_configdep.py +++ b/tools/test_build_system/test_configdep.py @@ -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')