fix(ulp): use builtin v2 project for legacy embed

Route ulp_embed_binary() through a built-in CMake v2 default ULP project instead of generating a compatibility project with a synthetic main component.

Attach legacy ULP sources directly to the ULP executable, pass the parent component include view through the existing ULP_S_SOURCES/COMPONENT_* channel, and remove the generated legacy project templates plus the component path ownership validation bypass they required.

Update the build-system coverage to assert that the builtin child project builds and rebuilds when parent-owned ULP sources change.
This commit is contained in:
Renz Bagaporo
2026-07-21 10:37:50 +09:00
parent dc813d91ea
commit 83cdcc1da3
8 changed files with 94 additions and 165 deletions

View File

@@ -1,5 +1,10 @@
cmake_minimum_required(VERSION 3.22)
if(IDF_BUILD_V2)
include(${CMAKE_CURRENT_LIST_DIR}/CMakeLists_v2.txt)
return()
endif()
include(${IDF_PATH}/tools/cmake/idf.cmake)
project(${ULP_APP_NAME})
add_executable(${ULP_APP_NAME})

View File

@@ -0,0 +1,52 @@
# CMake v2 default ULP project for ulp_embed_binary() callers. The ULP runtime
# is built from the ulp component graph, and the legacy source list is attached
# directly to the executable, matching the CMake v1 link semantics.
include(${CMAKE_CURRENT_LIST_DIR}/ulp_project.cmake)
if("${ULP_TYPE}" STREQUAL "fsm")
project(${ULP_APP_NAME} ASM)
else()
project(${ULP_APP_NAME} C CXX ASM)
endif()
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)
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})
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}")
endif()
if(COMPONENT_INCLUDES)
target_include_directories(${ULP_APP_NAME} 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})
endif()
if(DEFINED ULP_VAR_PREFIX AND NOT "${ULP_VAR_PREFIX}" STREQUAL "")
ulp_build_binary(${ULP_APP_NAME} PREFIX ${ULP_VAR_PREFIX})
else()
ulp_build_binary(${ULP_APP_NAME})
endif()
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}")

View File

@@ -1,10 +0,0 @@
cmake_minimum_required(VERSION 3.22)
include(${IDF_PATH}/components/ulp/cmake/ulp_project.cmake)
if("${ULP_TYPE}" STREQUAL "fsm")
project(${IDF_DEFAULT_PROJECT_NAME} ASM)
else()
project(${IDF_DEFAULT_PROJECT_NAME} C CXX ASM)
endif()
ulp_project_default()

View File

@@ -1,17 +0,0 @@
include(${CMAKE_CURRENT_LIST_DIR}/ulp_legacy_inputs.cmake)
if(ULP_LEGACY_PARENT_SDKCONFIG_HEADER)
add_compile_options(-include "${ULP_LEGACY_PARENT_SDKCONFIG_HEADER}")
endif()
idf_component_register(SRCS ${ULP_LEGACY_SOURCES}
INCLUDE_DIRS ${ULP_LEGACY_INCLUDE_DIRS}
REQUIRES ulp
# Match CMake v1-compatible idf_component_register(WHOLE_ARCHIVE)
# semantics used by existing ULP test/example components.
WHOLE_ARCHIVE)
# This generated compatibility component intentionally uses parent app sources
# and include directories. Skip __component_validation_check_sources() and
# __component_validation_check_include_dirs(), which validate native CMake v2
# component ownership in tools/cmakev2/component_validation.cmake.
idf_component_set_property("${COMPONENT_NAME}" __SKIP_COMPONENT_PATH_OWNERSHIP_VALIDATION YES)

View File

@@ -1,16 +0,0 @@
set(ULP_LEGACY_SOURCES
@ULP_LEGACY_SOURCES@
)
set(ULP_LEGACY_INCLUDE_DIR_CANDIDATES
@ULP_LEGACY_INCLUDE_DIR_CANDIDATES@
)
set(ULP_LEGACY_PARENT_SDKCONFIG_HEADER [=[@ULP_LEGACY_PARENT_SDKCONFIG_HEADER@]=])
set(ULP_LEGACY_INCLUDE_DIRS)
foreach(include IN LISTS ULP_LEGACY_INCLUDE_DIR_CANDIDATES)
if(IS_DIRECTORY "${include}")
list(APPEND ULP_LEGACY_INCLUDE_DIRS "${include}")
endif()
endforeach()

View File

@@ -6,88 +6,8 @@ endif()
#
# Create ULP binary and embed into the application.
# Generate a minimal CMake v2 ULP project in the parent build directory for
# legacy ulp_embed_binary() callers. This lets the v2 ULP component graph build
# legacy source lists without writing into the application source tree.
function(__ulp_write_legacy_project project_dir sources component_dir)
set(template_dir "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/cmake/legacy_project")
set(main_dir "${project_dir}/main")
file(MAKE_DIRECTORY "${main_dir}")
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
"${template_dir}/CMakeLists.txt.in"
"${template_dir}/main/CMakeLists.txt.in"
"${template_dir}/main/ulp_legacy_inputs.cmake.in")
file(GENERATE OUTPUT "${project_dir}/CMakeLists.txt"
INPUT "${template_dir}/CMakeLists.txt.in")
file(GENERATE OUTPUT "${main_dir}/CMakeLists.txt"
INPUT "${template_dir}/main/CMakeLists.txt.in")
set(ULP_LEGACY_SOURCES)
foreach(source IN LISTS sources)
string(APPEND ULP_LEGACY_SOURCES " [=[${source}]=]\n")
endforeach()
set(ULP_LEGACY_INCLUDE_DIR_CANDIDATES)
if(DEFINED COMPONENT_NAME)
set(legacy_include_components "${COMPONENT_NAME}")
foreach(require_property IN ITEMS REQUIRES PRIV_REQUIRES)
idf_component_get_property(component_requires "${COMPONENT_NAME}" ${require_property})
list(APPEND legacy_include_components ${component_requires})
endforeach()
list(REMOVE_DUPLICATES legacy_include_components)
foreach(component IN LISTS legacy_include_components)
if(component STREQUAL "ulp")
continue()
endif()
idf_component_get_property(include_component_dir "${component}" COMPONENT_DIR)
set(include_properties INCLUDE_DIRS)
if(component STREQUAL COMPONENT_NAME)
list(APPEND include_properties PRIV_INCLUDE_DIRS)
endif()
foreach(include_property IN LISTS include_properties)
idf_component_get_property(component_includes "${component}" ${include_property})
foreach(include IN LISTS component_includes)
if(IS_ABSOLUTE "${include}")
string(APPEND ULP_LEGACY_INCLUDE_DIR_CANDIDATES " [=[${include}]=]\n")
elseif(include_component_dir)
string(APPEND ULP_LEGACY_INCLUDE_DIR_CANDIDATES
" [=[${include_component_dir}/${include}]=]\n")
endif()
endforeach()
endforeach()
endforeach()
endif()
if(component_dir)
string(APPEND ULP_LEGACY_INCLUDE_DIR_CANDIDATES " [=[${component_dir}]=]\n")
endif()
set(ULP_LEGACY_PARENT_SDKCONFIG_HEADER)
if(SDKCONFIG_HEADER AND EXISTS "${SDKCONFIG_HEADER}")
set(ULP_LEGACY_PARENT_SDKCONFIG_HEADER "${main_dir}/ulp_parent_sdkconfig.h")
file(STRINGS "${SDKCONFIG_HEADER}" parent_sdkconfig_lines)
set(parent_sdkconfig_content
"/* Generated from the parent project sdkconfig for legacy ULP sources. */\n")
foreach(parent_sdkconfig_line IN LISTS parent_sdkconfig_lines)
if(parent_sdkconfig_line MATCHES "^#define CONFIG_IDF_TOOLCHAIN(_[A-Z]+)?[ \t]")
continue()
endif()
string(APPEND parent_sdkconfig_content "${parent_sdkconfig_line}\n")
endforeach()
file(GENERATE OUTPUT "${ULP_LEGACY_PARENT_SDKCONFIG_HEADER}"
CONTENT "${parent_sdkconfig_content}")
endif()
file(READ "${template_dir}/main/ulp_legacy_inputs.cmake.in" inputs_template)
string(CONFIGURE "${inputs_template}" inputs_content @ONLY)
file(GENERATE OUTPUT "${main_dir}/ulp_legacy_inputs.cmake"
CONTENT "${inputs_content}")
endfunction()
function(__setup_ulp_project app_name project_path prefix prefix_append_bin_name type binary_names
legacy_project s_sources exp_dep_srcs)
s_sources exp_dep_srcs)
if(NOT CMAKE_BUILD_EARLY_EXPANSION)
set(ulp_cmake_dir "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/cmake")
@@ -218,6 +138,7 @@ function(__setup_ulp_project app_name project_path prefix prefix_append_bin_name
-D__ULP_BUILD=1
-DIDF_PARENT_BUILD_DIR=${build_dir}
-DULP_PREFIX_APPEND_BIN_NAME=${prefix_append_bin_name}
-DULP_PARENT_SDKCONFIG_HEADER=${SDKCONFIG_HEADER}
-DSDKCONFIG=${sdkconfig}
-DSDKCONFIG_HEADER=${SDKCONFIG_HEADER}
-DSDKCONFIG_CMAKE=${SDKCONFIG_CMAKE}
@@ -229,27 +150,21 @@ function(__setup_ulp_project app_name project_path prefix prefix_append_bin_name
# share more of the normal IDF app toolchain response-file
# setup, but keep them under IDF_CUSTOM_TOOLCHAIN for now.
-DIDF_CUSTOM_TOOLCHAIN=1)
if(legacy_project)
set(generated_project_dir "${ulp_binary_dir}/legacy_project")
__ulp_write_legacy_project("${generated_project_dir}" "${sources}" "${COMPONENT_DIR}")
set(project_path "${generated_project_dir}")
elseif(s_sources)
if(s_sources)
list(APPEND ulp_project_args
-DULP_S_SOURCES=$<TARGET_PROPERTY:${app_name},ULP_SOURCES>)
endif()
if(NOT legacy_project)
if(DEFINED COMPONENT_TARGET AND TARGET ${COMPONENT_TARGET})
# Backward compatibility for existing ULP subprojects that
# include headers from the parent component. New full
# subprojects should declare their own component include
# directories and dependencies instead.
list(APPEND ulp_project_args
-DCOMPONENT_INCLUDES=$<TARGET_PROPERTY:${COMPONENT_TARGET},INTERFACE_INCLUDE_DIRECTORIES>)
endif()
if(COMPONENT_DIR)
list(APPEND ulp_project_args
-DCOMPONENT_DIR=${COMPONENT_DIR})
endif()
if(DEFINED COMPONENT_TARGET AND TARGET ${COMPONENT_TARGET})
# Backward compatibility for existing ULP subprojects that
# include headers from the parent component. New full
# subprojects should declare their own component include
# directories and dependencies instead.
list(APPEND ulp_project_args
-DCOMPONENT_INCLUDES=$<TARGET_PROPERTY:${COMPONENT_TARGET},INTERFACE_INCLUDE_DIRECTORIES>)
endif()
if(COMPONENT_DIR)
list(APPEND ulp_project_args
-DCOMPONENT_DIR=${COMPONENT_DIR})
endif()
else()
list(APPEND ulp_project_args
@@ -300,10 +215,8 @@ function(__setup_ulp_project app_name project_path prefix prefix_append_bin_name
endforeach()
endif()
# Only the CMake v1 path reads this property when passing ULP_S_SOURCES
# to the child project. Leaving it set for v2 is harmless and keeps the
# post-ExternalProject artifact wiring common. Can be dropped when
# v1 support is dropped.
# Both build system versions pass ULP_S_SOURCES to the child project
# through this property; see the generator expression above.
set_property(TARGET ${app_name} PROPERTY ULP_SOURCES "${sources}")
spaces2list(exp_dep_srcs)
@@ -377,7 +290,7 @@ function(ulp_embed_binary app_name s_sources exp_dep_srcs)
__setup_ulp_project("${app_name}" "${ulp_cmake_dir}"
"${ULP_PREFIX}" FALSE "${ULP_TYPE}"
"${app_name}"
TRUE "${s_sources}" "${exp_dep_srcs}")
"${s_sources}" "${exp_dep_srcs}")
endfunction()
function(ulp_add_project app_name project_path)
@@ -402,5 +315,5 @@ function(ulp_add_project app_name project_path)
endif()
__setup_ulp_project("${app_name}" "${project_path}" "${ULP_PREFIX}"
"${ULP_PREFIX_APPEND_BIN_NAME}" "${ULP_TYPE}" "${ULP_BINARIES}" FALSE "" "")
"${ULP_PREFIX_APPEND_BIN_NAME}" "${ULP_TYPE}" "${ULP_BINARIES}" "" "")
endfunction()

View File

@@ -170,11 +170,6 @@ function(__component_validation_run_checks)
# Run validation checks for each component
foreach(component_interface ${component_interfaces})
__idf_component_get_property_unchecked(skip_path_ownership_validation ${component_interface}
__SKIP_COMPONENT_PATH_OWNERSHIP_VALIDATION)
if(skip_path_ownership_validation)
continue()
endif()
__component_validation_check_sources(${component_interface})
__component_validation_check_include_dirs(${component_interface})
endforeach()

View File

@@ -108,9 +108,9 @@ def test_ulp_cmake_api_availability(
expected_available: tuple[str, ...],
expected_unavailable: tuple[str, ...],
) -> None:
# Full ULP subprojects and generated legacy ULP child projects intentionally
# expose different CMake API surfaces. Configure a minimal child project
# through each entry point and verify that only the expected commands exist.
# Full ULP subprojects and legacy ULP child projects intentionally expose
# different CMake API surfaces. Configure a minimal child project through
# each entry point and verify that only the expected commands exist.
idf_path = Path(os.environ['IDF_PATH'])
project_dir = tmp_path / 'project'
build_dir = tmp_path / 'build'
@@ -156,22 +156,19 @@ def test_ulp_cmake_api_availability(
@pytest.mark.test_app_copy('components/ulp/test_apps/ulp_fsm')
def test_ulp_embed_binary_uses_generated_legacy_project(idf_py: IdfPyFunc, test_app_copy: Path) -> None:
# Legacy ulp_embed_binary() callers should be built through a generated
# CMake v2 child project. The generated project references parent-owned ULP
def test_ulp_embed_binary_builds_v2_child_project(idf_py: IdfPyFunc, test_app_copy: Path) -> None:
# Legacy ulp_embed_binary() callers are built through the builtin ULP child
# project as a CMake v2 subproject. The child references parent-owned ULP
# sources by path, so editing those sources must still rebuild the child
# artifact on the next parent build.
assert test_app_copy.exists()
idf_py('-DIDF_TARGET=esp32', '-DSDKCONFIG=build/sdkconfig', 'build')
generated_projects = sorted(Path('build/subprojects').glob('*/legacy_project'))
assert len(generated_projects) == 1
ulp_binary_dirs = sorted(p for p in Path('build/subprojects').glob('*') if p.is_dir())
assert len(ulp_binary_dirs) == 1
generated_project = generated_projects[0]
ulp_binary_dir = generated_project.parent
assert (generated_project / 'CMakeLists.txt').exists()
assert (generated_project / 'main/ulp_legacy_inputs.cmake').exists()
ulp_binary_dir = ulp_binary_dirs[0]
ulp_binary = ulp_binary_dir / f'{ulp_binary_dir.name}.bin'
assert ulp_binary.exists()
original_ulp_binary_hash = _sha256(ulp_binary)
@@ -189,6 +186,16 @@ def test_ulp_embed_binary_uses_generated_legacy_project(idf_py: IdfPyFunc, test_
assert _sha256(ulp_binary) != original_ulp_binary_hash
@pytest.mark.test_app_copy('examples/system/ulp/ulp_riscv/interrupts')
def test_ulp_embed_binary_uses_parent_project_config(idf_py: IdfPyFunc, test_app_copy: Path) -> None:
# Legacy ulp_embed_binary() sources are compiled as part of the builtin ULP
# child project, but still rely on project-local CONFIG_* symbols from the
# parent app's Kconfig.projbuild.
assert test_app_copy.exists()
idf_py('-DIDF_TARGET=esp32s2', '-DSDKCONFIG=build/sdkconfig', 'build')
@pytest.mark.test_app_copy('tools/test_apps/system/ulp/full_subproject/lp_core')
def test_ulp_full_project_regenerates_config_from_parent_sdkconfig(idf_py: IdfPyFunc, test_app_copy: Path) -> None:
# Full ULP subprojects use the parent sdkconfig as their source of truth.