From 8cbc4296e7b8ae0b8cbe7d5768cc3bd0ca5a5e86 Mon Sep 17 00:00:00 2001 From: Frantisek Hrbata Date: Fri, 8 Aug 2025 16:24:10 +0200 Subject: [PATCH] feat(cmakev2/component): add idf_component_include function Add a core function of the build system, responsible for including the specified component identified by name, into the build process. Currently, the non-project components written in cmakev1 are ignored and not evaluated with the add_subdirectory command because there are no shims for the cmakev1 API yet. However, this allows for the evaluation of project components written using the cmakev2 approach, which is closer to the native CMake usage. Signed-off-by: Frantisek Hrbata --- tools/cmakev2/component.cmake | 193 ++++++++++++++++++++++++++++++++++ tools/cmakev2/idf.cmake | 14 +++ 2 files changed, 207 insertions(+) diff --git a/tools/cmakev2/component.cmake b/tools/cmakev2/component.cmake index 7eb61646c9f..8c3615224fd 100644 --- a/tools/cmakev2/component.cmake +++ b/tools/cmakev2/component.cmake @@ -418,12 +418,14 @@ function(__init_component) idf_build_set_property(COMPONENT_INTERFACES "${component_interface}" APPEND) idf_component_set_property("${component_name}" COMPONENT_LIB "${component_target}") + idf_component_set_property("${component_name}" COMPONENT_TARGET "${component_target}") idf_component_set_property("${component_name}" COMPONENT_NAME "${component_name}") idf_component_set_property("${component_name}" COMPONENT_DIR "${component_directory}") idf_component_set_property("${component_name}" COMPONENT_ALIAS "${component_alias}") idf_component_set_property("${component_name}" COMPONENT_SOURCE "${component_source}") idf_component_set_property("${component_name}" COMPONENT_INTERFACE "${component_interface}") idf_component_set_property("${component_name}" COMPONENT_PRIORITY ${component_priority}) + idf_component_set_property("${component_name}" COMPONENT_INCLUDED NO) # Set component properties for Kconfig files. idf_component_set_property("${component_name}" __KCONFIG "${component_kconfig}") @@ -448,3 +450,194 @@ function(__dump_component_properties components) endforeach() endforeach() endfunction() + +#[[api +.. cmakev2:function:: idf_component_include + + .. code-block:: cmake + + idf_component_include( + [INTERFACE ]) + + :name[in]: Component name. + :INTERFACE[out,opt]: Optional variable where the name of the target + interface for the included component will be stored. + + This is a core function of the build system, responsible for including the + specified component, as indicated by the ```` option, into the build + process. It does this by evaluating the component through the + ``add_subdirectory`` call for the component's directory. The component is + responsible for creating its own CMake target, with the target name provided + via the ``COMPONENT_TARGET`` variable. This target is linked to the + component's interface target, which is managed internally by the build + system. The component's interface target is used by other components to + establish their dependencies. + + When the "INTERFACE" variable is provided, the name of the included + component interface target will be stored in it. +#]] +function(idf_component_include name) + set(options) + set(one_value INTERFACE) + set(multi_value) + cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) + + # Check that the specified component name is among the discovered and + # recognized components. + idf_build_get_property(components_discovered COMPONENTS_DISCOVERED) + if(NOT "${name}" IN_LIST components_discovered) + idf_die("Component '${name}' not found. Available components: '${components_discovered}'") + endif() + + # Check if the component is already included, meaning the add_subdirectory + # has already been called for it and the component has been processed. + idf_build_get_property(components_included COMPONENTS_INCLUDED) + if("${name}" IN_LIST components_included) + idf_dbg("Component '${name}' is already included.") + # Set the parent variable ARG_INTERFACE to include the target name of + # the component interface. + if(DEFINED ARG_INTERFACE) + idf_component_get_property(component_interface "${name}" COMPONENT_INTERFACE) + set(${ARG_INTERFACE} ${component_interface} PARENT_SCOPE) + endif() + return() + endif() + + idf_dbg("Including component '${name}'") + + idf_component_get_property(component_name "${name}" COMPONENT_NAME) + idf_component_get_property(component_interface "${name}" COMPONENT_INTERFACE) + idf_component_get_property(component_target "${name}" COMPONENT_TARGET) + idf_component_get_property(component_directory "${name}" COMPONENT_DIR) + idf_component_get_property(component_alias "${name}" COMPONENT_ALIAS) + idf_component_get_property(component_lib "${name}" COMPONENT_LIB) + idf_component_get_property(component_version "${name}" COMPONENT_VERSION) + + # The following cmakev1 backward-compatible variables are passed to the + # component's CMakeLists.txt. + set(COMPONENT_NAME ${component_name}) + set(COMPONENT_DIR ${component_directory}) + set(COMPONENT_ALIAS ${component_alias}) + set(COMPONENT_LIB ${component_lib}) + set(COMPONENT_VERSION ${component_version}) + set(ESP_PLATFORM 1) + + # The COMPONENT_TARGET is simply an alias for COMPONENT_LIB, which more + # accurately reflects its purpose in the cmakev2 build system. The + # component's responsibility is to create the target with the name + # specified in the COMPONENT_TARGET variable. + set(COMPONENT_TARGET ${component_target}) + + # To handle potential circular dependencies among components, maintain a + # record of currently evaluated components in the __DEPENDENCY_CHAIN. This + # helps in detecting and reporting circular dependencies, such as + # C1->C2->C1. In this scenario, C2 can still use the C1 interface target, + # but C1 will only be fully evaluated after C2 has been evaluated. + if("${component_name}" IN_LIST __DEPENDENCY_CHAIN) + idf_dbg("Component '${name}' in circular dependency chain '${__DEPENDENCY_CHAIN}'") + return() + endif() + + # FIXME: Once the shims for cmakev1 components are ready, call + # add_subdirectory for all components. Currently, it is only called for + # project components that use cmakev2. Since add_subdirectory is not called + # for cmakev1 components, the COMPONENT_TARGET will not be created, and + # these components will be silently ignored later when the COMPONENT_TARGET + # is checked for existence. + idf_component_get_property(component_source "${component_name}" COMPONENT_SOURCE) + if("${component_source}" STREQUAL "project_components") + # Evaluate the CMakeLists.txt file of the component. + idf_build_get_property(build_dir BUILD_DIR build_dir) + add_subdirectory("${component_directory}" "${build_dir}/esp-idf/${component_name}" EXCLUDE_FROM_ALL) + endif() + + # The component has been evaluated; remove it from the dependency chain. + list(POP_BACK __DEPENDENCY_CHAIN) + + # Mark the component as included and add it to the COMPONENTS_INCLUDED + # build property. Note that COMPONENTS_INCLUDED is read again because it + # might have been updated further down the dependency chain. + idf_component_set_property("${component_name}" COMPONENT_INCLUDED YES) + idf_build_get_property(components_included COMPONENTS_INCLUDED) + list(APPEND components_included "${component_name}") + idf_build_set_property(COMPONENTS_INCLUDED "${components_included}") + + if(DEFINED ARG_INTERFACE) + idf_component_get_property(component_interface "${name}" COMPONENT_INTERFACE) + set(${ARG_INTERFACE} ${component_interface} PARENT_SCOPE) + endif() + + if(NOT TARGET "${component_target}") + # The cmakev1 system allows component to return without creating any + # cmake target. For instance, a component might not be supported for a + # given IDF_TARGET. Ideally, we should fail and abort the build process + # in such cases. However, for compatibility reasons, the absence of + # component's target is silently ignored here. + idf_dbg("Component '${name}' lacks a target") + idf_component_set_property("${component_name}" COMPONENT_REAL_TARGET NOTFOUND) + return() + endif() + + # The component might use the COMPONENT_TARGET name as an alias for another + # target. In this case, although unlikely, identify the real target and + # store it in the COMPONENT_REAL_TARGET component property. + __get_real_target(TARGET "${component_target}" OUTPUT component_real_target) + idf_component_set_property("${component_name}" COMPONENT_REAL_TARGET "${component_real_target}") + + # Set component target type as COMPONENT_REAL_TARGET_TYPE property. + get_target_property(component_real_target_type "${component_real_target}" TYPE) + idf_component_set_property("${component_name}" COMPONENT_REAL_TARGET_TYPE "${component_real_target_type}") + + # Link the target created by the component to the component interface + # target. + if("${component_real_target_type}" STREQUAL "INTERFACE_LIBRARY") + target_link_libraries("${component_interface}" INTERFACE "${component_real_target}") + elseif("${component_real_target_type}" STREQUAL "STATIC_LIBRARY") + idf_component_get_property(whole_archive "${component_name}" WHOLE_ARCHIVE) + if(whole_archive) + idf_build_get_property(linker_type LINKER_TYPE) + if(linker_type STREQUAL "GNU") + target_link_libraries("${component_interface}" INTERFACE + "-Wl,--whole-archive" "${component_real_target}" "-Wl,--no-whole-archive") + elseif(linker_type STREQUAL "Darwin") + target_link_libraries("${component_interface}" INTERFACE + "-Wl,-force_load" "${component_real_target}") + endif() + else() + target_link_libraries("${component_interface}" INTERFACE "${component_real_target}") + endif() + else() + idf_die("Unsupported target type '${component_real_target_type}' in component '${component_name}'") + endif() + + # Components for cmakev1 use the idf_component_register call and are + # managed in cmakev2 through a shim. This shim sets the COMPONENT_FORMAT + # property to CMAKEV1 and applies global compilation options and + # definitions using CMake's directory-scoped function for backward + # compatibility. Therefore, no additional configuration is required. + idf_component_get_property(component_format "${component_name}" COMPONENT_FORMAT) + if("${component_format}" STREQUAL "CMAKEV1") + return() + endif() + + # The component was not handled by the idf_component_register shim, + # consider it as a component for cmakev2. + idf_component_set_property("${component_name}" COMPONENT_FORMAT CMAKEV2) + + # The component's target is an interface library; nothing more needs to be + # set. + if("${component_real_target_type}" STREQUAL "INTERFACE_LIBRARY") + return() + endif() + + idf_build_get_property(include_directories INCLUDE_DIRECTORIES GENERATOR_EXPRESSION) + target_include_directories("${component_real_target}" BEFORE PRIVATE "${include_directories}") + + idf_build_get_property(compile_definitions COMPILE_DEFINITIONS GENERATOR_EXPRESSION) + target_compile_definitions("${component_real_target}" PRIVATE "${compile_definitions}") + + __get_compile_options(OUTPUT compile_options) + target_compile_options("${component_real_target}" BEFORE PRIVATE "${compile_options}") + + # FIXME: We likely still need to link the common component requirements here. +endfunction() diff --git a/tools/cmakev2/idf.cmake b/tools/cmakev2/idf.cmake index e023807edaf..0489055a5b0 100644 --- a/tools/cmakev2/idf.cmake +++ b/tools/cmakev2/idf.cmake @@ -446,11 +446,25 @@ endfunction() have properties attached to them. However, they are not necessarily included in the build through add_subdirectory. + :COMPONENT_INTERFACES: This is a list of component interface targets for the components in ``COMPONENTS_DISCOVERED``. It is used when searching for a component, such as by its name, to set or retrieve the component's properties. + :COMPONENTS_INCLUDED: This is a list of component names that were included + in the build, meaning their CMakeLists.txt files were + processed with an add_subdirectory call. Each + component is evaluated exactly once, and this list + serves as a record of which components have already + been evaluated. Although each component can only be + evaluated once, it can be used in multiple + idf_component_include calls. If a component is + requested to be included a second time, this list is + checked. If the component is already included, the + idf_component_include function simply returns, as + there is nothing further to do except add a new alias + target if requested. #]] add_library(idf_build_properties INTERFACE)