Files
esp-idf/tools/cmake/toolchain.cmake
Ivan Grokhotkov (bot) d20a986999 feat(build): add options to enable link-time optimization (LTO)
Link-time optimization (LTO) lets the compiler inline and optimize across
translation units. ESP-IDF relies heavily on linker-script placement rules
that match object files by name, which LTO does not preserve, so LTO cannot
be enabled for the whole framework. This change adds two opt-in options that
side-step that conflict:

- CONFIG_COMPILER_LTO_LINKTIME tells the linker to perform LTO on any object
  files that carry LTO information (compiled with -flto). On its own this is
  safe: users can add -flto to specific components (e.g. their own libraries)
  to shrink them, without affecting components that use linker fragments.

- CONFIG_COMPILER_LTO_COMPILETIME automatically compiles most
  components with -flto. A component is excluded when it has its own linker
  fragments, when it opts out via the NO_LTO component property, or when its
  object code is placed by *another* component's linker fragment (matched by
  archive name). The last case is handled by tools/cmake/lto.cmake, which
  scans linker fragments for explicit "archive: libNAME.a" placement and
  excludes those components. Without it, functions that must run from IRAM
  while the flash cache is disabled (e.g. the spi_flash / GDMA HAL routines,
  placed in IRAM by spi_flash/esp_driver_dma fragments) would be moved to
  flash by LTO and the device would panic with a cache error at run time.

Both options are disabled for the bootloader and ESP-TEE builds, which depend
on object-file-name based placement. LTO is also gated off for Clang
(needs LLD, IDF-8286) and host builds.

LTO works together with CONFIG_APP_REPRODUCIBLE_BUILD, but needs extra
flags: LTO defers code generation and most debug-info emission from compile
time to link time, where the reproducible-build path remapping (applied to
compile_options only) does not take effect. When both options are enabled,
three extra flags keep the .elf, .bin and .map byte-identical across build
directories (verified on esp32 / GCC 16.1):

- the -f*-prefix-map options are passed to the linker as well, so the LTO
  code generator remaps DW_AT_comp_dir (otherwise the build dir leaks into
  .debug_str, and cascades into esp_app_desc_t.app_elf_sha256 in the .bin);
- -save-temps makes lto-wrapper use stable LTRANS object names in the build
  dir instead of random $TMPDIR paths that leak into the .map;
- -frandom-seed=1 makes LTO GIMPLE bytecode objects byte-identical (a
  shared seed was verified not to collide, including for C++ file-local
  static variables and anonymous namespaces promoted by LTO).

The gcc-ar / gcc-ranlib wrappers are selected in the GCC toolchain file so
that the LTO plugin is loaded when creating and indexing static archives;
plain ar/ranlib do not record LTO symbols in the archive index.

Note: LTO, like other inlining, can also increase binary size. Enable it
together with CONFIG_COMPILER_OPTIMIZATION_SIZE to get a code-size benefit.

Related: IDF-71, IDF-8286

Closes https://github.com/espressif/esp-idf/issues/18741

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 18:59:46 +02:00

116 lines
6.3 KiB
CMake

include(${CMAKE_CURRENT_LIST_DIR}/toolchain_flags.cmake)
include($ENV{IDF_PATH}/tools/cmake/deduplicate_flags.cmake)
if(NOT CMAKE_PARENT_LIST_FILE)
message(FATAL_ERROR "toolchain.cmake cannot be used standalone (use chip-specific toolchain file instead)")
endif()
# Paths normalization
get_filename_component(_idf_toolchain_dir "$ENV{IDF_PATH}/tools/cmake" REALPATH)
file(TO_CMAKE_PATH "${_idf_toolchain_dir}" _idf_toolchain_dir)
get_filename_component(_toolchain_filename "${CMAKE_TOOLCHAIN_FILE}" NAME)
get_filename_component(_current_toolchain_dir "${CMAKE_TOOLCHAIN_FILE}" DIRECTORY REALPATH)
file(TO_CMAKE_PATH "${_current_toolchain_dir}" _current_toolchain_dir)
set(CMAKE_SYSTEM_NAME Generic)
# Windows CreateProcess enforces a short command line (~32k). IDF compile lines can be
# huge (includes, reproducible-build prefix maps, etc.). Tell the Ninja generator to
# pass compiler arguments via response files (CMake 3.15+). This is orthogonal to the
# early-stage @cflags/@cxxflags response files below, which only cover CMAKE_*_FLAGS.
if(CMAKE_HOST_WIN32 AND CMAKE_GENERATOR MATCHES "Ninja"
AND NOT DEFINED CACHE{CMAKE_NINJA_FORCE_RESPONSE_FILE})
set(CMAKE_NINJA_FORCE_RESPONSE_FILE ON CACHE BOOL
"Use Ninja response files on Windows hosts (avoid CreateProcess command-line limit).")
endif()
# Set compiler tools according to the toolchain type
string(FIND "${_toolchain_filename}" "clang" found_clang)
if(NOT found_clang EQUAL -1)
set(IDF_TOOLCHAIN "clang" CACHE STRING "IDF Build Toolchain Type" FORCE)
set(CMAKE_C_COMPILER clang)
set(CMAKE_CXX_COMPILER clang++)
set(CMAKE_ASM_COMPILER clang)
set(CMAKE_LINKER ${_CMAKE_TOOLCHAIN_PREFIX}clang-ld)
set(CMAKE_AR llvm-ar)
set(CMAKE_RANLIB llvm-ranlib)
set(CMAKE_OBJDUMP ${_CMAKE_TOOLCHAIN_PREFIX}clang-objdump)
else()
set(IDF_TOOLCHAIN "gcc" CACHE STRING "IDF Build Toolchain Type" FORCE)
set(CMAKE_C_COMPILER ${_CMAKE_TOOLCHAIN_PREFIX}gcc)
set(CMAKE_CXX_COMPILER ${_CMAKE_TOOLCHAIN_PREFIX}g++)
set(CMAKE_ASM_COMPILER ${_CMAKE_TOOLCHAIN_PREFIX}gcc)
# Use the gcc-ar/gcc-ranlib wrappers so that the LTO plugin is loaded when
# creating and indexing static archives. This is required for link-time
# optimization (CONFIG_COMPILER_LTO_LINKTIME) to work across static libraries;
# plain ar/ranlib do not record the LTO symbols in the archive index.
set(CMAKE_AR ${_CMAKE_TOOLCHAIN_PREFIX}gcc-ar)
set(CMAKE_RANLIB ${_CMAKE_TOOLCHAIN_PREFIX}gcc-ranlib)
endif()
# Handle different execution contexts for the toolchain file.
# CMake may execute this toolchain file in different contexts:
#
# 1. First execution (IDF project build):
# When CMAKE_TOOLCHAIN_FILE points to IDF sources, the toolchain file is
# executed for the main project. In this case, we create the response files
# directory in the build directory, copy the toolchain file there, initialize
# the response files, and update CMAKE_TOOLCHAIN_FILE to point to the copy.
#
# 2. Subsequent executions (External project builds):
# When CMAKE_TOOLCHAIN_FILE points to a copied toolchain file (from a
# previous root project build), the response files already exist in the same
# directory as the toolchain file. We simply set IDF_TOOLCHAIN_BUILD_DIR to
# point to that existing directory.
if(_idf_toolchain_dir STREQUAL _current_toolchain_dir)
set(IDF_TOOLCHAIN_BUILD_DIR "${CMAKE_BINARY_DIR}/toolchain"
CACHE PATH "Path to toolchain build directory containing response files and toolchain file copy" FORCE)
# Copy toolchain file into the build directory and update CMAKE_TOOLCHAIN_FILE
# to point to the copy. This approach allows us to avoid worrying about different
# CMAKE_BINARY_DIR values between base IDF-project builds and external projects.
# For external project builds, compiler response files are located in the same
# directory as CMAKE_TOOLCHAIN_FILE, making them easy to find.
file(MAKE_DIRECTORY "${IDF_TOOLCHAIN_BUILD_DIR}")
file(COPY "${CMAKE_TOOLCHAIN_FILE}" DESTINATION "${IDF_TOOLCHAIN_BUILD_DIR}")
set(CMAKE_TOOLCHAIN_FILE "${IDF_TOOLCHAIN_BUILD_DIR}/${_toolchain_filename}")
# Create response files before CMake performs compiler checks.
# These files are required for the compiler detection process to succeed.
# The files are created even when the flag variables are empty, ensuring
# they exist when referenced by CMAKE_*_FLAGS variables below.
idf_toolchain_add_flags(C_COMPILE_OPTIONS "${CMAKE_C_FLAGS}"
CXX_COMPILE_OPTIONS "${CMAKE_CXX_FLAGS}"
ASM_COMPILE_OPTIONS "${CMAKE_ASM_FLAGS}"
LINK_OPTIONS "${CMAKE_EXE_LINKER_FLAGS}")
# Clear CMAKE_*_FLAGS because all flags are written to response files.
set(CMAKE_C_FLAGS "")
set(CMAKE_CXX_FLAGS "")
set(CMAKE_ASM_FLAGS "")
set(CMAKE_EXE_LINKER_FLAGS "")
else()
set(IDF_TOOLCHAIN_BUILD_DIR "${_current_toolchain_dir}"
CACHE PATH "Path to toolchain build directory containing response files and toolchain file copy" FORCE)
endif()
# Merge the response file path with any existing CMAKE_*_FLAGS (e.g. from
# ExternalProject_Add), then remove duplicates. Deduplication is needed because
# CMake may execute this toolchain file multiple times during initialization.
remove_duplicated_flags("@\"${IDF_TOOLCHAIN_BUILD_DIR}/cflags\" ${CMAKE_C_FLAGS}" CMAKE_C_FLAGS)
remove_duplicated_flags("@\"${IDF_TOOLCHAIN_BUILD_DIR}/cxxflags\" ${CMAKE_CXX_FLAGS}" CMAKE_CXX_FLAGS)
remove_duplicated_flags("@\"${IDF_TOOLCHAIN_BUILD_DIR}/asmflags\" ${CMAKE_ASM_FLAGS}" CMAKE_ASM_FLAGS)
remove_duplicated_flags("@\"${IDF_TOOLCHAIN_BUILD_DIR}/ldflags\" ${CMAKE_EXE_LINKER_FLAGS}" CMAKE_EXE_LINKER_FLAGS)
# Configure CMake to use response files for compiler and linker flags.
# Some compilation options enabled by IDF configuration options are not yet
# defined at this very early CMake stage (toolchain.cmake execution). Response
# files allow these flags to be dynamically updated during the CMake configuration
# phase, after the options become available.
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS}" CACHE STRING "C Compiler Base Flags" FORCE)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}" CACHE STRING "C++ Compiler Base Flags" FORCE)
set(CMAKE_ASM_FLAGS "${CMAKE_ASM_FLAGS}" CACHE STRING "Asm Compiler Base Flags" FORCE)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}" CACHE STRING "Linker Base Flags" FORCE)