feat(cmakev2): Added support for injecting dependencies for components

This commit is contained in:
Sudeep Mohanty
2025-11-04 07:48:14 +01:00
committed by Frantisek Hrbata
parent 506fa52485
commit 7835d5d38c
2 changed files with 338 additions and 21 deletions
+218 -9
View File
@@ -186,10 +186,15 @@ function(__get_component_interface)
idf_build_get_property(component_prefix PREFIX)
set(component_interface NOTFOUND)
if("${ARG_COMPONENT}" IN_LIST component_names)
# The component name is among the discovered components, and the
# component interface is simply the component name with a prefix.
set(component_interface "${component_prefix}_${ARG_COMPONENT}")
# Try to resolve component name from short name to namespaced name
__resolve_component_name(COMPONENT "${ARG_COMPONENT}"
KNOWN_COMPONENTS "${component_names}"
OUTPUT resolved_component)
if("${resolved_component}" IN_LIST component_names)
# Found a resolved component name, use it
set(component_interface "${component_prefix}_${resolved_component}")
else()
# The component name might be an alias, so retrieve the actual target
# name.
@@ -467,6 +472,153 @@ function(__dump_component_properties components)
endforeach()
endfunction()
#[[
__component_name_without_namespace(COMPONENT <component_name>
OUTPUT <variable>)
:COMPONENT[in]: Component name that may include namespace.
:OUTPUT[out]: Output variable to store the component name without namespace.
Extract the component name without namespace. For example:
- "espressif__led_strip" -> "led_strip"
- "led_strip" -> "led_strip"
This follows the same logic as the component manager's name_without_namespace function.
#]]
function(__component_name_without_namespace)
set(options)
set(one_value COMPONENT OUTPUT)
set(multi_value)
cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN})
if(NOT DEFINED ARG_COMPONENT)
idf_die("COMPONENT option is required")
endif()
if(NOT DEFINED ARG_OUTPUT)
idf_die("OUTPUT option is required")
endif()
# Split by "__" and take the last part (same logic as component manager)
# Use rsplit with maxsplit=1 equivalent: split and take last part
string(FIND "${ARG_COMPONENT}" "__" last_pos REVERSE)
if(last_pos GREATER -1)
math(EXPR start_pos "${last_pos} + 2")
string(SUBSTRING "${ARG_COMPONENT}" ${start_pos} -1 name_without_ns)
else()
set(name_without_ns "${ARG_COMPONENT}")
endif()
set(${ARG_OUTPUT} "${name_without_ns}" PARENT_SCOPE)
endfunction()
#[[
__resolve_component_name(COMPONENT <component_name>
KNOWN_COMPONENTS <known_components_list>
OUTPUT <variable>)
:COMPONENT[in]: Component name to resolve (may be short name or canonical).
:KNOWN_COMPONENTS[in]: List of known/discovered component names.
:OUTPUT[out]: Output variable to store the resolved component name.
Resolve a component name to its canonical form by checking against known components.
This implements the same logic as the component manager's _choose_component function:
1. If the exact name exists in known components, return it
2. If a namespaced version exists (e.g., "espressif__led_strip" for "led_strip"), return that
3. If a non-namespaced version exists (e.g., "led_strip" for "espressif__led_strip"), return that
4. If any known component has the same name without namespace , return that
5. Otherwise, return the original name (will likely cause CMake error)
This function takes component priority into account by preferring components
with higher priority when multiple matches exist. This behavior differs from
the component manager's _choose_component function.
#]]
function(__resolve_component_name)
set(options)
set(one_value COMPONENT OUTPUT)
set(multi_value KNOWN_COMPONENTS)
cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN})
if(NOT DEFINED ARG_COMPONENT)
idf_die("COMPONENT option is required")
endif()
if(NOT DEFINED ARG_OUTPUT)
idf_die("OUTPUT option is required")
endif()
set(component "${ARG_COMPONENT}")
set(known_components "${ARG_KNOWN_COMPONENTS}")
# 1. If exact name exists, return it (highest priority match)
if("${component}" IN_LIST known_components)
set(${ARG_OUTPUT} "${component}" PARENT_SCOPE)
return()
endif()
# 2. Check for namespaced version (e.g., "led_strip" -> "espressif__led_strip")
# Look for components ending with "__${component}"
set(namespaced_suffix "__${component}")
set(best_match "")
set(best_priority -1)
foreach(known_component IN LISTS known_components)
string(LENGTH "${namespaced_suffix}" suffix_len)
string(LENGTH "${known_component}" comp_len)
if(comp_len GREATER_EQUAL suffix_len)
math(EXPR start_pos "${comp_len} - ${suffix_len}")
string(SUBSTRING "${known_component}" ${start_pos} -1 comp_suffix)
if("${comp_suffix}" STREQUAL "${namespaced_suffix}")
# Get component priority to choose the best match
idf_component_get_property(comp_priority "${known_component}" COMPONENT_PRIORITY)
if(comp_priority GREATER best_priority)
set(best_match "${known_component}")
set(best_priority ${comp_priority})
endif()
endif()
endif()
endforeach()
if(NOT "${best_match}" STREQUAL "")
idf_dbg("Component '${component}' resolved to namespaced version '${best_match}'")
set(${ARG_OUTPUT} "${best_match}" PARENT_SCOPE)
return()
endif()
# 3. Check for non-namespaced version (e.g., "espressif__led_strip" -> "led_strip")
__component_name_without_namespace(COMPONENT "${component}" OUTPUT component_without_ns)
if("${component_without_ns}" IN_LIST known_components)
idf_dbg("Component '${component}' resolved to non-namespaced version '${component_without_ns}'")
set(${ARG_OUTPUT} "${component_without_ns}" PARENT_SCOPE)
return()
endif()
# 4. Check if any known component has the same name without namespace (cross-namespace matching)
set(best_match "")
set(best_priority -1)
foreach(known_component IN LISTS known_components)
__component_name_without_namespace(COMPONENT "${known_component}" OUTPUT known_without_ns)
if("${component_without_ns}" STREQUAL "${known_without_ns}")
# Get component priority to choose the best match
idf_component_get_property(comp_priority "${known_component}" COMPONENT_PRIORITY)
if(comp_priority GREATER best_priority)
set(best_match "${known_component}")
set(best_priority ${comp_priority})
endif()
endif()
endforeach()
if(NOT "${best_match}" STREQUAL "")
idf_dbg("Component '${component}'
resolved via cross-namespace matching to '${best_match}' (priority ${best_priority})")
set(${ARG_OUTPUT} "${best_match}" PARENT_SCOPE)
return()
endif()
# 5. No match found, return original name (will likely cause CMake error)
set(${ARG_OUTPUT} "${component}" PARENT_SCOPE)
endfunction()
#[[api
.. cmakev2:function:: idf_component_include
@@ -498,13 +650,21 @@ function(idf_component_include name)
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.
# Resolve the component name to its namespaced form if needed
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}'")
__resolve_component_name(COMPONENT "${name}"
KNOWN_COMPONENTS "${components_discovered}"
OUTPUT resolved_name)
# Check that the resolved component name is among the discovered components
if(NOT "${resolved_name}" IN_LIST components_discovered)
idf_die("Component '${name}' (resolved to '${resolved_name}') not found.
Available components: '${components_discovered}'")
endif()
# Use the resolved name for the rest of the function
set(name "${resolved_name}")
# 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)
@@ -635,12 +795,61 @@ function(idf_component_include name)
target_add_binary_data(${COMPONENT_TARGET} "${file}" "TEXT")
endforeach()
# Inject managed dependencies if component manager is enabled
idf_build_get_property(idf_component_manager IDF_COMPONENT_MANAGER)
idf_component_get_property(component_format "${component_name}" COMPONENT_FORMAT)
if(idf_component_manager EQUAL 1)
idf_component_get_property(component_dir "${component_name}" COMPONENT_DIR)
# Check if component has manifest for managed dependency injection
if(EXISTS "${component_dir}/idf_component.yml")
__inject_requirements_for_component_from_manager("${component_name}")
# Include any managed dependencies
idf_component_get_property(managed_requires "${component_name}" MANAGED_REQUIRES)
idf_component_get_property(managed_priv_requires "${component_name}" MANAGED_PRIV_REQUIRES)
foreach(dep IN LISTS managed_requires managed_priv_requires)
if(dep)
idf_component_include("${dep}")
endif()
endforeach()
# For cmakev1 components, automatically link managed dependencies to maintain
# backward compatibility.
if("${component_format}" STREQUAL "CMAKEV1")
idf_component_get_property(component_type "${component_name}" COMPONENT_TYPE)
# Link managed public requirements
foreach(req IN LISTS managed_requires)
if(req)
idf_component_get_property(req_interface "${req}" COMPONENT_INTERFACE)
if(${component_type} STREQUAL LIBRARY)
target_link_libraries("${component_target}" PUBLIC "${req_interface}")
else()
target_link_libraries("${component_target}" INTERFACE "${req_interface}")
endif()
endif()
endforeach()
# Link managed private requirements
foreach(req IN LISTS managed_priv_requires)
if(req)
idf_component_get_property(req_interface "${req}" COMPONENT_INTERFACE)
if(${component_type} STREQUAL CONFIG_ONLY)
continue()
endif()
target_link_libraries("${component_target}" PRIVATE "${req_interface}")
endif()
endforeach()
endif()
endif()
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()
+120 -12
View File
@@ -82,9 +82,6 @@ function(__fetch_components_from_registry)
idf_die("IDF Component Manager error: ${cmgr_result}")
endif()
endwhile()
# Auto-include all project-level managed components after component manager is done
__include_project_level_managed_components()
endfunction()
#[[
@@ -178,15 +175,8 @@ function(__download_component_level_managed_components)
set(__contents "components:\n")
idf_build_get_property(component_names COMPONENTS_DISCOVERED)
foreach(name ${component_names})
# Respect EXCLUDE_COMPONENTS passed via -D
set(__include 1)
if(DEFINED EXCLUDE_COMPONENTS AND name IN_LIST EXCLUDE_COMPONENTS)
set(__include 0)
endif()
if(__include)
idf_component_get_property(dir ${name} COMPONENT_DIR)
set(__contents "${__contents} - name: \"${name}\"\n path: \"${dir}\"\n")
endif()
idf_component_get_property(dir ${name} COMPONENT_DIR)
set(__contents "${__contents} - name: \"${name}\"\n path: \"${dir}\"\n")
endforeach()
file(WRITE ${local_components_list_file} "${__contents}")
@@ -232,3 +222,121 @@ function(__component_manager_warn_if_disabled_and_manifests_exist)
${with_lines}\nHowever, the component manager is not enabled.")
endif()
endfunction()
#[[
__component_set_property(target property value)
Shim for setting component properties, primarily for use by the component manager
in build system v2. This function only processes dependency-related properties
(MANAGED_REQUIRES and MANAGED_PRIV_REQUIRES) produced by the component manager's
injection file. Other properties are ignored to avoid interfering with the
cmakev2 build flow. Target names with triple underscores are normalized.
#]]
function(__component_set_property target property value)
# If the target has 3 underscores, remove all of them and normalize the target
# This shim is only intended to process dependency-related properties produced
# by the component manager injection file. Ignore unrelated properties to avoid
# clobbering configuration already set by the cmakev2 build flow.
string(REPLACE "___" "" target "${target}")
# We only consume MANAGED_REQUIRES and MANAGED_PRIV_REQUIRES from the component manager.
# The manager's REQUIRES/PRIV_REQUIRES output contains both original and resolved names,
# which we don't want. We'll handle name resolution locally using our utility functions.
if(property STREQUAL "MANAGED_REQUIRES")
# Set the managed property for tracking
idf_component_set_property("${target}" "${property}" "${value}")
# Also append to the regular REQUIRES property
idf_component_set_property("${target}" REQUIRES "${value}" APPEND)
elseif(property STREQUAL "MANAGED_PRIV_REQUIRES")
# Set the managed property for tracking
idf_component_set_property("${target}" "${property}" "${value}")
# Also append to the regular PRIV_REQUIRES property
idf_component_set_property("${target}" PRIV_REQUIRES "${value}" APPEND)
else()
# Ignore REQUIRES, PRIV_REQUIRES, and other properties like INCLUDE_DIRS,
# __COMPONENT_SOURCE, __COMPONENT_REGISTERED, etc.
endif()
endfunction()
#[[
__inject_requirements_for_component_from_manager(<component_name>)
Managed dependency injection for a single component in build system v2.
Calls the Component Manager to compute manifest-derived dependencies and
updates the component's MANAGED_* properties.
#]]
function(__inject_requirements_for_component_from_manager component_name)
# Skip if already injected
idf_component_get_property(already_injected "${component_name}" __MANAGED_INJECTED)
if(already_injected)
return()
endif()
idf_dbg("Injecting requirements for component '${component_name}' from the component manager")
idf_build_get_property(python PYTHON)
idf_build_get_property(project_dir PROJECT_DIR)
idf_build_get_property(build_dir BUILD_DIR)
idf_build_get_property(dependencies_lock_file DEPENDENCIES_LOCK)
idf_build_get_property(sdkconfig_json __SDKCONFIG_JSON)
idf_build_get_property(component_manager_interface_version IDF_COMPONENT_MANAGER_INTERFACE_VERSION)
idf_build_get_property(idf_path IDF_PATH)
idf_build_get_property(component_prefix PREFIX)
idf_component_get_property(component_source "${component_name}" COMPONENT_SOURCE)
idf_component_get_property(component_dir "${component_name}" COMPONENT_DIR)
# The component manager will inject requirements for this component. To do this, it needs to files:
#
# 1. An input file which states the component's source type. This is a minimal build system v1-style file
# which contains the component's source type. To make the component manager happy, we create a file with
# shim __component_set_property(), which calls idf_component_set_property(). The component manager will
# modify this file by adding the component's requirements. TODO: Improve this.
# 2. A file which lists the components with manifests. This file is created by the component manager,
# and is deleted after the component manager is done. This works for build system v1 where we provide
# a global list of components with manifests. However, for build system v2, we need to provide this file
# for each component. Hence, we create this file and place it in the build directory.
set(out_file "${build_dir}/component_requires.${component_name}.temp.cmake")
set(cmgr_target "___${component_prefix}_${component_name}")
# We only provide component source to the component manager
file(WRITE "${out_file}" "__component_set_property(${cmgr_target} __COMPONENT_SOURCE \"${component_source}\")\n")
# Create components_with_manifests_list.temp file with only this component if it has a manifest
set(components_with_manifests_file "${build_dir}/components_with_manifests_list.temp")
if(EXISTS "${component_dir}/idf_component.yml")
file(WRITE "${components_with_manifests_file}" "${component_dir}\n")
else()
file(WRITE "${components_with_manifests_file}" "")
endif()
# Call component manager to inject requirements
execute_process(COMMAND ${python}
"-m"
"idf_component_manager.prepare_components"
"--project_dir=${project_dir}"
"--lock_path=${dependencies_lock_file}"
"--sdkconfig_json_file=${sdkconfig_json}"
"--interface_version=${component_manager_interface_version}"
"inject_requirements"
"--idf_path=${idf_path}"
"--build_dir=${build_dir}"
"--component_requires_file=${out_file}"
RESULT_VARIABLE result
ERROR_VARIABLE error)
if(NOT result EQUAL 0)
idf_die("Component manager requirements injection failed for '${component_name}': ${error}")
endif()
# Include the component manager's output
if(EXISTS "${out_file}")
include("${out_file}")
endif()
# Clean up temporary files
if(NOT DEFINED ENV{IDF_KEEP_CMANAGER_TEMP} OR NOT "$ENV{IDF_KEEP_CMANAGER_TEMP}" STREQUAL "1")
file(REMOVE "${out_file}")
file(REMOVE "${components_with_manifests_file}")
endif()
idf_component_set_property("${component_name}" __MANAGED_INJECTED YES)
endfunction()