Merge branch 'feat/buildv2_comp_opt_requires' into 'master'

fix(cmakev2): Defer idf_component_optional_requires linking to library build time

See merge request espressif/esp-idf!46059
This commit is contained in:
Sudeep Mohanty
2026-03-09 12:12:12 +01:00
4 changed files with 206 additions and 32 deletions
+6
View File
@@ -362,6 +362,12 @@ function(idf_build_library library)
target_link_libraries("${library}" INTERFACE "${component_interface}")
endforeach()
# Process optional requirements in DEFERRED mode only (no-op in IMMEDIATE or when unset).
idf_build_get_property(opt_req_mode IDF_COMPONENT_OPTIONAL_REQUIRES_MODE)
if("${opt_req_mode}" STREQUAL "DEFERRED")
__idf_component_process_optional_requires()
endif()
# Get all targets transitively linked to the library interface target.
__get_target_dependencies(TARGET "${library}" OUTPUT dependencies)
+173 -31
View File
@@ -196,6 +196,106 @@ function(target_linker_script target deptype scriptfiles)
endfunction()
#[[
__idf_component_process_optional_requires()
Called by idf_build_library() before LIBRARY_COMPONENTS_LINKED is computed.
For each pending (caller, type, req) entry recorded by
idf_component_optional_requires, links req's interface target to the
caller's real target if both are present in this library.
#]]
function(__idf_component_process_optional_requires)
# Nothing to do if no component has called idf_component_optional_requires.
idf_build_get_property(callers __DEFERRED_OPTIONAL_CALLERS)
if(NOT callers)
return()
endif()
# DEFERRED mode + multiple libraries is disallowed
idf_build_get_property(libraries_list LIBRARY_INTERFACES)
list(LENGTH libraries_list lib_count)
if(lib_count GREATER 1)
idf_die("DEFERRED optional requires mode cannot be used when building "
"multiple libraries (detected ${lib_count} libraries). "
"Set IDF_COMPONENT_OPTIONAL_REQUIRES_MODE to IMMEDIATE for multi-library projects.")
endif()
# Fetch the components included in the project (single library only here,
# so this equals the library's component set).
idf_build_get_property(components_included COMPONENTS_INCLUDED)
set(library_component_interfaces "")
foreach(comp_name IN LISTS components_included)
__get_component_interface(COMPONENT "${comp_name}" OUTPUT dep_interface)
if(dep_interface AND NOT "${dep_interface}" IN_LIST library_component_interfaces)
list(APPEND library_component_interfaces "${dep_interface}")
endif()
endforeach()
# For every caller that recorded optional requirements, check whether it is
# part of this library and, if so, apply any pending pairs whose
# requirement is also in the library.
foreach(caller_target IN LISTS callers)
# Resolve the caller target to its component interface.
__get_component_interface(COMPONENT "${caller_target}" OUTPUT caller_interface)
if(NOT caller_interface)
continue()
endif()
# Skip if this caller is not linked into the current library.
if(NOT "${caller_interface}" IN_LIST library_component_interfaces)
continue()
endif()
# Fetch this caller's pending pairs and the pairs already processed in
# earlier idf_build_library() calls.
idf_build_get_property(pairs "__OPT_REQ_${caller_target}")
idf_build_get_property(done "__OPT_REQ_DONE_${caller_target}")
foreach(pair IN LISTS pairs)
# Skip pairs already processed for a previous library. The
# target_link_libraries call and property updates are permanent
# global mutations; repeating them would be redundant.
if("${pair}" IN_LIST done)
continue()
endif()
# Decode the "type::::req_interface" entry.
string(REPLACE "::::" ";" split "${pair}")
list(GET split 0 link_type)
list(GET split 1 req_interface)
# Skip if the optional requirement is not part of this library.
if(NOT "${req_interface}" IN_LIST library_component_interfaces)
continue()
endif()
# Link the caller's real target to the requirement's interface target.
target_link_libraries("${caller_target}" "${link_type}" "${req_interface}")
# Update the caller's REQUIRES or PRIV_REQUIRES property.
idf_component_get_property(req_name "${req_interface}" COMPONENT_NAME)
if("${link_type}" STREQUAL "PRIVATE")
idf_component_get_property(existing "${caller_interface}" PRIV_REQUIRES)
if(NOT "${req_name}" IN_LIST existing)
idf_component_set_property("${caller_interface}" PRIV_REQUIRES
"${req_name}" APPEND)
endif()
elseif("${link_type}" STREQUAL "PUBLIC" OR "${link_type}" STREQUAL "INTERFACE")
idf_component_get_property(existing "${caller_interface}" REQUIRES)
if(NOT "${req_name}" IN_LIST existing)
idf_component_set_property("${caller_interface}" REQUIRES
"${req_name}" APPEND)
endif()
endif()
# Mark this pair as done so it is not processed again for subsequent
# libraries.
idf_build_set_property("__OPT_REQ_DONE_${caller_target}" "${pair}" APPEND)
endforeach()
endforeach()
endfunction()
#[[api
.. cmakev2:function:: idf_component_optional_requires
.. code-block:: cmake
@@ -212,44 +312,86 @@ endfunction()
evaluated component. It may be provided multiple times.
Add a dependency on a specific component only if the component is
recognized by the build system. The component is included if needed and
added as a requirement for the currently evaluated component using
target_link_libraries. This function should be avoided in cmakev2, where
dependencies should be added based on configuration options. This is purely
for backward compatibility with cmakev1.
recognized by the build system. The behavior is controlled by the
``IDF_COMPONENT_OPTIONAL_REQUIRES_MODE`` build property:
* **IMMEDIATE** (default): Include the component and link it to the
caller if it is discovered. Safe for multi-library projects but may
pull in more components than strictly needed.
* **DEFERRED**: Do not include or link immediately; record the request
and resolve it in :cmakev2:ref:`idf_build_library` so that the
component is linked only when it is part of the library's dependency
graph. Matches v1 semantics and reduces unnecessary components, but
must not be used when building multiple libraries (see docs).
.. note::
This function should be avoided in cmakev2, where
dependencies should be added based on configuration options. This is purely
for backward compatibility with cmakev1.
.. warning::
In multi-library projects with **DEFERRED** mode, optional requires
resolved when processing a later library apply globally to shared
component targets. Earlier libraries then link that optional component
too, but their per-library metadata (e.g. linker fragments) was
already computed and is not updated. DEFERRED mode is therefore
disallowed when more than one library is built.
#]]
function(idf_component_optional_requires type)
set(optional_reqs ${ARGN})
foreach(req ${optional_reqs})
__get_component_interface(COMPONENT "${req}" OUTPUT req_interface)
if("${req_interface}" STREQUAL "NOTFOUND")
# The component is not recognized by the build system.
continue()
endif()
idf_component_include("${req}")
# Map the requested type into PRIV_REQUIRES and REQUIRES, allowing the
# requirement to be added to the appropriate component dependency
# property.
if("${type}" STREQUAL "PRIVATE")
set(req_type PRIV_REQUIRES)
elseif("${type}" STREQUAL "PUBLIC")
set(req_type REQUIRES)
else()
set(req_type "")
endif()
idf_build_get_property(mode IDF_COMPONENT_OPTIONAL_REQUIRES_MODE)
if(NOT mode)
set(mode IMMEDIATE)
endif()
if(req_type)
idf_component_get_property(req_name "${req}" COMPONENT_NAME)
idf_component_get_property(target_reqs ${COMPONENT_TARGET} ${req_type})
if(NOT "${req_name}" IN_LIST target_reqs)
idf_component_set_property(${COMPONENT_TARGET} ${req_type} "${req_name}" APPEND)
if("${mode}" STREQUAL "DEFERRED")
# DEFERRED mode: record for resolution in idf_build_library.
# The component is linked only if it is part of the library's dependency graph.
foreach(req ${optional_reqs})
__get_component_interface(COMPONENT "${req}" OUTPUT req_interface)
if("${req_interface}" STREQUAL "NOTFOUND")
continue()
endif()
idf_build_get_property(callers __DEFERRED_OPTIONAL_CALLERS)
if(NOT "${COMPONENT_TARGET}" IN_LIST callers)
idf_build_set_property(__DEFERRED_OPTIONAL_CALLERS "${COMPONENT_TARGET}" APPEND)
endif()
# Store the optional requirement in the __OPT_REQ_<component_target> property.
idf_build_set_property("__OPT_REQ_${COMPONENT_TARGET}"
"${type}::::${req_interface}" APPEND)
endforeach()
else()
# IMMEDIATE mode: include and link discovered components unconditionally.
foreach(req ${optional_reqs})
__get_component_interface(COMPONENT "${req}" OUTPUT req_interface)
if("${req_interface}" STREQUAL "NOTFOUND")
continue()
endif()
idf_component_include("${req}")
if("${type}" STREQUAL "PRIVATE")
set(req_type PRIV_REQUIRES)
elseif("${type}" STREQUAL "PUBLIC")
set(req_type REQUIRES)
else()
set(req_type "")
endif()
if(req_type)
idf_component_get_property(req_name "${req_interface}" COMPONENT_NAME)
idf_component_get_property(target_reqs "${COMPONENT_NAME}" ${req_type})
if(NOT "${req_name}" IN_LIST target_reqs)
idf_component_set_property("${COMPONENT_NAME}" ${req_type} "${req_name}" APPEND)
target_link_libraries(${COMPONENT_TARGET} ${type} ${req_interface})
endif()
else()
target_link_libraries(${COMPONENT_TARGET} ${type} ${req_interface})
endif()
else()
target_link_libraries(${COMPONENT_TARGET} ${type} ${req_interface})
endif()
endforeach()
endforeach()
endif()
endfunction()
#[[
+5
View File
@@ -799,6 +799,11 @@ endfunction()
#]]
macro(idf_project_default)
idf_project_init()
# Use DEFERRED optional-requires resolution only when this will be the sole
# library being built.
idf_build_set_property(IDF_COMPONENT_OPTIONAL_REQUIRES_MODE DEFERRED)
# Only the idf_project_init macro needs be called within the global scope,
# as it includes the project_include.cmake files and the cmake version of
# the configuration. The remaining functionality of the idf_project_default