From aaca1e20ea3a5af43a0d1f644ab5d5495edce1f3 Mon Sep 17 00:00:00 2001 From: Frantisek Hrbata Date: Sat, 4 Jul 2026 20:00:06 +0200 Subject: [PATCH] feat(cmakev2): preprocess linker scripts with component includes Resolve #include lines in linker script templates against the linked component graph: idf_build_library appends every linked component's INCLUDE_DIRS to the C preprocessor invocation for each .in linker script, so a template can include any component header (e.g. a ULP memory-layout template including soc/soc.h) without the owning component having to know about it. Also: - Always pass -I so templates can include sdkconfig.h. - Add a FLAGS option to target_linker_script for explicit preprocessor flags (e.g. -D__ASSEMBLER__ or ld-snippet include dirs); when given, FLAGS replaces the parent-dir==target include heuristic for that script. - Add a MEMORY option to target_linker_script that emits the marked linker script as -T before all others, so section-placement scripts in one component can reference MEMORY regions and REGION_ALIASes declared by a memory-layout script in another component. - Make C-preprocessor comment keeping (-C) part of the default flag set rather than hardcoded, so FLAGS can drop it. linker_script_preprocessor no longer forces -C; both the cmakev1 and cmakev2 preprocessors add it to their default CFLAGS (output unchanged for existing scripts). A ULP template that includes soc/soc.h omits -C so ld does not choke on the header's // comments. - Store per-script metadata (generated output, flags) in MD5-keyed component properties instead of parallel lists. Signed-off-by: Frantisek Hrbata --- tools/cmake/linker_script_preprocessor.cmake | 6 +- tools/cmake/utilities.cmake | 5 +- tools/cmakev2/build.cmake | 100 ++++++++++++++----- tools/cmakev2/compat.cmake | 46 ++++++++- tools/cmakev2/utilities.cmake | 77 ++++++++++---- 5 files changed, 184 insertions(+), 50 deletions(-) diff --git a/tools/cmake/linker_script_preprocessor.cmake b/tools/cmake/linker_script_preprocessor.cmake index df06494a702..c70cccd7b2b 100644 --- a/tools/cmake/linker_script_preprocessor.cmake +++ b/tools/cmake/linker_script_preprocessor.cmake @@ -4,7 +4,11 @@ # Note: Paths are expected to be quoted in the CFLAGS variable separate_arguments(CFLAGS_LIST UNIX_COMMAND "${CFLAGS}") -execute_process(COMMAND "${CC}" "-C" "-P" "-x" "c" "-E" ${CFLAGS_LIST} "${SOURCE}" +# Comment handling is left to the caller via CFLAGS. The default linker-script +# CFLAGS keep comments (-C) to preserve historical output. Callers whose +# templates include headers with C++-style "//" comments, which ld rejects, +# e.g. the ULP memory layout including soc/soc.h, omit -C in their flags. +execute_process(COMMAND "${CC}" "-P" "-x" "c" "-E" ${CFLAGS_LIST} "${SOURCE}" RESULT_VARIABLE RET_CODE OUTPUT_VARIABLE PREPROCESSED_LINKER_SCRIPT ERROR_VARIABLE ERROR_VAR) diff --git a/tools/cmake/utilities.cmake b/tools/cmake/utilities.cmake index 4580c30e2a9..ddbbbe149a3 100644 --- a/tools/cmake/utilities.cmake +++ b/tools/cmake/utilities.cmake @@ -214,13 +214,16 @@ function(preprocess_linker_file cmake_target script_in output_var preserve_suffi set(extra_cflags "-I\"${dir_to_include}\"") endif() + # Keep comments (-C): historical behavior for cmakev1 linker scripts. It was + # previously hardcoded in linker_script_preprocessor.cmake, which now leaves + # comment handling to the caller. add_custom_command( OUTPUT ${script_out} COMMAND ${CMAKE_COMMAND} "-DCC=${CMAKE_C_COMPILER}" "-DSOURCE=${script_in}" "-DTARGET=${script_out}" - "-DCFLAGS=-I\"${config_dir}\" ${extra_cflags}" + "-DCFLAGS=-C -I\"${config_dir}\" ${extra_cflags}" -P "${linker_script_generator}" MAIN_DEPENDENCY ${script_in} DEPENDS ${sdkconfig_header} diff --git a/tools/cmakev2/build.cmake b/tools/cmakev2/build.cmake index 343a11013ca..bf3dd00638b 100644 --- a/tools/cmakev2/build.cmake +++ b/tools/cmakev2/build.cmake @@ -473,8 +473,33 @@ function(idf_build_library library) string(MAKE_C_IDENTIFIER "_${library}" suffix) idf_library_get_property(component_interfaces_linked "${library}" LIBRARY_COMPONENT_INTERFACES_LINKED) + + # Build -I arguments for the public include dirs of every component in the + # linked closure. These are appended to every linker-script preprocess so a + # template can #include any component header (e.g. the ULP memory layout + # including soc/soc.h) without the component having to know about it. + # INCLUDE_DIRS is stored relative to each component's COMPONENT_DIR. + set(component_include_flags "") + foreach(ci IN LISTS component_interfaces_linked) + idf_component_get_property(ci_dir "${ci}" COMPONENT_DIR) + idf_component_get_property(ci_includes "${ci}" INCLUDE_DIRS) + foreach(ci_include IN LISTS ci_includes) + get_filename_component(ci_include_abs "${ci_include}" ABSOLUTE BASE_DIR "${ci_dir}") + # Quote the path: the preprocessor invocation splits CFLAGS with + # separate_arguments(UNIX_COMMAND), so unquoted paths containing + # spaces would fall apart. + string(APPEND component_include_flags " -I\"${ci_include_abs}\"") + endforeach() + endforeach() + + # Linker scripts marked MEMORY (the memory-layout base for the link) are + # emitted as -T before the rest, so section-placement scripts from any + # component can reference their MEMORY regions and REGION_ALIASes. Collect + # across all components into two ordered lists and emit memory ones first. + set(memory_scripts "") + set(other_scripts "") + foreach(component_interface IN LISTS component_interfaces_linked) - set(scripts) idf_component_get_property(component_dir "${component_interface}" COMPONENT_DIR) idf_component_get_property(component_build_dir "${component_interface}" COMPONENT_BUILD_DIR) idf_component_get_property(preprocessed "${component_interface}" __LINKER_SCRIPTS_PREPROCESSED) @@ -483,6 +508,9 @@ function(idf_build_library library) idf_component_get_property(scripts_static "${component_interface}" LINKER_SCRIPTS) __get_absolute_paths(PATHS "${scripts_static}" BASE_DIR "${component_dir}" OUTPUT scripts_static_abs) foreach(script IN LISTS scripts_static_abs) + __linker_script_key("${script}" script_key) + idf_component_get_property(script_is_memory "${component_interface}" + "LINKER_SCRIPT_MEMORY_${script_key}") if(script MATCHES "\\.in$") # If the linker script file name ends with the ".in" extension, # preprocess it with the C preprocessor. @@ -492,27 +520,37 @@ function(idf_build_library library) if(NOT preprocessed) file(MAKE_DIRECTORY "${component_build_dir}/ld") - __preprocess_linker_script("${script}" "${script_out}") + idf_component_get_property(script_flags "${component_interface}" + "LINKER_SCRIPT_FLAGS_${script_key}") + __preprocess_linker_script("${script}" "${script_out}" + "${script_flags}" "${component_include_flags}") # Add a custom target for the preprocessed script. add_custom_target(${script_target} DEPENDS "${script_out}") endif() # Add dependency for the library interface to ensure the script is # generated before linking. add_dependencies("${library}" ${script_target}) - list(APPEND scripts "${script_out}") + set(emit_script "${script_out}") else() - list(APPEND scripts "${script}") + set(emit_script "${script}") + endif() + if(script_is_memory) + list(APPEND memory_scripts "${emit_script}") + else() + list(APPEND other_scripts "${emit_script}") endif() endforeach() - # Generate linker scripts from templates. - # LINKER_SCRIPTS_TEMPLATE and LINKER_SCRIPTS_GENERATED are parallel - # lists. The first holds the template linker script path, and the - # second holds the generated linker script path. + # Generate linker scripts from templates. The generated output path and + # the optional preprocessor flags for each template are stored as + # component properties keyed by the template, so no second list has to + # stay index-aligned with LINKER_SCRIPTS_TEMPLATE. idf_component_get_property(template_scripts "${component_interface}" LINKER_SCRIPTS_TEMPLATE) - idf_component_get_property(generated_scripts "${component_interface}" LINKER_SCRIPTS_GENERATED) - foreach(template script IN ZIP_LISTS template_scripts generated_scripts) + foreach(template IN LISTS template_scripts) + __linker_script_key("${template}" template_key) + idf_component_get_property(script "${component_interface}" + "LINKER_SCRIPT_GENERATED_${template_key}") if(template MATCHES "\\.in$") # If the linker script file name ends with the ".in" extension, # preprocess it with the C preprocessor. @@ -520,7 +558,10 @@ function(idf_build_library library) set(template_out "${component_build_dir}/ld/${template_name}") if(NOT preprocessed) file(MAKE_DIRECTORY "${component_build_dir}/ld") - __preprocess_linker_script("${template}" "${template_out}") + idf_component_get_property(template_flags "${component_interface}" + "LINKER_SCRIPT_FLAGS_${template_key}") + __preprocess_linker_script("${template}" "${template_out}" + "${template_flags}" "${component_include_flags}") endif() set(template "${template_out}") endif() @@ -529,7 +570,13 @@ function(idf_build_library library) TEMPLATE "${template}" SUFFIX "${suffix}" OUTPUT "${script}") - list(APPEND scripts "${script}") + idf_component_get_property(template_is_memory "${component_interface}" + "LINKER_SCRIPT_MEMORY_${template_key}") + if(template_is_memory) + list(APPEND memory_scripts "${script}") + else() + list(APPEND other_scripts "${script}") + endif() # Add a custom target for the generated script and include it as a # dependency for the library interface to ensure the script is # generated before linking. @@ -545,21 +592,22 @@ function(idf_build_library library) # property to indicate that the linker scripts have already been # preprocessed. idf_component_set_property("${component_interface}" __LINKER_SCRIPTS_PREPROCESSED YES) + endforeach() - # Finally, add all preprocessed and ldgen-generated linker scripts to - # the library interface. - foreach(script IN LISTS scripts) - get_filename_component(script_dir "${script}" DIRECTORY) - get_filename_component(script_name "${script}" NAME) - # Add linker script directory to the linker search path. - target_link_directories("${library}" INTERFACE "${script_dir}") - # Add linker script to link. Regarding the usage of SHELL, see - # https://cmake.org/cmake/help/latest/command/target_link_options.html#option-de-duplication - target_link_options("${library}" INTERFACE "SHELL:-T ${script_name}") - # Add the linker script as a dependency to ensure the executable is - # re-linked if the script changes. - set_property(TARGET "${library}" APPEND PROPERTY INTERFACE_LINK_DEPENDS "${script}") - endforeach() + # Emit the collected linker scripts as -T on the library interface, with + # memory-layout scripts first so their MEMORY regions and REGION_ALIASes are + # seen before any section-placement script references them. + foreach(script IN LISTS memory_scripts other_scripts) + get_filename_component(script_dir "${script}" DIRECTORY) + get_filename_component(script_name "${script}" NAME) + # Add linker script directory to the linker search path. + target_link_directories("${library}" INTERFACE "${script_dir}") + # Add linker script to link. Regarding the usage of SHELL, see + # https://cmake.org/cmake/help/latest/command/target_link_options.html#option-de-duplication + target_link_options("${library}" INTERFACE "SHELL:-T ${script_name}") + # Add the linker script as a dependency to ensure the executable is + # re-linked if the script changes. + set_property(TARGET "${library}" APPEND PROPERTY INTERFACE_LINK_DEPENDS "${script}") endforeach() # Validate components linked to this library diff --git a/tools/cmakev2/compat.cmake b/tools/cmakev2/compat.cmake index c3a0dabe944..e1e19158356 100644 --- a/tools/cmakev2/compat.cmake +++ b/tools/cmakev2/compat.cmake @@ -165,6 +165,25 @@ endfunction() ``linkerscript`` template. The ``linkerscript`` is processed with ldgen to produce the ``output``. + *FLAGS[in,opt]* + + Explicit preprocessor flags for the linker script(s) registered by this + call, for example ``-D`` defines or extra ``-I`` include directories. + When given, they replace the default parent-dir==target include + heuristic while a ``.in`` script or template is preprocessed. + ``-I`` and the linked components' include directories are + always added regardless. Applies to every ``scriptfile`` in the same + call. + + *MEMORY[opt]* + + Marks the script(s) as the memory-layout base for the link. Such + scripts are emitted as ``-T`` before all other linker scripts, so that + section-placement scripts from other components can reference their + ``MEMORY`` regions and ``REGION_ALIAS`` names. Needed when the memory + layout lives in a different component than the section scripts that + depend on it. + This function adds one or more linker scripts to the specified component target, incorporating the linker script into the linking process. @@ -178,19 +197,38 @@ function(target_linker_script target deptype scriptfiles) # The linker script files, templates, and their output filenames are stored # only as component properties. The script files are generated and added to # the library link interface in the idf_build_library function. - set(options) - set(one_value PROCESS) + set(options MEMORY) + set(one_value PROCESS FLAGS) set(multi_value) cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) foreach(scriptfile ${scriptfiles}) get_filename_component(scriptfile "${scriptfile}" ABSOLUTE) idf_msg("Adding linker script ${scriptfile}") + __linker_script_key("${scriptfile}" script_key) if(ARG_PROCESS) get_filename_component(output "${ARG_PROCESS}" ABSOLUTE) idf_component_set_property("${target}" LINKER_SCRIPTS_TEMPLATE "${scriptfile}" APPEND) - idf_component_set_property("${target}" LINKER_SCRIPTS_GENERATED "${output}" APPEND) + # Key the generated output path by the template instead of keeping a + # second list index-aligned with LINKER_SCRIPTS_TEMPLATE. + idf_component_set_property("${target}" "LINKER_SCRIPT_GENERATED_${script_key}" "${output}") else() - idf_component_set_property("${target}" LINKER_SCRIPTS ${scriptfile} APPEND) + idf_component_set_property("${target}" LINKER_SCRIPTS "${scriptfile}" APPEND) + endif() + # Memory-layout scripts are emitted before other scripts in the link, + # so section-placement scripts can reference their MEMORY regions. + if(ARG_MEMORY) + idf_component_set_property("${target}" "LINKER_SCRIPT_MEMORY_${script_key}" TRUE) + endif() + # Per-script preprocessor flags, consumed by __preprocess_linker_script + # when a ".in" script or template is built. The "__DEFAULT__" sentinel + # means FLAGS was not given (use the parent-dir==target heuristic); any + # other value, including empty, means FLAGS was given and replaces it. + # This distinguishes "not given" from "given empty", which the property + # value alone cannot (both would read back as empty). + if(DEFINED ARG_FLAGS) + idf_component_set_property("${target}" "LINKER_SCRIPT_FLAGS_${script_key}" "${ARG_FLAGS}") + else() + idf_component_set_property("${target}" "LINKER_SCRIPT_FLAGS_${script_key}" "__DEFAULT__") endif() endforeach() endfunction() diff --git a/tools/cmakev2/utilities.cmake b/tools/cmakev2/utilities.cmake index 5be6325b6ff..d44a4cd5b28 100644 --- a/tools/cmakev2/utilities.cmake +++ b/tools/cmakev2/utilities.cmake @@ -1182,7 +1182,30 @@ function(fail_at_build_time target_name message_line0) endfunction() #[[ - __preprocess_linker_script( ) + __linker_script_key( ) + + *path[in]* + + Linker script or template path. + + *output[out]* + + Variable set to a stable key derived from the absolute ``path``. + + Compute a collision-resistant key for a linker script. It is used to store + per-script metadata such as the generated output path and preprocessor + flags as component properties. Both the producer ``target_linker_script`` + and the consumer ``idf_build_library`` must derive the key identically, so + the absolute-path normalization lives here and nowhere else. +#]] +function(__linker_script_key path output) + get_filename_component(path_abs "${path}" ABSOLUTE) + string(MD5 hash "${path_abs}") + set(${output} "${hash}" PARENT_SCOPE) +endfunction() + +#[[ + __preprocess_linker_script( ) *script_in[in]* @@ -1192,29 +1215,47 @@ endfunction() Path where the preprocessed linker script will be saved. + *flags[in]* + + Explicit preprocessor flags from ``target_linker_script``'s ``FLAGS`` + option, or the literal ``__DEFAULT__`` when no ``FLAGS`` was given. + ``__DEFAULT__`` keeps comments (``-C``) and selects the parent-dir==target + include heuristic; any other value, including an empty string, replaces + that whole set, so a caller can drop ``-C``. + + *component_includes[in]* + + Pre-built ``-I`` arguments for the linked component graph, always + appended so a template can include any component header. + Run the C preprocessor on ``script_in`` and store the result in - ``script_out``. + ``script_out``. The preprocessor always receives ``-I`` so + ``sdkconfig.h`` resolves, plus ``component_includes``. #]] -function(__preprocess_linker_script script_in script_out) +function(__preprocess_linker_script script_in script_out flags component_includes) idf_build_get_property(sdkconfig_header __SDKCONFIG_HEADER) idf_build_get_property(idf_path IDF_PATH) idf_build_get_property(config_dir CONFIG_DIR) idf_build_get_property(idf_target IDF_TARGET) - # This approach is not ideal, but it is the current method used in cmakev1 - # for the esp_system component. The linker script files for specific - # targets within the esp_system component include the ld.common file. This - # adds the ld.common directory to the search path for the C preprocessor. - # This method works for the specific directory layout of esp_common, but if - # other components with different layouts adopt this approach, adjustments - # will be necessary. It might be better to include the files using relative - # paths in the linker scripts. - get_filename_component(script_parent_dir "${script_in}" DIRECTORY) - get_filename_component(script_parent_name "${script_parent_dir}" NAME) - set(extra_cflags "") - if(script_parent_name STREQUAL idf_target) - get_filename_component(dir_to_include "${script_parent_dir}" DIRECTORY) - set(extra_cflags "-I\"${dir_to_include}\"") + if(flags STREQUAL "__DEFAULT__") + # No FLAGS were given: keep comments (-C, the historical default) and + # apply the parent-dir==target include heuristic. esp_system's + # target-specific scripts live in ld// and include the sibling + # ld.common file, so add the parent directory to the C preprocessor + # search path. Works for that layout; a component with a different + # layout, or one that must drop -C, should pass FLAGS instead. + set(base_flags "-C") + get_filename_component(script_parent_dir "${script_in}" DIRECTORY) + get_filename_component(script_parent_name "${script_parent_dir}" NAME) + if(script_parent_name STREQUAL idf_target) + get_filename_component(dir_to_include "${script_parent_dir}" DIRECTORY) + string(APPEND base_flags " -I\"${dir_to_include}\"") + endif() + else() + # Explicit FLAGS replace the whole default set, including -C. config_dir + # and the component include dirs are always added regardless. + set(base_flags "${flags}") endif() set(linker_script_generator "${idf_path}/tools/cmake/linker_script_preprocessor.cmake") @@ -1225,7 +1266,7 @@ function(__preprocess_linker_script script_in script_out) "-DCC=${CMAKE_C_COMPILER}" "-DSOURCE=${script_in}" "-DTARGET=${script_out}" - "-DCFLAGS=-I\"${config_dir}\" ${extra_cflags}" + "-DCFLAGS=-I\"${config_dir}\" ${base_flags} ${component_includes}" -P "${linker_script_generator}" MAIN_DEPENDENCY "${script_in}" DEPENDS "${sdkconfig_header}"