diff --git a/tools/cmakev2/manager.cmake b/tools/cmakev2/manager.cmake index 0cb5264a5eb..7ff6e9c4816 100644 --- a/tools/cmakev2/manager.cmake +++ b/tools/cmakev2/manager.cmake @@ -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") diff --git a/tools/test_build_system/buildv2/test_component.py b/tools/test_build_system/buildv2/test_component.py index 89a6cbc3d1a..2c7bd1e3840 100644 --- a/tools/test_build_system/buildv2/test_component.py +++ b/tools/test_build_system/buildv2/test_component.py @@ -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 `/` while the project has a + local `components/` that shadows it. The dep must resolve to the + local short-named component rather than failing with + `Failed to resolve component '__'`. + + 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}'