diff --git a/tools/cmakev2/build.cmake b/tools/cmakev2/build.cmake index f7f0121ef6..f1e64c1b1e 100644 --- a/tools/cmakev2/build.cmake +++ b/tools/cmakev2/build.cmake @@ -6,6 +6,7 @@ include_guard(GLOBAL) include(utilities) include(CheckCCompilerFlag) include(CheckCXXCompilerFlag) +include(component_validation) #[[api .. cmakev2:function:: idf_build_set_property @@ -501,6 +502,9 @@ function(idf_build_library library) set_property(TARGET "${library}" APPEND PROPERTY INTERFACE_LINK_DEPENDS "${script}") endforeach() endforeach() + + # Validate components linked to this library + __component_validation_run_checks(LIBRARY "${library}") endfunction() #[[ diff --git a/tools/cmakev2/component.cmake b/tools/cmakev2/component.cmake index 0b169753b2..e6bab67a6d 100644 --- a/tools/cmakev2/component.cmake +++ b/tools/cmakev2/component.cmake @@ -709,7 +709,7 @@ function(__set_component_cmakev1_properties component_name) get_target_property(component_sources "${component_real_target}" SOURCES) if(component_sources) __get_absolute_paths(PATHS "${component_sources}" BASE_DIR "${component_dir}" OUTPUT sources) - idf_component_set_property("${component_name}" SOURCES "${sources}") + idf_component_set_property("${component_name}" SRCS "${sources}") idf_component_set_property("${component_name}" COMPONENT_TYPE LIBRARY) else() idf_component_set_property("${component_name}" COMPONENT_TYPE CONFIG_ONLY) diff --git a/tools/cmakev2/component_validation.cmake b/tools/cmakev2/component_validation.cmake new file mode 100644 index 0000000000..403400fa64 --- /dev/null +++ b/tools/cmakev2/component_validation.cmake @@ -0,0 +1,173 @@ +# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +# +# Component validation checks +# +# This module contains checks that validate component source files and include directories +# to ensure they belong to the correct component. These checks run after all components +# have been discovered and processed. + +#[[ + __component_validation_get_component_for_path( ) + + *var[out]* + + Output variable to store the component name. + + *path[in]* + + File or directory path to check. + + Determine which component a given path belongs to. Returns the component name in ``var``. +#]] +function(__component_validation_get_component_for_path var path) + # Determine the starting directory to check: use the path itself if it's a directory, + # otherwise use its containing directory + set(current_dir "${path}") + if(NOT IS_DIRECTORY "${current_dir}") + get_filename_component(current_dir "${path}" DIRECTORY) + endif() + + # Get all component names + idf_build_get_property(component_names COMPONENTS_DISCOVERED) + + # Walk up the directory tree from the deepest path towards root and return + # the first component whose COMPONENT_DIR matches exactly. This guarantees + # selecting the deepest matching component without extra heuristics. + while(NOT "${current_dir}" STREQUAL "" AND + NOT "${current_dir}" STREQUAL "/" AND + NOT "${current_dir}" MATCHES "^[A-Za-z]:/$") + foreach(component_name ${component_names}) + idf_component_get_property(component_dir ${component_name} COMPONENT_DIR) + if(current_dir STREQUAL component_dir) + set(${var} ${component_name} PARENT_SCOPE) + return() + endif() + endforeach() + get_filename_component(current_dir "${current_dir}" DIRECTORY) + endwhile() + + # If no component found, return empty + set(${var} "" PARENT_SCOPE) +endfunction() + +#[[ + __component_validation_check_sources() + + *component_name[in]* + + Component name to validate. + + Validate that all source files of the specified component belong to that + component's directory tree. Issues warnings if source files belong to other + components. +#]] +function(__component_validation_check_sources component_name) + idf_component_get_property(sources ${component_name} SRCS) + + # Skip validation if component does not have any sources + if(NOT sources) + return() + endif() + + foreach(src ${sources}) + # Check if this source file belongs to another component + __component_validation_get_component_for_path(owner_component ${src}) + + if(owner_component AND NOT owner_component STREQUAL component_name) + idf_warn( + "Source file '${src}' belongs to component ${owner_component} but is being built by " + "component ${component_name}. It is recommended to build source files by " + "defining component dependencies for ${component_name} " + "via using idf_component_register(REQUIRES ${owner_component}) " + "or idf_component_register(PRIV_REQUIRES ${owner_component}) in the CMakeLists.txt of " + "${component_name}.") + endif() + endforeach() +endfunction() + +#[[ + __component_validation_check_include_dirs() + + *component_name[in]* + + Component name to validate. + + Validate that all include directories (INCLUDE_DIRS and PRIV_INCLUDE_DIRS + properties) of the specified component belong to that component's directory + tree. Issues warnings if include directories belong to other components. +#]] +function(__component_validation_check_include_dirs component_name) + idf_component_get_property(include_dirs ${component_name} INCLUDE_DIRS) + idf_component_get_property(priv_include_dirs ${component_name} PRIV_INCLUDE_DIRS) + idf_component_get_property(component_dir ${component_name} COMPONENT_DIR) + + # Check public include directories + foreach(dir ${include_dirs}) + # Check if this include directory belongs to another component + # Normalize to absolute path relative to this component directory + get_filename_component(abs_dir ${dir} ABSOLUTE BASE_DIR ${component_dir}) + __component_validation_get_component_for_path(owner_component ${abs_dir}) + + if(owner_component AND NOT owner_component STREQUAL component_name) + idf_warn( + "Include directory '${abs_dir}' belongs to component ${owner_component} but is being " + "used by component ${component_name}. It is recommended to define the " + "component dependency for '${component_name}' on the component ${owner_component}, " + "i.e. 'idf_component_register(... REQUIRES ${owner_component})' in the " + "CMakeLists.txt of ${component_name}, and specify the included directory " + "as idf_component_register(... INCLUDE_DIRS ) " + "in the CMakeLists.txt of component ${owner_component}.") + endif() + endforeach() + + # Check private include directories + foreach(dir ${priv_include_dirs}) + # Check if this include directory belongs to another component + # Normalize to absolute path relative to this component directory + get_filename_component(abs_dir ${dir} ABSOLUTE BASE_DIR ${component_dir}) + __component_validation_get_component_for_path(owner_component ${abs_dir}) + + if(owner_component AND NOT owner_component STREQUAL component_name) + idf_warn( + "Private include directory '${abs_dir}' belongs to component ${owner_component} but " + "is being used by component ${component_name}. " + "It is recommended to define the component dependency for ${component_name} " + "on the component ${owner_component}, " + "i.e. 'idf_component_register(... PRIV_REQUIRES ${owner_component})' in the " + "CMakeLists.txt of ${component_name}, " + "and specify the included directory as " + "idf_component_register(... PRIV_INCLUDE_DIRS ) " + "in the CMakeLists.txt of component ${owner_component}.") + endif() + endforeach() +endfunction() + +#[[ + __component_validation_run_checks(LIBRARY ) + + *library[in]* + + Library name for which to run validation checks. + + Run validation checks for all components linked to the specified library. +#]] +function(__component_validation_run_checks) + set(options) + set(one_value LIBRARY) + set(multi_value) + cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) + + if(NOT DEFINED ARG_LIBRARY) + idf_die("LIBRARY option is required for __component_validation_run_checks") + endif() + + # Validate only components linked to the specified library + idf_library_get_property(component_names "${ARG_LIBRARY}" LIBRARY_COMPONENTS_LINKED) + + # Run validation checks for each component + foreach(component_name ${component_names}) + __component_validation_check_sources(${component_name}) + __component_validation_check_include_dirs(${component_name}) + endforeach() +endfunction() diff --git a/tools/test_build_system/test_components.py b/tools/test_build_system/test_components.py index a0258ec6fd..9828bec316 100644 --- a/tools/test_build_system/test_components.py +++ b/tools/test_build_system/test_components.py @@ -336,7 +336,6 @@ def test_unknown_component_error(idf_py: IdfPyFunc, test_app_copy: Path) -> None assert "Failed to resolve component 'unknown'" in ret.stderr -@pytest.mark.buildv2_skip('not yet implemented in cmakev2') def test_component_with_improper_dependency(idf_py: IdfPyFunc, test_app_copy: Path) -> None: # test for __component_validation_check_include_dirs and __component_validation_check_sources # Checks that the following warnings are produced: @@ -377,7 +376,6 @@ def test_component_with_improper_dependency(idf_py: IdfPyFunc, test_app_copy: Pa assert re_source.search(ret.stderr) is not None, f'Expected source file warning not found in: {ret.stderr}' -@pytest.mark.buildv2_skip('not yet implemented in cmakev2') def test_component_validation_not_run_in_subprojects(idf_py: IdfPyFunc, test_app_copy: Path) -> None: # test that component validation doesn't run in subprojects like bootloader logging.info('Check that component validation warnings are not shown in subprojects') @@ -419,7 +417,6 @@ def test_component_validation_not_run_in_subprojects(idf_py: IdfPyFunc, test_app assert ret.returncode == 0, 'Build should complete successfully with validation warnings' -@pytest.mark.buildv2_skip('not yet implemented in cmakev2') def test_component_validation_private_include_dirs(idf_py: IdfPyFunc, test_app_copy: Path) -> None: # test that component validation works for private include directories logging.info('Check that component validation warnings are shown for private include directories') @@ -450,7 +447,6 @@ def test_component_validation_private_include_dirs(idf_py: IdfPyFunc, test_app_c ) -@pytest.mark.buildv2_skip('not yet implemented in cmakev2') def test_component_validation_finds_right_component(idf_py: IdfPyFunc, test_app_copy: Path) -> None: # test that __component_validation_get_component_for_path finds the correct component for a given path #