diff --git a/tools/cmake/build.cmake b/tools/cmake/build.cmake index b9a3ebd967..6f77a72278 100644 --- a/tools/cmake/build.cmake +++ b/tools/cmake/build.cmake @@ -579,11 +579,15 @@ macro(idf_build_process target) # Call for component manager to download dependencies for all components idf_build_get_property(idf_component_manager IDF_COMPONENT_MANAGER) + + set(result 0) if(idf_component_manager EQUAL 1) idf_build_get_property(build_dir BUILD_DIR) set(managed_components_list_file ${build_dir}/managed_components_list.temp.cmake) set(local_components_list_file ${build_dir}/local_components_list.temp.yml) + set(__RERUN_EXITCODE 10) # missing kconfig + set(__contents "components:\n") idf_build_get_property(build_component_targets BUILD_COMPONENT_TARGETS) foreach(__build_component_target ${build_component_targets}) @@ -610,6 +614,7 @@ macro(idf_build_process target) "idf_component_manager.prepare_components" "--project_dir=${project_dir}" "--lock_path=${dependencies_lock_file}" + "--sdkconfig_json_file=${build_dir}/config/sdkconfig.json" "--interface_version=${component_manager_interface_version}" "prepare_dependencies" "--local_components_list_file=${local_components_list_file}" @@ -618,7 +623,11 @@ macro(idf_build_process target) ERROR_VARIABLE error) if(NOT result EQUAL 0) - message(FATAL_ERROR "${error}") + if(result EQUAL ${__RERUN_EXITCODE}) + message(WARNING "${error}") + else() + message(FATAL_ERROR "${error}") + endif() endif() include(${managed_components_list_file}) @@ -689,21 +698,30 @@ macro(idf_build_process target) # Generate config values in different formats idf_build_get_property(sdkconfig SDKCONFIG) idf_build_get_property(sdkconfig_defaults SDKCONFIG_DEFAULTS) - __kconfig_generate_config("${sdkconfig}" "${sdkconfig_defaults}") + + # add target here since we have all components + if(result EQUAL 0) + __kconfig_generate_config("${sdkconfig}" "${sdkconfig_defaults}" CREATE_MENUCONFIG_TARGET) + else() + __kconfig_generate_config("${sdkconfig}" "${sdkconfig_defaults}") + endif() + __build_import_configs() - # All targets built under this scope is with the ESP-IDF build system - set(ESP_PLATFORM 1) - idf_build_set_property(COMPILE_DEFINITIONS "ESP_PLATFORM" APPEND) + if(result EQUAL 0) + # All targets built under this scope is with the ESP-IDF build system + set(ESP_PLATFORM 1) + idf_build_set_property(COMPILE_DEFINITIONS "ESP_PLATFORM" APPEND) - # Perform component processing (inclusion of project_include.cmake, adding component - # subdirectories, creating library targets, linking libraries, etc.) - __build_process_project_includes() + # Perform component processing (inclusion of project_include.cmake, adding component + # subdirectories, creating library targets, linking libraries, etc.) + __build_process_project_includes() - idf_build_get_property(idf_path IDF_PATH) - add_subdirectory(${idf_path} ${build_dir}/esp-idf) + idf_build_get_property(idf_path IDF_PATH) + add_subdirectory(${idf_path} ${build_dir}/esp-idf) - unset(ESP_PLATFORM) + unset(ESP_PLATFORM) + endif() endmacro() # idf_build_executable diff --git a/tools/cmake/kconfig.cmake b/tools/cmake/kconfig.cmake index 6197b5ca2a..2018eb0e8f 100644 --- a/tools/cmake/kconfig.cmake +++ b/tools/cmake/kconfig.cmake @@ -104,12 +104,14 @@ function(__get_init_config_version config version_out) endforeach() endfunction() - # # Generate the config files and create config related targets and configure # dependencies. # function(__kconfig_generate_config sdkconfig sdkconfig_defaults) + set(options OPTIONAL CREATE_MENUCONFIG_TARGET) + cmake_parse_arguments(PARSE_ARGV 2 "ARG" "${options}" "" "") + # List all Kconfig and Kconfig.projbuild in known components idf_build_get_property(component_targets __COMPONENT_TARGETS) idf_build_get_property(build_component_targets __BUILD_COMPONENT_TARGETS) @@ -268,6 +270,10 @@ function(__kconfig_generate_config sdkconfig sdkconfig_defaults) set(MENUCONFIG_CMD ${python} -m menuconfig) set(TERM_CHECK_CMD ${python} ${idf_path}/tools/check_term.py) + if(NOT ${ARG_CREATE_MENUCONFIG_TARGET}) + return() + endif() + # Generate the menuconfig target add_custom_target(menuconfig ${menuconfig_depends} diff --git a/tools/cmake/project.cmake b/tools/cmake/project.cmake index 19219c81f8..72504c061c 100644 --- a/tools/cmake/project.cmake +++ b/tools/cmake/project.cmake @@ -61,7 +61,7 @@ if(NOT "$ENV{IDF_COMPONENT_MANAGER}" EQUAL "0") idf_build_set_property(IDF_COMPONENT_MANAGER 1) endif() # Set component manager interface version -idf_build_set_property(__COMPONENT_MANAGER_INTERFACE_VERSION 3) +idf_build_set_property(__COMPONENT_MANAGER_INTERFACE_VERSION 4) # # Parse and store the VERSION argument provided to the project() command. @@ -733,14 +733,31 @@ macro(project project_name) message(STATUS "Building ESP-IDF components for target ${IDF_TARGET}") - idf_build_process(${IDF_TARGET} - SDKCONFIG_DEFAULTS "${sdkconfig_defaults}" - SDKCONFIG ${sdkconfig} - BUILD_DIR ${build_dir} - PROJECT_NAME ${CMAKE_PROJECT_NAME} - PROJECT_DIR ${CMAKE_CURRENT_LIST_DIR} - PROJECT_VER "${project_ver}" - COMPONENTS "${components};${test_components}") + set(result 0) + set(retried 0) + + while(true) + idf_build_process(${IDF_TARGET} + SDKCONFIG_DEFAULTS "${sdkconfig_defaults}" + SDKCONFIG ${sdkconfig} + BUILD_DIR ${build_dir} + PROJECT_NAME ${CMAKE_PROJECT_NAME} + PROJECT_DIR ${CMAKE_CURRENT_LIST_DIR} + PROJECT_VER "${project_ver}" + COMPONENTS "${components};${test_components}" + ) + + if(result EQUAL 0) + break() + elseif(result EQUAL 10 AND retried EQUAL 0) + message(WARNING "Missing kconfig option. Re-run the build process...") + set(retried 1) + elseif(result EQUAL 10 AND retried EQUAL 1) + message(FATAL_ERROR "Missing required kconfig option after retry.") + else() + message(FATAL_ERROR "idf_build_process failed with exit code ${result}") + endif() + endwhile() # Special treatment for 'main' component for standard projects (not part of core build system). # Have it depend on every other component in the build. This is diff --git a/tools/test_build_system/test_component_manager.py b/tools/test_build_system/test_component_manager.py index ace7f85524..e0b0d10ff1 100644 --- a/tools/test_build_system/test_component_manager.py +++ b/tools/test_build_system/test_component_manager.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD +# SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 import os.path from pathlib import Path @@ -44,3 +44,115 @@ def test_trimmed_components_still_passed_to_cmake(idf_py: IdfPyFunc, test_app_co fs = f.read() assert ' example/cmp:' in fs + + +class TestOptionalDependencyWithKconfig: + def test_direct_kconfig(self, idf_py: IdfPyFunc, test_app_copy: Path) -> None: + (test_app_copy / 'main' / 'idf_component.yml').write_text(""" + dependencies: + example/cmp: + version: "*" + rules: + - if: $CONFIG{BOOTLOADER_PROJECT_VER} == 1 + """) + + idf_py('reconfigure') + + with open('dependencies.lock', 'r') as f: + fs = f.read() + assert ' example/cmp:' in fs + + (test_app_copy / 'main' / 'idf_component.yml').write_text(""" + dependencies: + example/cmp: + version: "*" + rules: + - if: $CONFIG{BOOTLOADER_PROJECT_VER} != 1 + """) + + idf_py('reconfigure') + + with open('dependencies.lock', 'r') as f: + fs = f.read() + assert ' example/cmp:' not in fs + + def test_missing_kconfig_first_round(self, idf_py: IdfPyFunc, test_app_copy: Path) -> None: + (test_app_copy / 'main' / 'idf_component.yml').write_text(""" + dependencies: + espressif/mdns: + version: "*" + + example/cmp: + version: "*" + rules: + - if: $CONFIG{MDNS_MAX_SERVICES} == 10 # mdns kconfig CONFIG_MDNS_MAX_SERVICES default to 10 + """) + + idf_py('reconfigure') + + with open('dependencies.lock', 'r') as f: + fs = f.read() + assert ' example/cmp:' in fs + + def test_kconfig_load_existing_sdkconfig_defaults(self, idf_py: IdfPyFunc, test_app_copy: Path) -> None: + (test_app_copy / 'main' / 'idf_component.yml').write_text(""" + dependencies: + example/cmp: + version: "*" + rules: + - if: $CONFIG{BOOTLOADER_LOG_LEVEL_WARN} == True + """) + + (test_app_copy / 'sdkconfig.defaults').write_text(""" + CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y + CONFIG_LOG_DEFAULT_LEVEL_WARN=y + """) + + idf_py('reconfigure') + + with open('dependencies.lock', 'r') as f: + fs = f.read() + assert ' example/cmp:' in fs + + def test_kconfig_load_missing_sdkconfig_defaults(self, idf_py: IdfPyFunc, test_app_copy: Path) -> None: + (test_app_copy / 'main' / 'idf_component.yml').write_text(""" + dependencies: + espressif/mdns: + version: "*" + + example/cmp: + version: "*" + rules: + - if: $CONFIG{MDNS_MAX_SERVICES} == 30 # mdns kconfig CONFIG_MDNS_MAX_SERVICES default to 10 + """) + + (test_app_copy / 'sdkconfig.defaults').write_text(""" + CONFIG_MDNS_MAX_SERVICES=30 + """) + + idf_py('reconfigure') + + with open('dependencies.lock', 'r') as f: + fs = f.read() + assert ' example/cmp:' in fs + + def test_missing_kconfig_second_round(self, idf_py: IdfPyFunc, test_app_copy: Path) -> None: + (test_app_copy / 'main' / 'idf_component.yml').write_text(""" + dependencies: + espressif/mdns: + version: "*" + + example/cmp: + version: "*" + rules: + - if: $CONFIG{OF_COURSE_NO_ONE_USE_FOO} == "bar" + """) + + res = idf_py('reconfigure', check=False) + + assert res.returncode != 0 + assert ( + f'OF_COURSE_NO_ONE_USE_FOO, introduced by example/cmp, ' + f'defined in {str(test_app_copy / "main" / "idf_component.yml")}' in res.stderr + ) + assert 'Missing required kconfig option after retry.' in res.stderr