fix(ulp): preserve legacy ulp_embed_binary link order

Legacy ulp_embed_binary() call-site sources were attached directly to the CMake v2 child executable. However, the ULP runtime is linked with WHOLE_ARCHIVE through target_link_options(), which CMake emits before executable objects. This reversed the CMake v1 order, changed ULP code placement, and allowed runtime weak definitions to take precedence over legacy strong handlers.

Build the call-site sources as a dedicated static archive and place its whole-archive option before the runtime archive. Preserve the parent sdkconfig and include view, including FSM-specific include behavior, without generating a synthetic component tree.

Factor linker-specific whole-archive handling into a shared CMake v2 helper for GNU, Darwin, and ULP FSM linkers. Link the archive target normally as well so CMake tracks build and relink dependencies when a legacy ULP source changes.
This commit is contained in:
Renz Bagaporo
2026-07-29 21:32:40 +09:00
parent 1f5cba3070
commit 2fe7e0f047
3 changed files with 55 additions and 32 deletions

View File

@@ -15,30 +15,47 @@ ulp_project_init()
# safe and keeps the linked component set minimal.
idf_build_set_property(IDF_COMPONENT_OPTIONAL_REQUIRES_MODE DEFERRED)
set(executable_args COMPONENTS ulp)
if(NOT BUILD_FSM)
list(APPEND executable_args MAPFILE_TARGET ${ULP_APP_NAME}_mapfile)
# CMake v1 linked the ulp_embed_binary() call-site sources before the ULP runtime.
# In CMake v2 the runtime's WHOLE_ARCHIVE is emitted as a link option, before
# executable objects, so attaching these sources directly would reverse that
# order. Keep them in a separate archive that can be placed before the runtime,
# preserving legacy code placement and strong-over-weak symbol resolution.
function(__ulp_embed_binary_add_legacy_sources executable)
set(legacy_sources_lib "${executable}_legacy_sources")
add_library(${legacy_sources_lib} STATIC ${ULP_S_SOURCES})
target_link_libraries(${legacy_sources_lib} PRIVATE "library_${executable}")
if(ADD_PICOLIBC_SPECS)
target_compile_options(${legacy_sources_lib} PRIVATE $<$<COMPILE_LANG_AND_ID:C,GNU>:-specs=picolibc.specs>)
target_compile_options(${legacy_sources_lib} PRIVATE $<$<COMPILE_LANG_AND_ID:CXX,GNU>:-specs=picolibcpp.specs>)
endif()
ulp_build_executable(${ULP_APP_NAME} ${executable_args})
target_sources(${ULP_APP_NAME} PRIVATE ${ULP_S_SOURCES})
if(ULP_PARENT_SDKCONFIG_HEADER)
# Legacy ulp_embed_binary() sources are authored against the parent app's
# sdkconfig, including project-local Kconfig.projbuild symbols.
get_filename_component(parent_sdkconfig_dir "${ULP_PARENT_SDKCONFIG_HEADER}" DIRECTORY)
target_include_directories(${ULP_APP_NAME} BEFORE PRIVATE "${parent_sdkconfig_dir}")
target_include_directories(${legacy_sources_lib} BEFORE PRIVATE "${parent_sdkconfig_dir}")
endif()
if(COMPONENT_INCLUDES)
target_include_directories(${ULP_APP_NAME} PRIVATE ${COMPONENT_INCLUDES})
target_include_directories(${legacy_sources_lib} PRIVATE ${COMPONENT_INCLUDES})
endif()
# CMake v1 exposed COMPONENT_DIR only to the FSM assembly preprocessing, not to
# the compilation of ULP C sources. The v2 FSM toolchain preprocesses inside
# the compile rule, which takes its include flags from the target, so keep the
# directory on the target for FSM builds only.
if(BUILD_FSM AND COMPONENT_DIR)
target_include_directories(${ULP_APP_NAME} PRIVATE ${COMPONENT_DIR})
target_include_directories(${legacy_sources_lib} PRIVATE ${COMPONENT_DIR})
endif()
__idf_build_link_whole_archive(${executable} PRIVATE ${legacy_sources_lib} BEFORE)
endfunction()
set(executable_args COMPONENTS ulp)
if(NOT BUILD_FSM)
list(APPEND executable_args MAPFILE_TARGET ${ULP_APP_NAME}_mapfile)
endif()
ulp_build_executable(${ULP_APP_NAME} ${executable_args})
__ulp_embed_binary_add_legacy_sources(${ULP_APP_NAME})
if(DEFINED ULP_VAR_PREFIX AND NOT "${ULP_VAR_PREFIX}" STREQUAL "")
ulp_build_binary(${ULP_APP_NAME} PREFIX ${ULP_VAR_PREFIX})
else()

View File

@@ -298,6 +298,25 @@ function(__dump_library_properties libraries)
endforeach()
endfunction()
function(__idf_build_link_whole_archive target scope library)
idf_build_get_property(linker_type LINKER_TYPE)
if(linker_type STREQUAL "GNU")
set(link_option "SHELL:-Wl,--whole-archive $<TARGET_FILE:${library}> -Wl,--no-whole-archive")
elseif(linker_type STREQUAL "Darwin")
set(link_option "SHELL:-Wl,-force_load $<TARGET_FILE:${library}>")
elseif(linker_type STREQUAL "ULP_FSM")
set(link_option "SHELL:--whole-archive $<TARGET_FILE:${library}> --no-whole-archive")
else()
target_link_libraries(${target} ${scope} ${library})
return()
endif()
# ARGN may contain BEFORE to place the archive ahead of existing link options.
# Also link the target normally so CMake tracks its build and relink dependencies.
target_link_options(${target} ${ARGN} ${scope} "${link_option}")
target_link_libraries(${target} ${scope} ${library})
endfunction()
#[[api
.. cmakev2:function:: idf_build_library

View File

@@ -1102,20 +1102,7 @@ function(idf_component_include name)
elseif("${component_real_target_type}" STREQUAL "STATIC_LIBRARY")
idf_component_get_property(whole_archive "${component_name}" WHOLE_ARCHIVE)
if(whole_archive)
idf_build_get_property(linker_type LINKER_TYPE)
if(linker_type STREQUAL "GNU")
target_link_options("${component_interface}" INTERFACE
"SHELL:-Wl,--whole-archive $<TARGET_FILE:${component_real_target}> -Wl,--no-whole-archive")
target_link_libraries("${component_interface}" INTERFACE "${component_real_target}")
elseif(linker_type STREQUAL "Darwin")
target_link_options("${component_interface}" INTERFACE
"SHELL:-Wl,-force_load $<TARGET_FILE:${component_real_target}>")
target_link_libraries("${component_interface}" INTERFACE "${component_real_target}")
elseif(linker_type STREQUAL "ULP_FSM")
target_link_options("${component_interface}" INTERFACE
"SHELL:--whole-archive $<TARGET_FILE:${component_real_target}> --no-whole-archive")
target_link_libraries("${component_interface}" INTERFACE "${component_real_target}")
endif()
__idf_build_link_whole_archive("${component_interface}" INTERFACE "${component_real_target}")
else()
target_link_libraries("${component_interface}" INTERFACE "${component_real_target}")
endif()