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

This commit introduces a new build property, __OPTIONAL_REQUIRES_MODE,
and uses it to either defer or link immediately, optional requirements
to components that request such linkage via the
idf_component_optional_requires() function in build system v2. The
DEFERRED mode is intended for single-binary projects where in the linking
of optional components happens after the library target is created the
dependency graph is available to the build system, thereby allowing it to
behave like the v1 version of the function.

Made-with: Cursor
This commit is contained in:
Sudeep Mohanty
2026-02-25 10:37:06 +01:00
parent 7b09685987
commit 45dc2e5868
4 changed files with 206 additions and 32 deletions
+22 -1
View File
@@ -439,7 +439,28 @@ The Behavior of ``idf_component_optional_requires`` has Changed
In v1, the ``idf_component_optional_requires`` function adds a dependency on a specified component only if that component is already included in the build (for instance, if it is already required by another component). To achieve this, v1 examines the ``BUILD_COMPONENTS`` build property, which is generated during the early evaluation phase and lists all components involved in the build.
In v2, the ``BUILD_COMPONENTS`` build property is no longer available because the early evaluation phase has been removed. Consequently, the function now adds the dependency if the component is recognized by the build system. This behavior may pull unnecessary components into the build, leading to longer build times. Using ``idf_component_optional_requires`` in v2 should be avoided, instead, optional dependencies should be added using conditional logic based on the project configuration.
In v2, there is no early collection phase and ``BUILD_COMPONENTS`` does not exist. The build system discovers components as it evaluates dependencies. So v2 cannot use the same "only if already in the build" check; it has to choose a different rule.
The build system supports two behaviors, controlled by the ``IDF_COMPONENT_OPTIONAL_REQUIRES_MODE`` build property:
* **IMMEDIATE (default)** — When a component calls ``idf_component_optional_requires(type req_component)``, the build system includes ``req_component`` and links it to the caller if it is recognized (discovered). No check is made whether the rest of the project actually needs that component. This is safe for multi-binary projects (multiple executables or binaries), but it can pull in more components than necessary and increase build time.
* **DEFERRED** — The build system does not include or link immediately. It records the request and resolves it later in :cmakev2:ref:`idf_build_library`: the optional component is linked only if it ends up in that library's dependency graph. This matches v1 semantics and keeps the number of linked components minimal. It **must not** be used when building more than one library (see below).
A multi-binary project is one that creates more than one executable or binary (for example, several application executables built from the same tree). Such a project calls :cmakev2:ref:`idf_build_library` or :cmakev2:ref:`idf_build_executable` more than once. In v2, component targets are shared globally across all libraries. If ``IDF_COMPONENT_OPTIONAL_REQUIRES_MODE`` is set to **DEFERRED**, the build system resolves optional requirements when it processes each library. When it processes the second or a later library, it may add new links to component targets that are already used by the first library. The first library's metadata (such as the list of linker fragments or linked components) was already computed when that library was processed and is not updated. As a result, linker script generation and section placement for the first library can be incorrect or stale. For this reason, DEFERRED mode is not allowed when more than one library is built; the build fails with an error in that case. **IMMEDIATE** mode does not have this problem, because optional requirements are applied during component evaluation, before any per-library metadata is computed. The side effect of the IMMEDIATE mode is that it can pull in more components than necessary and increase build time.
:cmakev2:ref:`idf_project_default` (the usual entry point for a single-executable project) sets ``IDF_COMPONENT_OPTIONAL_REQUIRES_MODE`` to **DEFERRED** before building the default executable when no libraries have been created yet. So if your project uses ``idf_project_default()`` and builds only one executable, you get DEFERRED behavior automatically and do not need to do anything.
If you do not use :cmakev2:ref:`idf_project_default` and instead call :cmakev2:ref:`idf_project_init` and then the lower-level API (:cmakev2:ref:`idf_build_executable`, :cmakev2:ref:`idf_build_library`) yourself, the default mode is **IMMEDIATE**. If you build only one library/executable and want the same efficient, v1-like behavior as ``idf_project_default``, you must set the mode to DEFERRED yourself after project init:
.. code-block:: cmake
idf_project_init()
idf_build_set_property(IDF_COMPONENT_OPTIONAL_REQUIRES_MODE DEFERRED)
idf_build_executable(my_app COMPONENTS main ...)
# ... rest of your project ...
Do **not** set ``IDF_COMPONENT_OPTIONAL_REQUIRES_MODE`` to ``DEFERRED`` if you build multiple libraries; the build will error. Keep the default IMMEDIATE in that case.
API Reference
=============
+6
View File
@@ -336,6 +336,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