Merge branch 'feat/llvm-opt-integration' into 'master'

feat(compiler): add ESP-IDF LLVM optimization enablement framework

See merge request espressif/esp-idf!51154
This commit is contained in:
Marius Vikhammer
2026-09-07 10:11:37 +08:00
24 changed files with 742 additions and 2 deletions
+11 -1
View File
@@ -1,3 +1,5 @@
include(${CMAKE_CURRENT_LIST_DIR}/llvm_optimizations.cmake)
#
# Internal function for retrieving component properties from a component target.
#
@@ -440,8 +442,9 @@ endfunction()
# @param[in, optional] KCONFIG (single value) override the default Kconfig
# @param[in, optional] KCONFIG_PROJBUILD (single value) override the default Kconfig
# @param[in, optional] WHOLE_ARCHIVE (option) link the component as --whole-archive
# @param[in, optional] ENABLE_LLVM_OPT (option) enable the LLVM optimizations selected in menuconfig
function(idf_component_register)
set(options WHOLE_ARCHIVE)
set(options WHOLE_ARCHIVE ENABLE_LLVM_OPT)
set(single_value KCONFIG KCONFIG_PROJBUILD)
set(multi_value SRCS SRC_DIRS EXCLUDE_SRCS
INCLUDE_DIRS PRIV_INCLUDE_DIRS LDFRAGMENTS REQUIRES
@@ -487,9 +490,16 @@ function(idf_component_register)
idf_build_get_property(config_dir CONFIG_DIR)
# Publish IDF_LLVM_OPT_* even when this component does not opt in, so the
# application can apply them to a third-party target with standard CMake.
__idf_llvm_opt_publish_flags()
# The contents of 'sources' is from the __component_add_sources call
if(sources OR __EMBED_FILES OR __EMBED_TXTFILES)
add_library(${component_lib} STATIC ${sources})
if(__ENABLE_LLVM_OPT)
__idf_apply_llvm_opt_to_target(${component_lib})
endif()
__component_set_property(${component_target} COMPONENT_TYPE LIBRARY)
__component_add_include_dirs(${component_lib} "${__INCLUDE_DIRS}" PUBLIC)
__component_add_include_dirs(${component_lib} "${__PRIV_INCLUDE_DIRS}" PRIVATE)
+140
View File
@@ -0,0 +1,140 @@
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
#
# SPDX-License-Identifier: Apache-2.0
include_guard(GLOBAL)
# ESP-IDF LLVM optimization framework: map menuconfig-selected optimizations to
# compiler flags. Component CMakeLists only mark opt-in scopes (file / list /
# component). Flags for each menuconfig option are also published as
# IDF_LLVM_OPT_* for standard CMake (third-party).
function(__idf_llvm_opt_collect_flags memcpy_out all_out)
set(memcpy "")
set(all "")
if(CONFIG_IDF_TOOLCHAIN_CLANG AND CMAKE_C_COMPILER_ID MATCHES "Clang")
if(CONFIG_COMPILER_LLVM_MEMCPY_OPTIMIZATION)
list(APPEND memcpy "-mllvm=-riscv-esp32-p4-mem-intrin")
# PIE 2.1 vs 2.2 comes from -mcpu (components/soc/project_include.cmake).
# +espv-lowering is still required for .m intrinsic isel (vld/vst.128.ip.m).
# Do not pass +xespv2p1: that name is not a Clang target-feature and is ignored.
list(APPEND memcpy "-Xclang" "-target-feature" "-Xclang" "+espv-lowering")
endif()
list(APPEND all ${memcpy})
endif()
set(${memcpy_out} "${memcpy}" PARENT_SCOPE)
set(${all_out} "${all}" PARENT_SCOPE)
endfunction()
# SHELL-wrapped lists for target_compile_options. Empty when Kconfig is off or not Clang.
function(__idf_llvm_opt_publish_flags)
__idf_llvm_opt_collect_flags(memcpy_flags all_flags)
__idf_llvm_opt_shell_wrap_xclang("${memcpy_flags}" memcpy_wrapped)
__idf_llvm_opt_shell_wrap_xclang("${all_flags}" all_wrapped)
set(IDF_LLVM_OPT_MEMCPY "${memcpy_wrapped}" CACHE INTERNAL
"Flags for CONFIG_COMPILER_LLVM_MEMCPY_OPTIMIZATION; empty if off or not Clang" FORCE)
set(IDF_LLVM_OPT_ALL "${all_wrapped}" CACHE INTERNAL
"Flags for all LLVM optimizations enabled in menuconfig; empty if none or not Clang" FORCE)
endfunction()
function(__idf_llvm_opt_get_options output)
set(options ${ARGN})
__idf_llvm_opt_publish_flags()
if(NOT CONFIG_IDF_TOOLCHAIN_CLANG OR NOT CMAKE_C_COMPILER_ID MATCHES "Clang")
set(${output} "" PARENT_SCOPE)
return()
endif()
__idf_llvm_opt_collect_flags(_memcpy all_flags)
list(APPEND options ${all_flags})
set(${output} "${options}" PARENT_SCOPE)
endfunction()
# target_compile_options de-duplicates identical tokens, which splits
# "-Xclang" "<arg>" pairs. SHELL: keeps each consecutive -Xclang group intact.
# Source COMPILE_OPTIONS are not de-duplicated and do not honour SHELL:.
function(__idf_llvm_opt_shell_wrap_xclang input output)
set(result "")
set(i 0)
list(LENGTH input n)
while(i LESS n)
list(GET input ${i} item)
if(item STREQUAL "-Xclang")
set(group "")
while(i LESS n)
list(GET input ${i} tok)
if(NOT tok STREQUAL "-Xclang")
break()
endif()
math(EXPR nxt "${i} + 1")
if(nxt GREATER_EQUAL n)
message(FATAL_ERROR "LLVM opt flags: -Xclang missing argument")
endif()
list(GET input ${nxt} arg)
if(group STREQUAL "")
set(group "-Xclang ${arg}")
else()
string(APPEND group " -Xclang ${arg}")
endif()
math(EXPR i "${nxt} + 1")
endwhile()
list(APPEND result "SHELL:${group}")
else()
list(APPEND result "${item}")
math(EXPR i "${i} + 1")
endif()
endwhile()
set(${output} "${result}" PARENT_SCOPE)
endfunction()
function(__idf_apply_llvm_opt_to_target target)
__idf_llvm_opt_get_options(options ${ARGN})
if(options)
__idf_llvm_opt_shell_wrap_xclang("${options}" options)
target_compile_options(${target} PRIVATE ${options})
endif()
endfunction()
function(__idf_apply_llvm_opt_to_sources)
set(multi_value SRCS OPTIONS)
cmake_parse_arguments(ARG "" "" "${multi_value}" ${ARGN})
if(ARG_UNPARSED_ARGUMENTS)
message(FATAL_ERROR "Unknown idf_component_enable_llvm_opt arguments: ${ARG_UNPARSED_ARGUMENTS}")
endif()
__idf_llvm_opt_get_options(options ${ARG_OPTIONS})
if(options)
set_property(SOURCE ${ARG_SRCS} APPEND PROPERTY COMPILE_OPTIONS ${options})
endif()
endfunction()
# Apply menuconfig-selected LLVM optimizations to one source, a source list, or
# the whole current component. OPTIONS is an advanced escape hatch for custom
# Clang/LLVM optimization options and is ignored with non-Clang toolchains.
function(idf_component_enable_llvm_opt)
set(multi_value SRCS OPTIONS)
cmake_parse_arguments(ARG "" "" "${multi_value}" ${ARGN})
if(ARG_UNPARSED_ARGUMENTS)
message(FATAL_ERROR "Unknown idf_component_enable_llvm_opt arguments: ${ARG_UNPARSED_ARGUMENTS}")
endif()
if(ARG_SRCS)
__idf_apply_llvm_opt_to_sources(SRCS ${ARG_SRCS} OPTIONS ${ARG_OPTIONS})
return()
endif()
if(COMPONENT_LIB AND TARGET ${COMPONENT_LIB})
set(component_target ${COMPONENT_LIB})
elseif(COMPONENT_TARGET AND TARGET ${COMPONENT_TARGET})
set(component_target ${COMPONENT_TARGET})
else()
message(FATAL_ERROR "idf_component_enable_llvm_opt must be called after idf_component_register")
endif()
__idf_apply_llvm_opt_to_target(${component_target} ${ARG_OPTIONS})
endfunction()
@@ -0,0 +1,93 @@
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
#
# SPDX-License-Identifier: Apache-2.0
cmake_minimum_required(VERSION 3.22)
# Never compiles: only inspects COMPILE_OPTIONS. Host CI (Windows minimal
# cmake jobs) has no C compiler on PATH, so skip compiler detection.
set(CMAKE_C_COMPILER "${CMAKE_COMMAND}" CACHE FILEPATH "" FORCE)
set(CMAKE_C_COMPILER_WORKS TRUE CACHE BOOL "" FORCE)
set(CMAKE_C_COMPILER_FORCED TRUE)
project(llvm_optimizations_test C)
include(${CMAKE_CURRENT_LIST_DIR}/../../llvm_optimizations.cmake)
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/component.c "void component(void) {}\n")
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/selected.c "void selected(void) {}\n")
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/plain.c "void plain(void) {}\n")
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/gcc.c "void gcc_source(void) {}\n")
set(CMAKE_C_COMPILER_ID Clang)
set(CONFIG_IDF_TOOLCHAIN_CLANG ON)
set(CONFIG_COMPILER_LLVM_MEMCPY_OPTIMIZATION ON)
add_library(component STATIC ${CMAKE_CURRENT_BINARY_DIR}/component.c)
set(COMPONENT_LIB component)
idf_component_enable_llvm_opt()
get_target_property(component_options component COMPILE_OPTIONS)
list(JOIN component_options " " component_options)
if(NOT component_options MATCHES "riscv-esp32-p4-mem-intrin")
message(FATAL_ERROR "Component scope is missing the selected LLVM optimization")
endif()
if(NOT component_options MATCHES "SHELL:")
message(FATAL_ERROR "Component scope must SHELL-wrap -Xclang pairs")
endif()
if(component_options MATCHES "xespv2p1")
message(FATAL_ERROR "PIE revision must come from -mcpu, not +xespv2p1")
endif()
if(NOT component_options MATCHES "espv-lowering")
message(FATAL_ERROR "Component scope is missing +espv-lowering for .m intrinsic isel")
endif()
if(NOT IDF_LLVM_OPT_ALL OR NOT IDF_LLVM_OPT_MEMCPY)
message(FATAL_ERROR "Exported IDF_LLVM_OPT_ALL / IDF_LLVM_OPT_MEMCPY must be set when memcpy is enabled")
endif()
if(NOT "${IDF_LLVM_OPT_ALL}" STREQUAL "${IDF_LLVM_OPT_MEMCPY}")
message(FATAL_ERROR "With only the memcpy optimization enabled, IDF_LLVM_OPT_ALL must equal IDF_LLVM_OPT_MEMCPY")
endif()
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/third_party.c "void third_party(void) {}\n")
add_library(fake_lvgl STATIC ${CMAKE_CURRENT_BINARY_DIR}/third_party.c)
target_compile_options(fake_lvgl PRIVATE ${IDF_LLVM_OPT_ALL})
get_target_property(third_party_options fake_lvgl COMPILE_OPTIONS)
list(JOIN third_party_options " " third_party_options)
if(NOT third_party_options MATCHES "riscv-esp32-p4-mem-intrin")
message(FATAL_ERROR "Standard CMake apply of IDF_LLVM_OPT_ALL missed the memcpy flags")
endif()
if(NOT third_party_options MATCHES "SHELL:")
message(FATAL_ERROR "IDF_LLVM_OPT_ALL must be SHELL-wrapped for target_compile_options")
endif()
add_library(selected STATIC
${CMAKE_CURRENT_BINARY_DIR}/selected.c
${CMAKE_CURRENT_BINARY_DIR}/plain.c)
set(COMPONENT_LIB selected)
idf_component_enable_llvm_opt(
SRCS ${CMAKE_CURRENT_BINARY_DIR}/selected.c
OPTIONS "-custom-llvm-option")
get_source_file_property(selected_options ${CMAKE_CURRENT_BINARY_DIR}/selected.c COMPILE_OPTIONS)
get_source_file_property(plain_options ${CMAKE_CURRENT_BINARY_DIR}/plain.c COMPILE_OPTIONS)
list(JOIN selected_options " " selected_options)
if(NOT selected_options MATCHES "riscv-esp32-p4-mem-intrin"
OR NOT selected_options MATCHES "custom-llvm-option")
message(FATAL_ERROR "Source scope is missing selected or custom options")
endif()
if(selected_options MATCHES "SHELL:")
message(FATAL_ERROR "Source COMPILE_OPTIONS must not use SHELL:")
endif()
if(plain_options)
message(FATAL_ERROR "LLVM optimization leaked to an unselected source")
endif()
set(CMAKE_C_COMPILER_ID GNU)
unset(CONFIG_IDF_TOOLCHAIN_CLANG)
add_library(gcc_component STATIC ${CMAKE_CURRENT_BINARY_DIR}/gcc.c)
set(COMPONENT_LIB gcc_component)
idf_component_enable_llvm_opt(OPTIONS "-custom-llvm-option")
get_target_property(gcc_options gcc_component COMPILE_OPTIONS)
if(gcc_options)
message(FATAL_ERROR "LLVM optimization options must be ignored for non-Clang compilers")
endif()
if(IDF_LLVM_OPT_ALL OR IDF_LLVM_OPT_MEMCPY)
message(FATAL_ERROR "Exported LLVM opt flags must be empty for non-Clang compilers")
endif()
+13 -1
View File
@@ -3,6 +3,8 @@
include_guard(GLOBAL)
include(${CMAKE_CURRENT_LIST_DIR}/../cmake/llvm_optimizations.cmake)
#[[
check_expected_tool_version()
@@ -607,6 +609,7 @@ endfunction()
[REQUIRED_IDF_TARGETS <target>...]
[EMBED_FILES <file>...]
[EMBED_TXTFILES <file>...]
[ENABLE_LLVM_OPT]
[WHOLE_ARCHIVE])
*SRCS[in,opt]*
@@ -659,12 +662,16 @@ endfunction()
Link the component as --whole-archive.
*ENABLE_LLVM_OPT[in,opt]*
Enable the LLVM optimizations selected in menuconfig for this component.
Register a new component with the build system using the provided options.
This function also automatically links all commonly required and managed
components to the component's target.
#]]
function(idf_component_register)
set(options WHOLE_ARCHIVE)
set(options WHOLE_ARCHIVE ENABLE_LLVM_OPT)
set(one_value KCONFIG KCONFIG_PROJBUILD)
set(multi_value SRCS SRC_DIRS EXCLUDE_SRCS
INCLUDE_DIRS PRIV_INCLUDE_DIRS LDFRAGMENTS REQUIRES
@@ -737,8 +744,13 @@ function(idf_component_register)
endif()
endforeach()
__idf_llvm_opt_publish_flags()
if(sources OR ARG_EMBED_FILES OR ARG_EMBED_TXTFILES)
add_library("${COMPONENT_TARGET}" STATIC ${sources})
if(ARG_ENABLE_LLVM_OPT)
__idf_apply_llvm_opt_to_target("${COMPONENT_TARGET}")
endif()
foreach(include_dir IN LISTS include_dirs)
target_include_directories("${COMPONENT_TARGET}" PUBLIC "${include_dir}")
+11
View File
@@ -440,3 +440,14 @@ def test_cmake_preset_sdkconfig_defaults_integration(test_app_copy: Path) -> Non
sdkconfig_content = sdkconfig_path.read_text()
assert 'CONFIG_LWIP_IPV6=y' in sdkconfig_content
assert 'CONFIG_ESP_TASK_WDT_TIMEOUT_S=15' in sdkconfig_content
def test_cmake_llvm_optimizations_framework(idf_copy: Path) -> None:
logging.info('Test CMake configuration of the LLVM optimization framework')
test_project = idf_copy / 'tools' / 'cmake' / 'tests' / 'llvm_optimizations'
# The standalone test project fakes the compiler ID and CONFIG_* variables to
# verify component/source scope flag selection, custom options, non-leakage
# to unselected sources, exported IDF_LLVM_OPT_* for stock CMake, and the
# non-Clang no-op. run_cmake raises on failure.
# Windows runners default to NMake; nmake is absent. Ninja is on PATH.
run_cmake(str(test_project), '-G', 'Ninja')