feat(cmakev2/build): add idf_build_generate_metadata function

The `idf_build_generate_metadata()` function is used to generate project
metadata, specifically the `project_description.json` file, for a given
executable target. The output file path can be specified as an argument
to the function, allowing metadata generation for multiple executables.
It identifies the library, created by the `idf_build_library` function,
that is linked to the executable based on the `LINK_LIBRARIES`
executable target property. The `LIBRARY_INTERFACES` build property
tracks all library interfaces created with the `idf_build_library`
function, so the targets in the `LINK_LIBRARIES` executable property are
checked against this list to find the library. The library contains
information about components linked to it in the
`LIBRARY_COMPONENTS_LINKED` library property. These components are
processed and added to the `build_component_info` dictionary in the
`project_description.json` along with other information.

Note that, compared to cmakev1, the `all_component_info` dictionary in
`project_description` does not include some information by default, such
as included directories. This information is available in cmakev1 due to
early component evaluation, which is not done in cmakev2. This will
likely require some adjustments on the hints side.

Signed-off-by: Frantisek Hrbata <frantisek.hrbata@espressif.com>
This commit is contained in:
Frantisek Hrbata
2025-08-29 14:38:50 +02:00
parent e87205592a
commit aad467b5d1
4 changed files with 200 additions and 29 deletions

View File

@@ -434,3 +434,189 @@ function(idf_build_executable executable)
target_link_libraries(${executable} PRIVATE ${library})
endfunction()
#[[
__get_components_metadata(COMPONENTS <component>...
OUTPUT <variable>)
:COMPONENTS[in]: List of components for which to generate metadata.
:OUTPUT[out]: Output variable to store JSON metadata.
Generate metadata in JSON format from ``COMPONENTS`` and store it in the
``OUTPUT`` variable.
#]]
function(__get_components_metadata)
set(options)
set(one_value OUTPUT)
set(multi_value COMPONENTS)
cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN})
if(NOT DEFINED ARG_COMPONENTS)
idf_die("COMPONENTS option is required")
endif()
if(NOT DEFINED ARG_OUTPUT)
idf_die("OUTPUT option is required")
endif()
set(components_json "")
foreach(name IN LISTS ARG_COMPONENTS)
idf_component_get_property(target ${name} COMPONENT_REAL_TARGET)
idf_component_get_property(alias ${name} COMPONENT_ALIAS)
idf_component_get_property(prefix ${name} __PREFIX)
idf_component_get_property(dir ${name} COMPONENT_DIR)
idf_component_get_property(type ${name} COMPONENT_TYPE)
idf_component_get_property(lib ${name} COMPONENT_LIB)
idf_component_get_property(reqs ${name} REQUIRES)
idf_component_get_property(include_dirs ${name} INCLUDE_DIRS)
idf_component_get_property(priv_reqs ${name} PRIV_REQUIRES)
idf_component_get_property(managed_reqs ${name} MANAGED_REQUIRES)
idf_component_get_property(managed_priv_reqs ${name} MANAGED_PRIV_REQUIRES)
if("${type}" STREQUAL "LIBRARY")
set(file "$<TARGET_LINKER_FILE:${lib}>")
# The idf_component_register function is converting each source file path defined
# in SRCS into absolute one. But source files can be also added with cmake's
# target_sources and have relative paths. This is used for example in log
# component. Let's make sure all source files have absolute path.
set(sources "")
get_target_property(srcs ${lib} SOURCES)
foreach(src IN LISTS srcs)
get_filename_component(src "${src}" ABSOLUTE BASE_DIR "${dir}")
list(APPEND sources "${src}")
endforeach()
else()
set(file "")
set(sources "")
endif()
__make_json_list("${reqs}" OUTPUT reqs)
__make_json_list("${priv_reqs}" OUTPUT priv_reqs)
__make_json_list("${managed_reqs}" OUTPUT managed_reqs)
__make_json_list("${managed_priv_reqs}" OUTPUT managed_priv_reqs)
__make_json_list("${include_dirs}" OUTPUT include_dirs)
__make_json_list("${sources}" OUTPUT sources)
string(JOIN "\n" component_json
" \"${name}\": {"
" \"alias\": \"${alias}\","
" \"target\": \"${target}\","
" \"prefix\": \"${prefix}\","
" \"dir\": \"${dir}\","
" \"type\": \"${type}\","
" \"lib\": \"${lib}\","
" \"reqs\": ${reqs},"
" \"priv_reqs\": ${priv_reqs},"
" \"managed_reqs\": ${managed_reqs},"
" \"managed_priv_reqs\": ${managed_priv_reqs},"
" \"file\": \"${file}\","
" \"sources\": ${sources},"
" \"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(${ARG_OUTPUT} "${components_json}" PARENT_SCOPE)
endfunction()
#[[api
.. cmakev2:function:: idf_build_generate_metadata
.. code-block:: cmake
idf_build_generate_metadata(<executable>
[FILE <file>])
:executable[in]: Executable target for which to generate a metadata file.
:FILE[in,opt]: Optional output file path for storing the metadata. If not
provided, the default path ``<build>/project_description.json``
is used.
Generate metadata for the specified ``executable`` and store it in the
specified ``FILE``. If no ``FILE`` is provided, the default location
``<build>/project_description.json`` will be used.
#]]
function(idf_build_generate_metadata executable)
set(options)
set(one_value FILE)
set(multi_value)
cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN})
__get_executable_library_or_die(TARGET "${executable}" OUTPUT library)
idf_build_get_property(PROJECT_NAME PROJECT_NAME)
idf_build_get_property(PROJECT_VER PROJECT_VER)
idf_build_get_property(PROJECT_PATH PROJECT_DIR)
idf_build_get_property(IDF_PATH IDF_PATH)
idf_build_get_property(BUILD_DIR BUILD_DIR)
idf_build_get_property(SDKCONFIG SDKCONFIG)
idf_build_get_property(SDKCONFIG_DEFAULTS SDKCONFIG_DEFAULTS)
set(PROJECT_EXECUTABLE "$<TARGET_FILE_NAME:${executable}>")
# The PROJECT_BIN executable property must be set by the idf_build_binary
# function.
get_target_property(PROJECT_BIN "${executable}" PROJECT_BIN)
if(NOT PROJECT_BIN)
set(PROJECT_BIN "")
endif()
if(CONFIG_APP_BUILD_TYPE_RAM)
set(PROJECT_BUILD_TYPE ram_app)
else()
set(PROJECT_BUILD_TYPE flash_app)
endif()
idf_build_get_property(IDF_VER IDF_VER)
idf_build_get_property(common_component_reqs __COMPONENT_REQUIRES_COMMON)
list(SORT common_component_reqs)
__make_json_list("${common_component_reqs}" OUTPUT common_component_reqs_json)
idf_library_get_property(build_components "${library}" LIBRARY_COMPONENTS_LINKED)
list(SORT build_components)
__make_json_list("${build_components}" OUTPUT build_components_json)
set(build_component_paths)
set(COMPONENT_KCONFIGS)
set(COMPONENT_KCONFIGS_PROJBUILD)
foreach(component_name IN LISTS build_components)
idf_component_get_property(component_dir "${component_name}" COMPONENT_DIR)
idf_component_get_property(component_kconfig "${component_name}" __KCONFIG)
idf_component_get_property(component_kconfig_projbuild "${component_name}" __KCONFIG_PROJBUILD)
list(APPEND build_component_paths "${component_dir}")
if(component_kconfig)
list(APPEND COMPONENT_KCONFIGS "${component_kconfig}")
endif()
if(component_kconfig_projbuild)
list(APPEND COMPONENT_KCONFIGS_PROJBUILD "${component_kconfig_projbuild}")
endif()
endforeach()
__make_json_list("${build_component_paths}" OUTPUT build_component_paths_json)
__get_components_metadata(COMPONENTS "${build_components}" OUTPUT build_component_info_json)
idf_build_get_property(components_discovered COMPONENTS_DISCOVERED)
__get_components_metadata(COMPONENTS "${components_discovered}" OUTPUT all_component_info_json)
__generate_gdbinit()
idf_build_get_property(gdbinit_files_prefix_map GDBINIT_FILES_PREFIX_MAP)
idf_build_get_property(gdbinit_files_symbols GDBINIT_FILES_SYMBOLS)
idf_build_get_property(gdbinit_files_py_extensions GDBINIT_FILES_PY_EXTENSIONS)
idf_build_get_property(gdbinit_files_connect GDBINIT_FILES_CONNECT)
__get_openocd_options(debug_arguments_openocd)
if(NOT DEFINED ARG_FILE)
set(ARG_FILE "${BUILD_DIR}/project_description.json")
endif()
configure_file("${IDF_PATH}/tools/cmake/project_description.json.in" "${ARG_FILE}.templ")
file(READ "${ARG_FILE}.templ" project_description_json_templ)
file(REMOVE "${ARG_FILE}.templ")
file(GENERATE OUTPUT "${ARG_FILE}"
CONTENT "${project_description_json_templ}")
endfunction()

View File

@@ -17,6 +17,16 @@ set(CMAKE_MODULE_PATH
# for both cmakev1 and cmakev2.
include(${CMAKE_CURRENT_LIST_DIR}/../cmake/version.cmake)
# The gdbinit.cmake file from cmakev1 contains a single function,
# __generate_gdbinit, which is used in the generation of
# project_description.json.
include(${CMAKE_CURRENT_LIST_DIR}/../cmake/gdbinit.cmake)
# The openocd.cmake file from cmakev1 contains a single function,
# __get_openocd_options, which is used in the generation of
# project_description.json.
include(${CMAKE_CURRENT_LIST_DIR}/../cmake/openocd.cmake)
include(component)
include(build)
include(kconfig)

View File

@@ -565,34 +565,6 @@ macro(idf_project_init)
unset(project_initialized)
endmacro()
#[[
__generate_project_info()
Generate a dummy project_description.json file to enable the use of idf.py
commands that require ensure_build_directory, such as reconfigure.
FIXME: This needs to generate a complete project_description.json for the
specified executable.
#]]
function(__generate_project_info)
idf_build_get_property(IDF_PATH IDF_PATH)
idf_build_get_property(PROJECT_NAME PROJECT_NAME)
idf_build_get_property(PROJECT_VER PROJECT_VER)
idf_build_get_property(PROJECT_PATH PROJECT_DIR)
idf_build_get_property(BUILD_DIR BUILD_DIR)
idf_build_get_property(SDKCONFIG SDKCONFIG)
idf_build_get_property(SDKCONFIG_DEFAULTS SDKCONFIG_DEFAULTS)
set(common_component_reqs_json "\"\"")
set(build_components_json "\"\"")
set(build_component_paths_json "\"\"")
set(build_component_info_json "\"\"")
set(all_component_info_json "\"\"")
configure_file("${IDF_PATH}/tools/cmake/project_description.json.in"
"${BUILD_DIR}/project_description.json")
endfunction()
#[[api
.. cmakev2:macro:: idf_project_default
@@ -609,5 +581,6 @@ macro(idf_project_default)
idf_project_init()
idf_build_get_property(project_name PROJECT_NAME)
idf_build_executable("${project_name}" COMPONENTS main SUFFIX ".elf")
idf_build_generate_metadata("${project_name}")
unset(project_name)
endmacro()

View File

@@ -208,8 +208,11 @@ endfunction()
# cmake --build build/ --target hello_world_example
# cmake --build build/ --target fatfs_example
function(test_executable)
idf_build_get_property(build_dir BUILD_DIR)
idf_build_executable(fatfs_example COMPONENTS fatfs_example)
idf_build_generate_metadata(fatfs_example FILE "${build_dir}/project_description_fatfs.json")
idf_build_executable(hello_world_example COMPONENTS hello_world_example)
idf_build_generate_metadata(hello_world_example FILE "${build_dir}/project_description_hello_world.json")
endfunction()
# Run tests
@@ -229,6 +232,5 @@ idf_project_default()
test_component_priority()
__dump_all_properties()
__generate_project_info()
message("ALL TESTS PASSED")