From 7341f0564bc2752e45253689af5d142593f0d3ec Mon Sep 17 00:00:00 2001 From: Roland Dobai Date: Sun, 22 Mar 2026 06:22:30 +0100 Subject: [PATCH] feat(cmake): Create a merged hints database in the build directory --- tools/cmake/project.cmake | 20 +++++++++++ tools/cmake/project_description.json.in | 3 +- tools/cmakev2/build.cmake | 38 +++++++++++++++++++-- tools/cmakev2/project.cmake | 9 +++-- tools/test_build_system/test_common.py | 44 +++++++++++++++++++++++++ 5 files changed, 108 insertions(+), 6 deletions(-) diff --git a/tools/cmake/project.cmake b/tools/cmake/project.cmake index 052596fbd6..84f406ed38 100644 --- a/tools/cmake/project.cmake +++ b/tools/cmake/project.cmake @@ -382,6 +382,7 @@ function(__project_info test_components) # file with cmake's variables substituted and unprocessed generator expressions. The second # step, with file(GENERATE), processes the temporary file and substitute generator expression # into the final project_description.json file. + set(HINTS_FILE "${build_dir}/hints.yml") configure_file("${idf_path}/tools/cmake/project_description.json.in" "${build_dir}/project_description.json.templ") file(READ "${build_dir}/project_description.json.templ" project_description_json_templ) @@ -392,6 +393,25 @@ function(__project_info test_components) # Generate component dependency graph depgraph_generate("${build_dir}/component_deps.dot") + # Assumption: all hints.yml files are bare YAML lists (no "---" document + # separators). Plain string concatenation is safe under this assumption. + # Note for consumers: yaml.safe_load() only parses the first YAML document, + # so document separators in source files would cause data loss. + set(_merged_hints "") + set(_global_hints_file "${idf_path}/tools/idf_py_actions/hints.yml") + if(EXISTS "${_global_hints_file}") + file(READ "${_global_hints_file}" _hints_content) + string(APPEND _merged_hints "${_hints_content}\n") + endif() + foreach(_comp_dir ${build_component_paths} ${test_component_paths}) + set(_hints_file "${_comp_dir}/hints.yml") + if(EXISTS "${_hints_file}") + file(READ "${_hints_file}" _hints_content) + string(APPEND _merged_hints "${_hints_content}\n") + endif() + endforeach() + file(WRITE "${build_dir}/hints.yml" "${_merged_hints}") + # We now have the following component-related variables: # # build_components is the list of components to include in the build. diff --git a/tools/cmake/project_description.json.in b/tools/cmake/project_description.json.in index 3106e9c8e7..43cfa4fb0f 100644 --- a/tools/cmake/project_description.json.in +++ b/tools/cmake/project_description.json.in @@ -35,5 +35,6 @@ "03_py_extensions": "${gdbinit_files_py_extensions}", "04_connect": "${gdbinit_files_connect}" }, - "debug_arguments_openocd": "${debug_arguments_openocd}" + "debug_arguments_openocd": "${debug_arguments_openocd}", + "hints_file": "${HINTS_FILE}" } diff --git a/tools/cmakev2/build.cmake b/tools/cmakev2/build.cmake index bbdf8c532c..5c1f3253ba 100644 --- a/tools/cmakev2/build.cmake +++ b/tools/cmakev2/build.cmake @@ -754,7 +754,8 @@ endfunction() idf_build_generate_metadata([BINARY ] [EXECUTABLE ] - [OUTPUT_FILE ]) + [OUTPUT_FILE ] + [HINTS_OUTPUT_FILE ]) *BINARY[in,opt]* @@ -769,6 +770,13 @@ endfunction() Optional output file path for storing the metadata. If not provided, the default path ``/project_description.json`` is used. + *HINTS_OUTPUT_FILE[in,opt]* + + Optional output file path for storing the merged hints YAML file. If + not provided, hints generation is skipped entirely. This opt-in + behaviour prevents hint files from different binaries overwriting each + other in multi-binary projects. + Generate metadata for the specified ``binary`` or ``executable`` target and store it in the specified ``OUTPUT_FILE``. If no ``OUTPUT_FILE`` is provided, the default location ``/project_description.json`` will be @@ -776,7 +784,7 @@ endfunction() #]] function(idf_build_generate_metadata) set(options) - set(one_value OUTPUT_FILE BINARY EXECUTABLE) + set(one_value OUTPUT_FILE BINARY EXECUTABLE HINTS_OUTPUT_FILE) set(multi_value) cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) @@ -865,11 +873,37 @@ function(idf_build_generate_metadata) get_filename_component(ARG_OUTPUT_FILE "${ARG_OUTPUT_FILE}" ABSOLUTE BASE_DIR "${BUILD_DIR}") + if(DEFINED ARG_HINTS_OUTPUT_FILE) + set(HINTS_FILE "${ARG_HINTS_OUTPUT_FILE}") + else() + set(HINTS_FILE "") + endif() configure_file("${IDF_PATH}/tools/cmake/project_description.json.in" "${ARG_OUTPUT_FILE}.templ") file(READ "${ARG_OUTPUT_FILE}.templ" project_description_json_templ) file(REMOVE "${ARG_OUTPUT_FILE}.templ") file(GENERATE OUTPUT "${ARG_OUTPUT_FILE}" CONTENT "${project_description_json_templ}") + + # Assumption: all hints.yml files are bare YAML lists (no "---" document + # separators). Plain string concatenation is safe under this assumption. + # Note for consumers: yaml.safe_load() only parses the first YAML document, + # so document separators in source files would cause data loss. + if(DEFINED ARG_HINTS_OUTPUT_FILE) + set(_merged_hints "") + set(_global_hints_file "${IDF_PATH}/tools/idf_py_actions/hints.yml") + if(EXISTS "${_global_hints_file}") + file(READ "${_global_hints_file}" _hints_content) + string(APPEND _merged_hints "${_hints_content}\n") + endif() + foreach(_comp_dir ${build_component_paths}) + set(_hints_file "${_comp_dir}/hints.yml") + if(EXISTS "${_hints_file}") + file(READ "${_hints_file}" _hints_content) + string(APPEND _merged_hints "${_hints_content}\n") + endif() + endforeach() + file(WRITE "${ARG_HINTS_OUTPUT_FILE}" "${_merged_hints}") + endif() endfunction() #[[ diff --git a/tools/cmakev2/project.cmake b/tools/cmakev2/project.cmake index b26a9a49a1..4122691c17 100644 --- a/tools/cmakev2/project.cmake +++ b/tools/cmakev2/project.cmake @@ -741,7 +741,8 @@ function(__project_default) TARGET app-flash NAME "app" FLASH) - idf_build_generate_metadata(BINARY "${executable}_binary_signed") + idf_build_generate_metadata(BINARY "${executable}_binary_signed" + HINTS_OUTPUT_FILE "${BUILD_DIR}/hints.yml") else() idf_build_binary("${executable}" OUTPUT_FILE "${build_dir}/${executable}.bin" @@ -758,12 +759,14 @@ function(__project_default) idf_create_dfu("${executable}_binary" TARGET dfu) - idf_build_generate_metadata(BINARY "${executable}_binary") + idf_build_generate_metadata(BINARY "${executable}_binary" + HINTS_OUTPUT_FILE "${BUILD_DIR}/hints.yml") endif() idf_build_generate_flasher_args() else() - idf_build_generate_metadata(EXECUTABLE "${executable}") + idf_build_generate_metadata(EXECUTABLE "${executable}" + HINTS_OUTPUT_FILE "${BUILD_DIR}/hints.yml") endif() idf_create_menuconfig("${executable}" diff --git a/tools/test_build_system/test_common.py b/tools/test_build_system/test_common.py index e1729cd473..821af6d962 100644 --- a/tools/test_build_system/test_common.py +++ b/tools/test_build_system/test_common.py @@ -11,6 +11,7 @@ import textwrap from pathlib import Path import pytest +import yaml from test_build_system_helpers import EnvDict from test_build_system_helpers import IdfPyFunc from test_build_system_helpers import append_to_file @@ -397,3 +398,46 @@ def test_hints_components_loading( assert 'HINT FROM PROJECT COMPONENT' in ret.stderr, ( 'Hint from project component should be displayed in build output' ) + + +def test_merged_hints_artifact_in_build_dir(idf_py: IdfPyFunc, test_app_copy: Path) -> None: + """Check that hints.yml is generated in the build directory and that hints from all components are merged""" + # Create a local component with a uniquely identifiable hint entry so we + # can verify it ends up in the merged output. + test_comp_dir = test_app_copy / 'components' / 'test_hint_comp' + test_comp_dir.mkdir(parents=True, exist_ok=True) + (test_comp_dir / 'CMakeLists.txt').write_text('idf_component_register()\n') + (test_comp_dir / 'hints.yml').write_text( + '- re: "UNIQUE_TEST_HINT_MARKER_12345"\n hint: "This is a test hint for merge verification"\n' + ) + # In buildv2, only components in the REQUIRES chain are included in + # build_component_paths. Add test_hint_comp so its hints are merged. + # This call is harmless in v1 (all components are auto-discovered). + replace_in_file( + test_app_copy / 'main' / 'CMakeLists.txt', + '# placeholder_inside_idf_component_register', + 'REQUIRES test_hint_comp', + ) + + idf_py('reconfigure') + hints_file = test_app_copy / 'build' / 'hints.yml' + assert hints_file.is_file(), 'hints.yml should exist in the build directory after reconfigure' + content = hints_file.read_text(encoding='utf-8') + parsed = yaml.safe_load(content) + assert isinstance(parsed, list), 'hints.yml should be a valid YAML list' + assert len(parsed) > 0, 'hints.yml should be non-empty' + + # Verify hints from the custom component are actually merged in + hint_patterns = [entry.get('re', '') for entry in parsed if isinstance(entry, dict)] + assert any('UNIQUE_TEST_HINT_MARKER_12345' in p for p in hint_patterns), ( + 'Custom component hint should be present in merged hints.yml' + ) + + +@pytest.mark.buildv2_skip('hello_world uses cmake/project.cmake (v1 only)') +def test_merged_hints_artifact_real_project(idf_py: IdfPyFunc, test_app_copy: Path) -> None: + """Check that hints.yml is generated in a custom build directory (-B flag)""" + # Verify the build dir is dynamic, not hardcoded + idf_py('-B', 'custom_build', 'reconfigure') + custom_hints_file = test_app_copy / 'custom_build' / 'hints.yml' + assert custom_hints_file.is_file(), 'hints.yml should exist in a custom build directory'