Merge branch 'fix/cmakev2_manager_seed_known_components' into 'master'

fix(cmakev2/manager): seed known_components for namespace rewrite

See merge request espressif/esp-idf!48453
This commit is contained in:
Frantisek Hrbata
2026-05-19 11:38:19 +02:00
2 changed files with 82 additions and 8 deletions

View File

@@ -292,23 +292,46 @@ function(__inject_requirements_for_component_from_manager component_name)
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.
# 1. An input file which seeds the component manager with one entry per
# discovered component, each carrying its __COMPONENT_SOURCE. This is a
# minimal build system v1-style file consumed by the manager via the
# shim __component_set_property(), which calls idf_component_set_property().
# Seeding the whole project ensures handle_project_requirements()'s
# known_components matches the project-wide list v1 provides, so its
# _choose_component() can rewrite a manifest-declared namespaced dep
# (e.g. "lvgl__lvgl") to its locally-shadowing short name ("lvgl") when
# a local component shadows a managed dependency. Without seeding, the
# manager would only see the one component being processed and the
# rewrite would never fire. The manager then modifies this file by
# appending the component's resolved 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.
# Iterate COMPONENT_INTERFACES (not COMPONENTS_DISCOVERED) and read each
# component's properties via __idf_component_get_property_unchecked so the
# COMPONENT_SOURCE comes from the component's own interface target rather
# than the alias-aware lookup. After __init_component_interface_cache
# short-name aliasing fires (e.g. a higher-priority "example__cmp" rebinds
# the cache entry "cmp" to its own interface), the name-based getter
# returns the alias target's source -- in that case both "cmp" and
# "example__cmp" would be seeded as "project_managed_components" and the
# manager's _override_requirements_by_component_sources would reject the
# duplicate.
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")
idf_build_get_property(component_interfaces COMPONENT_INTERFACES)
set(requires_content "")
foreach(seed_interface IN LISTS component_interfaces)
__idf_component_get_property_unchecked(seed_name "${seed_interface}" COMPONENT_NAME)
__idf_component_get_property_unchecked(seed_source "${seed_interface}" COMPONENT_SOURCE)
string(APPEND requires_content
"__component_set_property(___${component_prefix}_${seed_name} __COMPONENT_SOURCE \"${seed_source}\")\n")
endforeach()
file(WRITE "${out_file}" "${requires_content}")
# 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")

View File

@@ -1,5 +1,6 @@
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import json
import logging
from pathlib import Path
@@ -297,3 +298,53 @@ def test_idf_component_set_get_property_apis(idf_py: IdfPyFunc) -> None:
assert 'LIB=' in result and len(result.split('LIB=')[1].split('\n')[0]) > 0, (
'idf_component_get_property should retrieve COMPONENT_LIB'
)
@pytest.mark.usefixtures('test_app_copy')
def test_local_component_shadows_managed_dep_in_manifest(idf_py: IdfPyFunc) -> None:
"""A component's manifest declares `<ns>/<name>` while the project has a
local `components/<name>` that shadows it. The dep must resolve to the
local short-named component rather than failing with
`Failed to resolve component '<ns>__<name>'`.
Regression test for the cmakev2 per-component injection path that lost
the project-wide context _choose_component needs to rewrite a manifest-
declared namespaced dep to its locally-shadowing short name. The fix
seeds the per-component requirements file with one entry per discovered
component so known_components matches what cmakev1's project-wide
injection produces.
"""
logging.info('Testing local component shadows manifest-declared managed dep')
# Local shadow named `lvgl`.
(Path('components/lvgl')).mkdir(parents=True)
(Path('components/lvgl/CMakeLists.txt')).write_text('idf_component_register(SRCS "lvgl_stub.c" INCLUDE_DIRS ".")\n')
(Path('components/lvgl/lvgl_stub.c')).write_text('void lvgl_local_stub(void) {}\n')
# A second local component whose manifest declares the namespaced dep.
# override_path keeps the component manager offline by pointing the dep
# at the local lvgl directory.
(Path('components/consumer')).mkdir(parents=True)
(Path('components/consumer/CMakeLists.txt')).write_text(
'idf_component_register(SRCS "consumer.c" INCLUDE_DIRS ".")\n'
)
(Path('components/consumer/consumer.c')).write_text('void consumer_stub(void) {}\n')
(Path('components/consumer/idf_component.yml')).write_text(
'dependencies:\n idf: ">=5.0"\n lvgl/lvgl:\n version: "*"\n override_path: "../lvgl"\n'
)
# Force consumer into the build via main (cmakev2 only builds required components).
replace_in_file(
'main/CMakeLists.txt',
'# placeholder_inside_idf_component_register',
'PRIV_REQUIRES consumer',
)
# Without the fix this fails with "Failed to resolve component 'lvgl__lvgl'".
idf_py('reconfigure')
with open('build/project_description.json') as f:
data = json.load(f)
paths = data.get('build_component_paths', [])
assert any(p.endswith('/components/lvgl') for p in paths), f'local lvgl not in build_component_paths: {paths}'
assert not any('lvgl__lvgl' in p for p in paths), f'managed lvgl__lvgl should not be in build: {paths}'