From dba7d1109668b66f87ee81243fc709384bf5cdcf Mon Sep 17 00:00:00 2001 From: Frantisek Hrbata Date: Mon, 2 Oct 2023 15:31:06 +0200 Subject: [PATCH 1/2] feat(tools): export information about all components in __COMPONENT_TARGETS Add new "all_component_info" dictionary into the project_description.json file. It contains information about all registered components presented in the __COMPONENT_TARGETS list. Since components in this list are not fully evaluated, because only the first stage of cmakefiles processing is done, it does not contain the same information as the "build_component_info" dictionary. The "type", "file" and "sources" variables are missing. Most of the properties are already attached to the component target, so this only adds INCLUDE_DIRS property to the target during the first cmakefiles processing stage. The "all_component_info" dict is generated in a separate function, even though the original function for "build_component_info" could be adjusted. This introduces a little bit of boilerplate, but keeps it logically separated and probably easier if we want to extend it in the future. Signed-off-by: Frantisek Hrbata --- tools/cmake/project.cmake | 61 ++++++++++++++++++- tools/cmake/project_description.json.in | 3 +- .../scripts/component_get_requirements.cmake | 6 +- 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/tools/cmake/project.cmake b/tools/cmake/project.cmake index 290b468f48..253dc5665d 100644 --- a/tools/cmake/project.cmake +++ b/tools/cmake/project.cmake @@ -119,7 +119,7 @@ function(paths_with_spaces_to_list variable_name) endif() endfunction() -function(__component_info components output) +function(__build_component_info components output) set(components_json "") foreach(name ${components}) __component_get_target(target ${name}) @@ -187,6 +187,62 @@ function(__component_info components output) set(${output} "${components_json}" PARENT_SCOPE) endfunction() +function(__all_component_info output) + set(components_json "") + + idf_build_get_property(build_prefix __PREFIX) + idf_build_get_property(component_targets __COMPONENT_TARGETS) + + foreach(target ${component_targets}) + __component_get_property(name ${target} COMPONENT_NAME) + __component_get_property(alias ${target} COMPONENT_ALIAS) + __component_get_property(prefix ${target} __PREFIX) + __component_get_property(dir ${target} COMPONENT_DIR) + __component_get_property(type ${target} COMPONENT_TYPE) + __component_get_property(lib ${target} COMPONENT_LIB) + __component_get_property(reqs ${target} REQUIRES) + __component_get_property(include_dirs ${target} INCLUDE_DIRS) + __component_get_property(priv_reqs ${target} PRIV_REQUIRES) + __component_get_property(managed_reqs ${target} MANAGED_REQUIRES) + __component_get_property(managed_priv_reqs ${target} MANAGED_PRIV_REQUIRES) + + if(prefix STREQUAL build_prefix) + set(name ${name}) + else() + set(name ${alias}) + endif() + + make_json_list("${reqs}" reqs) + make_json_list("${priv_reqs}" priv_reqs) + make_json_list("${managed_reqs}" managed_reqs) + make_json_list("${managed_priv_reqs}" managed_priv_reqs) + make_json_list("${include_dirs}" include_dirs) + + string(JOIN "\n" component_json + " \"${name}\": {" + " \"alias\": \"${alias}\"," + " \"target\": \"${target}\"," + " \"prefix\": \"${prefix}\"," + " \"dir\": \"${dir}\"," + " \"lib\": \"${lib}\"," + " \"reqs\": ${reqs}," + " \"priv_reqs\": ${priv_reqs}," + " \"managed_reqs\": ${managed_reqs}," + " \"managed_priv_reqs\": ${managed_priv_reqs}," + " \"include_dirs\": ${include_dirs}" + " }" + ) + string(CONFIGURE "${component_json}" component_json) + if(NOT "${components_json}" STREQUAL "") + string(APPEND components_json ",\n") + endif() + string(APPEND components_json "${component_json}") + endforeach() + string(PREPEND components_json "{\n") + string(APPEND components_json "\n }") + set(${output} "${components_json}" PARENT_SCOPE) +endfunction() + # # Output the built components to the user. Generates files for invoking esp_idf_monitor # that doubles as an overview of some of the more important build properties. @@ -251,7 +307,8 @@ function(__project_info test_components) make_json_list("${build_component_paths};${test_component_paths}" build_component_paths_json) make_json_list("${common_component_reqs}" common_component_reqs_json) - __component_info("${build_components};${test_components}" build_component_info_json) + __build_component_info("${build_components};${test_components}" build_component_info_json) + __all_component_info(all_component_info_json) # The configure_file function doesn't process generator expressions, which are needed # e.g. to get component target library(TARGET_LINKER_FILE), so the project_description diff --git a/tools/cmake/project_description.json.in b/tools/cmake/project_description.json.in index feee0dd845..1805db5339 100644 --- a/tools/cmake/project_description.json.in +++ b/tools/cmake/project_description.json.in @@ -1,5 +1,5 @@ { - "version": "1", + "version": "1.1", "project_name": "${PROJECT_NAME}", "project_version": "${PROJECT_VER}", "project_path": "${PROJECT_PATH}", @@ -28,5 +28,6 @@ "build_components" : ${build_components_json}, "build_component_paths" : ${build_component_paths_json}, "build_component_info" : ${build_component_info_json}, + "all_component_info" : ${all_component_info_json}, "debug_prefix_map_gdbinit": "${debug_prefix_map_gdbinit}" } diff --git a/tools/cmake/scripts/component_get_requirements.cmake b/tools/cmake/scripts/component_get_requirements.cmake index 21d2da8614..89531881a4 100644 --- a/tools/cmake/scripts/component_get_requirements.cmake +++ b/tools/cmake/scripts/component_get_requirements.cmake @@ -76,6 +76,7 @@ macro(idf_component_register) set(__component_requires "${__REQUIRES}") set(__component_kconfig "${__KCONFIG}") set(__component_kconfig_projbuild "${__KCONFIG_PROJBUILD}") + set(__component_include_dirs "${__INCLUDE_DIRS}") set(__component_registered 1) return() endmacro() @@ -107,11 +108,13 @@ function(__component_get_requirements) spaces2list(__component_requires) spaces2list(__component_priv_requires) + spaces2list(__component_include_dirs) set(__component_requires "${__component_requires}" PARENT_SCOPE) set(__component_priv_requires "${__component_priv_requires}" PARENT_SCOPE) set(__component_kconfig "${__component_kconfig}" PARENT_SCOPE) set(__component_kconfig_projbuild "${__component_kconfig_projbuild}" PARENT_SCOPE) + set(__component_include_dirs "${__component_include_dirs}" PARENT_SCOPE) set(__component_registered ${__component_registered} PARENT_SCOPE) endfunction() @@ -141,7 +144,8 @@ foreach(__component_target ${__component_targets}) set(__contents "__component_set_property(${__component_target} REQUIRES \"${__component_requires}\") __component_set_property(${__component_target} PRIV_REQUIRES \"${__component_priv_requires}\") -__component_set_property(${__component_target} __COMPONENT_REGISTERED ${__component_registered})" +__component_set_property(${__component_target} __COMPONENT_REGISTERED ${__component_registered}) +__component_set_property(${__component_target} INCLUDE_DIRS \"${__component_include_dirs}\")" ) if(__component_kconfig) From 64d82b54bc58e811e8c2ce6c69a2ddcfab5b1563 Mon Sep 17 00:00:00 2001 From: Frantisek Hrbata Date: Mon, 2 Oct 2023 16:24:41 +0200 Subject: [PATCH 2/2] feat(hints): use all_component_info from project_description.json Currently the component_requirements hint module does not work as expected if the component list for a project is trimmed down. With the new "all_component_info" dictionary info in project_description.json, the module can produce hints even if cmake's COMPONENTS variable is set. Signed-off-by: Frantisek Hrbata --- .../hint_modules/component_requirements.py | 8 ++--- tools/test_idf_py/test_hints.py | 29 +++++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/tools/idf_py_actions/hint_modules/component_requirements.py b/tools/idf_py_actions/hint_modules/component_requirements.py index ab555b9545..04d1675a99 100644 --- a/tools/idf_py_actions/hint_modules/component_requirements.py +++ b/tools/idf_py_actions/hint_modules/component_requirements.py @@ -58,7 +58,7 @@ def generate_hint(output: str) -> Optional[str]: # find the source_component that contains the source file found_source_component_name = None found_source_component_info = None - for component_name, component_info in proj_desc['build_component_info'].items(): + for component_name, component_info in proj_desc['all_component_info'].items(): # look if the source_filename is within a component directory, not only # at component_info['sources'], because the missing file may be included # from header file, which is not present in component_info['sources'] @@ -86,7 +86,7 @@ def generate_hint(output: str) -> Optional[str]: original_file_match = ORIGINAL_FILE_RE.match(lines[-2]) if original_file_match: original_filename = _get_absolute_path(original_file_match.group(1), proj_desc['build_dir']) - for component_name, component_info in proj_desc['build_component_info'].items(): + for component_name, component_info in proj_desc['all_component_info'].items(): component_dir = os.path.normpath(component_info['dir']) if original_filename.startswith(component_dir): found_original_component_name = component_name @@ -100,7 +100,7 @@ def generate_hint(output: str) -> Optional[str]: # look for the header file in the public include directories of all components found_dep_component_names = [] - for candidate_component_name, candidate_component_info in proj_desc['build_component_info'].items(): + for candidate_component_name, candidate_component_info in proj_desc['all_component_info'].items(): if candidate_component_name == found_source_component_name: # skip the component that contains the source file continue @@ -117,7 +117,7 @@ def generate_hint(output: str) -> Optional[str]: # directories if we can find the missing header there and notify user about possible # missing entry in INCLUDE_DIRS. candidate_component_include_dirs = [] - for component_name, component_info in proj_desc['build_component_info'].items(): + for component_name, component_info in proj_desc['all_component_info'].items(): component_dir = os.path.normpath(component_info['dir']) for root, _, _ in os.walk(component_dir): full_path = os.path.normpath(os.path.join(root, missing_header)) diff --git a/tools/test_idf_py/test_hints.py b/tools/test_idf_py/test_hints.py index 18c6092430..faa0b82a3e 100755 --- a/tools/test_idf_py/test_hints.py +++ b/tools/test_idf_py/test_hints.py @@ -171,5 +171,34 @@ class TestNestedModuleComponentRequirements(unittest.TestCase): self.tmpdir.cleanup() +class TestTrimmedModuleComponentRequirements(unittest.TestCase): + def setUp(self) -> None: + # Set up a dummy project with a trimmed down list of components and main component. + # The main component includes "esp_http_client.h", but the esp_http_client + # component is not added to main's requirements. + self.tmpdir = tempfile.TemporaryDirectory() + self.tmpdirpath = Path(self.tmpdir.name) + + self.projectdir = self.tmpdirpath / 'project' + self.projectdir.mkdir(parents=True) + (self.projectdir / 'CMakeLists.txt').write_text(( + 'cmake_minimum_required(VERSION 3.16)\n' + 'set(COMPONENTS main)\n' + 'include($ENV{IDF_PATH}/tools/cmake/project.cmake)\n' + 'project(foo)')) + + maindir = self.projectdir / 'main' + maindir.mkdir() + (maindir / 'CMakeLists.txt').write_text('idf_component_register(SRCS "foo.c")') + (maindir / 'foo.c').write_text('#include "esp_http_client.h"\nvoid app_main(){}') + + def test_trimmed_component_requirements(self) -> None: + output = run_idf(['app'], self.projectdir) + self.assertIn('To fix this, add esp_http_client to PRIV_REQUIRES list of idf_component_register call in', output) + + def tearDown(self) -> None: + self.tmpdir.cleanup() + + if __name__ == '__main__': unittest.main()