refactor(ulp): provide a ulp_project.cmake subproject wrapper API

Build ULP full subprojects through a dedicated entry file,
components/ulp/cmake/ulp_project.cmake, that wraps tools/cmakev2/idf.cmake
and layers a small ULP API on top, mirroring the cmakev2 layering:

  ulp_project_init      like idf_project_init (init, detect the ULP type,
                        reset the compile/link options inherited from the app)
  ulp_build_executable  like idf_build_executable, plus the embeddable
                        .bin/.h/.ld artifacts
  ulp_project_default   like idf_project_default (single-executable case)

A child project now includes this one file instead of idf.cmake and calls
these helpers directly, so idf_build_executable is used as-is for the
multi-binary case and the module-path indirection (include(IDFULPProject)
resolved via -DCMAKE_MODULE_PATH) is gone.

As a result:
- IDFULPProjectv2.cmake is removed; its setup moves into the wrapper.
- IDFULPProject.cmake becomes the CMake v1-only entry point.
- The ULP component no longer registers a POST_ELF callback; the binary
  artifacts are produced by ulp_build_executable.
- The v2 full-subproject examples (lp_core, riscv, fsm, multi_binary,
  combined) are updated to the new API.
- The ULP subproject API is documented in build-system-v2.rst.

Also fix a latent bug this exercises: idf_build_library emitted linker
scripts as "-T <name>" relying on a following "-L" search directory. GNU
ld only searches -L directories that precede -T, so the direct
esp32ulp-elf-ld link used for ULP FSM failed to open the script. Emit the
absolute path instead, matching what the CMake v1 ULP build already does.

Finally, replace the parent-argument bypass loop that used to live in
IDFULPProjectv2.cmake with --no-warn-unused-cli on the child configure,
and stop passing the unused IDF_PARENT_BUILD_DIR.

Signed-off-by: Frantisek Hrbata <frantisek.hrbata@espressif.com>
This commit is contained in:
Frantisek Hrbata
2026-07-15 09:34:05 +09:00
committed by Renz Bagaporo
parent ceaa0b7a3d
commit a9ea4f1105
13 changed files with 233 additions and 142 deletions
-16
View File
@@ -181,19 +181,3 @@ elseif(ULP_TYPE STREQUAL "fsm")
FLAGS "${ulp_ld_flags}")
target_link_options(${COMPONENT_LIB} INTERFACE "SHELL:-u entry")
endif()
function(__ulp_configure_executable ulp_app_name)
# Generate the files consumed by the parent app: .bin payload plus symbol
# header/linker exports produced from the ULP ELF.
set(binary_target_args)
if(DEFINED ULP_VAR_PREFIX)
set(ulp_var_prefix "${ULP_VAR_PREFIX}")
if(ULP_PREFIX_APPEND_BIN_NAME)
string(MAKE_C_IDENTIFIER "${ulp_var_prefix}${ulp_app_name}_" ulp_var_prefix)
endif()
list(APPEND binary_target_args PREFIX ${ulp_var_prefix})
endif()
ulp_add_build_binary_targets(${ulp_app_name} ${binary_target_args})
endfunction()
idf_component_register_build_event_callback(EVENT POST_ELF CALLBACK __ulp_configure_executable)
+4 -5
View File
@@ -1,8 +1,7 @@
if(IDF_BUILD_V2 AND COMMAND idf_project_init)
include(${CMAKE_CURRENT_LIST_DIR}/IDFULPProjectv2.cmake)
return()
endif()
# This is the CMake v1 (legacy) ULP child entry point, used by ulp_embed_binary.
# CMake v2 full-subproject builds include components/ulp/cmake/ulp_project.cmake
# instead.
#
# Legacy ULP child projects are plain CMake projects, so the parent-provided
# sdkconfig.cmake has to be loaded here before the common ULP helpers inspect
# CONFIG_* values. CMake v1 callers pass this path explicitly.
@@ -1,87 +0,0 @@
include(${CMAKE_CURRENT_LIST_DIR}/IDFULPProjectCommon.cmake)
if(NOT COMMAND idf_project_init)
message(FATAL_ERROR "CMake v2 ULP projects must include tools/cmakev2/idf.cmake "
"before including IDFULPProject.")
endif()
# Full IDF-style ULP projects include sdkconfig.cmake again during
# idf_project_init(). Re-including it is harmless because the generated file
# only assigns CONFIG_* variables, and this early include keeps lower-level ULP
# helper APIs working before idf_project_init().
include(${SDKCONFIG_CMAKE})
ulp_detect_build_type()
get_property(enabled_languages GLOBAL PROPERTY ENABLED_LANGUAGES)
if(NOT ASM IN_LIST enabled_languages)
enable_language(ASM)
endif()
ulp_apply_build_type_options()
function(ulp_prepare_build)
idf_build_get_property(ulp_build_prepared __ULP_BUILD_PREPARED)
if(ulp_build_prepared)
return()
endif()
if(BUILD_FSM)
check_expected_tool_version("esp32ulp-elf" ${CMAKE_ASM_COMPILER})
# FSM links with esp32ulp-elf-ld directly, not through GCC, so avoid
# GCC-driver link grouping flags from the generic IDF executable helper.
idf_build_set_property(LINKER_TYPE ULP_FSM)
endif()
# ULP projects use their own toolchain files and component-provided options,
# so discard compile/link options added for normal app builds.
foreach(property IN ITEMS
COMPILE_OPTIONS
C_COMPILE_OPTIONS
CXX_COMPILE_OPTIONS
ASM_COMPILE_OPTIONS
COMPILE_DEFINITIONS
LINK_OPTIONS)
idf_build_set_property(${property} "")
endforeach()
# Do not apply the default app component closure to ULP child projects.
# ULP components should contribute dependencies explicitly.
idf_build_set_property(__COMPONENT_REQUIRES_COMMON "")
idf_build_set_property(__COMMON_COMPONENT_INTERFACES "")
idf_build_set_property(__COMMON_COMPONENTS_INITIALIZED YES)
# Some helpers can be called in sequence by custom ULP projects; keep this
# setup idempotent within a child configure.
idf_build_set_property(__ULP_BUILD_PREPARED YES)
endfunction()
function(__ulp_project_default)
set(ulp_app_name "${PROJECT_NAME}")
set(executable_args COMPONENTS main)
if(NOT BUILD_FSM)
list(APPEND executable_args MAPFILE_TARGET ${ulp_app_name}_mapfile)
endif()
idf_build_executable(${ulp_app_name} ${executable_args})
idf_build_generate_metadata(EXECUTABLE ${ulp_app_name})
if(TARGET "${ulp_app_name}_mapfile")
idf_create_size_report("${ulp_app_name}_mapfile"
TARGET size)
endif()
idf_build_generate_depgraph("${ulp_app_name}")
endfunction()
macro(ulp_project_default)
idf_project_init()
ulp_prepare_build()
# Use DEFERRED optional-requires resolution only when this will be the sole
# library being built.
idf_build_set_property(IDF_COMPONENT_OPTIONAL_REQUIRES_MODE DEFERRED)
__ulp_project_default()
endmacro()
+188
View File
@@ -0,0 +1,188 @@
# ULP subproject entry point for CMake v2 full-subproject builds.
#
# A ULP child project includes this file instead of tools/cmakev2/idf.cmake
# directly. It pulls in the cmakev2 build system and layers the ULP subproject
# API on top: ulp_project_init, ulp_build_executable, ulp_build_binary and
# ulp_project_default, the ULP analogs of idf_project_init,
# idf_build_executable, idf_build_binary and idf_project_default.
include(${IDF_PATH}/tools/cmakev2/idf.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/IDFULPProjectCommon.cmake)
# Reset the compile/link state inherited from the parent app build and select
# ULP-specific build behavior. Idempotent within a child configure.
function(__ulp_prepare_build)
idf_build_get_property(ulp_build_prepared __ULP_BUILD_PREPARED)
if(ulp_build_prepared)
return()
endif()
if(BUILD_FSM)
check_expected_tool_version("esp32ulp-elf" ${CMAKE_ASM_COMPILER})
# FSM links with esp32ulp-elf-ld directly, not through GCC, so avoid
# GCC-driver link grouping flags from the generic executable helper.
idf_build_set_property(LINKER_TYPE ULP_FSM)
endif()
# ULP projects use their own toolchain files and component-provided options,
# so discard compile/link options added for normal app builds.
foreach(property IN ITEMS
COMPILE_OPTIONS
C_COMPILE_OPTIONS
CXX_COMPILE_OPTIONS
ASM_COMPILE_OPTIONS
COMPILE_DEFINITIONS
LINK_OPTIONS)
idf_build_set_property(${property} "")
endforeach()
# Do not apply the default app component closure to ULP child projects.
# ULP components should contribute dependencies explicitly.
idf_build_set_property(__COMPONENT_REQUIRES_COMMON "")
idf_build_set_property(__COMMON_COMPONENT_INTERFACES "")
idf_build_set_property(__COMMON_COMPONENTS_INITIALIZED YES)
idf_build_set_property(__ULP_BUILD_PREPARED YES)
endfunction()
#[[api
.. cmakev2:macro:: ulp_project_init
.. code-block:: cmake
ulp_project_init()
Initialize a ULP full-subproject build. Calls :cmakev2:ref:`idf_project_init`
and then applies the ULP-specific setup: it detects the ULP type (RISC-V, LP
core or FSM) from the ``ULP_TYPE`` variable or the project configuration and
resets the compile and link options inherited from the parent application
build, so the ULP executable uses only its own toolchain and
component-provided options.
This is the ULP analog of :cmakev2:ref:`idf_project_init` and is the entry
point for projects that build more than one ULP executable, followed by
:cmakev2:ref:`ulp_build_executable` and :cmakev2:ref:`ulp_build_binary`
calls. For the common single-executable case use
:cmakev2:ref:`ulp_project_default` instead.
#]]
macro(ulp_project_init)
idf_project_init()
ulp_detect_build_type()
ulp_apply_build_type_options()
__ulp_prepare_build()
endmacro()
#[[api
.. cmakev2:function:: ulp_build_executable
.. code-block:: cmake
ulp_build_executable(<executable>
[COMPONENTS <component>...]
[MAPFILE_TARGET <target>])
*executable[in]*
Name of the ULP executable target to create.
Build a ULP executable. This is the ULP analog of
:cmakev2:ref:`idf_build_executable` and forwards its arguments to
:cmakev2:ref:`idf_build_executable`. Call :cmakev2:ref:`ulp_build_binary`
for each executable that should produce embeddable ULP artifacts.
#]]
function(ulp_build_executable executable)
if("PREFIX" IN_LIST ARGN)
message(FATAL_ERROR "Use ulp_build_binary(${executable} PREFIX ...) to set ULP symbol prefixes.")
endif()
idf_build_executable(${executable} ${ARGN})
endfunction()
#[[api
.. cmakev2:function:: ulp_build_binary
.. code-block:: cmake
ulp_build_binary(<executable>
[PREFIX <prefix>])
*executable[in]*
ULP executable target for which to generate embeddable artifacts.
*PREFIX[in,opt]*
Symbol name prefix for the generated symbol header and linker export
files. Defaults to ``ulp_``. Use a distinct prefix per executable when a
project builds several ULP binaries.
Generate the ``.bin`` payload and symbol header/linker export files for a
ULP executable. This is the ULP analog of :cmakev2:ref:`idf_build_binary`.
#]]
function(ulp_build_binary executable)
cmake_parse_arguments(_ULP "" "PREFIX" "" ${ARGN})
if(_ULP_UNPARSED_ARGUMENTS)
message(FATAL_ERROR "Unexpected arguments to ulp_build_binary(): ${_ULP_UNPARSED_ARGUMENTS}")
endif()
if(DEFINED _ULP_PREFIX)
ulp_add_build_binary_targets(${executable} PREFIX "${_ULP_PREFIX}")
elseif(ULP_PREFIX_APPEND_BIN_NAME)
set(prefix "ulp_")
if(DEFINED ULP_VAR_PREFIX)
set(prefix "${ULP_VAR_PREFIX}")
endif()
string(MAKE_C_IDENTIFIER "${prefix}${executable}_" prefix)
ulp_add_build_binary_targets(${executable} PREFIX "${prefix}")
else()
ulp_add_build_binary_targets("${executable}")
endif()
endfunction()
# Build the single default ULP executable from the main component.
function(__ulp_project_default)
set(ulp_app_name "${PROJECT_NAME}")
set(executable_args COMPONENTS main)
set(binary_args)
if(NOT BUILD_FSM)
list(APPEND executable_args MAPFILE_TARGET ${ulp_app_name}_mapfile)
endif()
if(DEFINED ULP_VAR_PREFIX AND NOT "${ULP_VAR_PREFIX}" STREQUAL "")
list(APPEND binary_args PREFIX ${ULP_VAR_PREFIX})
endif()
ulp_build_executable(${ulp_app_name} ${executable_args})
ulp_build_binary(${ulp_app_name} ${binary_args})
idf_build_generate_metadata(EXECUTABLE ${ulp_app_name})
if(TARGET "${ulp_app_name}_mapfile")
idf_create_size_report("${ulp_app_name}_mapfile" TARGET size)
endif()
idf_build_generate_depgraph("${ulp_app_name}")
endfunction()
#[[api
.. cmakev2:macro:: ulp_project_default
.. code-block:: cmake
ulp_project_default()
Build a single ULP executable from the ``main`` component and its transitive
dependencies, the ULP analog of :cmakev2:ref:`idf_project_default`. Calls
:cmakev2:ref:`ulp_project_init`, then builds the executable (named after
``PROJECT_NAME``) with :cmakev2:ref:`ulp_build_executable` and generates its
embeddable artifacts with :cmakev2:ref:`ulp_build_binary`.
#]]
macro(ulp_project_default)
ulp_project_init()
# A single executable is built, so DEFERRED optional-requires resolution is
# safe and keeps the linked component set minimal.
idf_build_set_property(IDF_COMPONENT_OPTIONAL_REQUIRES_MODE DEFERRED)
__ulp_project_default()
endmacro()
+7 -2
View File
@@ -125,6 +125,10 @@ function(__setup_ulp_project app_name project_path prefix prefix_append_bin_name
set(ulp_project_args)
if(IDF_BUILD_V2)
set(ulp_project_args
# The parent passes a superset of variables that different ULP
# subproject styles consume (e.g. ULP_VAR_PREFIX only by
# ulp_project_default, COMPONENT_INCLUDES only by hand-written
# subprojects). Do not warn about the ones a given child ignores.
--no-warn-unused-cli
-DIDF_DEFAULT_PROJECT_NAME=${app_name}
-DIDF_BUILD_V2=y
@@ -166,7 +170,9 @@ function(__setup_ulp_project app_name project_path prefix prefix_append_bin_name
-DCOMPONENT_DIR=${COMPONENT_DIR}
-DCOMPONENT_INCLUDES=$<TARGET_PROPERTY:${COMPONENT_TARGET},INTERFACE_INCLUDE_DIRECTORIES>
-DSDKCONFIG_HEADER=${SDKCONFIG_HEADER}
-DSDKCONFIG_CMAKE=${SDKCONFIG_CMAKE})
-DSDKCONFIG_CMAKE=${SDKCONFIG_CMAKE}
# The v1 ULP child resolves include(IDFULPProject) via the module path.
-DCMAKE_MODULE_PATH=${ulp_cmake_dir})
endif()
externalproject_add(${app_name}
@@ -183,7 +189,6 @@ function(__setup_ulp_project app_name project_path prefix prefix_append_bin_name
-DIDF_TARGET=${idf_target}
-DIDF_PATH=${idf_path}
-DPYTHON=${python}
-DCMAKE_MODULE_PATH=${ulp_cmake_dir}
${extra_cmake_args}
BUILD_COMMAND ${CMAKE_COMMAND} --build ${ulp_binary_dir} --target build
BUILD_BYPRODUCTS ${ulp_artifacts} ${ulp_artifacts_extras} ${ulp_ps_sources}