fix(cmakev2/compat): change idf_component_optional_requires behavior

The idf_component_optional_requires compatibility function in cmakev2
currently attempts to mimic the behavior of cmakev1 by adding a
dependency only if a component is already included in the build. In
cmakev1, this is handled by checking the BUILD_COMPONENTS list, which is
created during an early evaluation phase. Because cmakev2 removed this
early phase to allow for configuration-based component dependencies, the
build system does not inherently know which components will be part of
the build beforehand. To compensate, the current compatibility function
relies on the TARGET_EXISTS generator expression to determine if a
component should be linked.

This approach poses a problem because generator expressions are not
evaluated until the generation phase, but cmakev2 processes linker
scripts during the configuration phase by recursively scanning targets
linked to a library. Because the scanner does not recognize and cannot
evaluate generator expressions, any component linked optionally through
generator expression is skipped. If that component carries a linker
script, the script is never added to the library interface, resulting in
build failures. Since cmakev2 aims to support multiple libraries, a
component might also exist globally, causing TARGET_EXISTS to evaluate
to true, yet still be missed during a specific library's recursive scan,
leading to the same omission of necessary linker scripts.

To resolve this, the implementation of idf_component_optional_requires
is changed to check if a component is known to the build system in
COMPONENTS_DISCOVERED build property. If so, it is explicitly included
and linked directly via its interface target. While this may pull more
components into a build than the previous generator expression method,
potentially increasing build times, it ensures that dependency trees are
fully visible to the library target scanner. This serves as a practical
middle ground that maintains compatibility with existing components and
ensures build stability. This approach allows for the gradual fixing of
idf_component_optional_requires usage in components for cmakev2, with
the eventual goal of removing its usage in cmakev2 entirely.

This change also fixes cases where idf_component_optional_requires is
used to conditionally add requirements based on configuration options.
In cmakev2, the presence of a configuration option does not guarantee[1]
that a component has been included via add_subdirectory, unlike the
behavior in cmakev1. With this change even constructions like

```cmake
if(CONFIG_VFS_SUPPORT_IO)
    idf_component_optional_requires(PRIVATE vfs)
endif()
```

will now work in cmakev2, as the updated idf_component_optional_requires
explicitly includes the required component if it is available to the
build system.

Closes https://github.com/espressif/esp-idf/issues/18133

[1] https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/build-system-v2.html#id9

Signed-off-by: Frantisek Hrbata <frantisek.hrbata@espressif.com>
This commit is contained in:
Frantisek Hrbata
2026-02-04 09:28:35 +01:00
parent 0d188359a1
commit e1eae669bc
2 changed files with 38 additions and 8 deletions
+31 -8
View File
@@ -211,21 +211,44 @@ endfunction()
The component name that should be added as a dependency to the
evaluated component. It may be provided multiple times.
Add a dependency on a specific component only if it is included in the
build.
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.
#]]
function(idf_component_optional_requires req_type)
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_get_property(req_alias "${req}" COMPONENT_ALIAS)
# The component alias is created only after the component is included,
# meaning the add_subdirectory command for it has been called. This can
# be used to detect if a component has already been added to the build.
target_link_libraries(${COMPONENT_TARGET} ${req_type} "$<$<TARGET_EXISTS:${req_alias}>:${req_interface}>")
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()
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)
target_link_libraries(${COMPONENT_TARGET} ${type} ${req_interface})
endif()
else()
target_link_libraries(${COMPONENT_TARGET} ${type} ${req_interface})
endif()
endforeach()
endfunction()