Toolchain Overview
Cross-compilation means building code on one platform (the host) to run on a different platform (the target). CMake handles this through toolchain files that tell CMake which compilers, linkers, and search paths to use for the target system.
CMAKE_TOOLCHAIN_FILE
Specify the toolchain file at configure time:
# Pass toolchain file during configuration
cmake -S . -B build-arm \
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/arm-none-eabi.cmake
# The toolchain file is cached — subsequent cmake invocations remember it
cmake --build build-arm
System Variables
Key variables that a toolchain file must set (see CMake cross-compiling docs):
| Variable | Purpose | Example |
|---|---|---|
CMAKE_SYSTEM_NAME | Target OS | Linux, Windows, Generic (bare-metal) |
CMAKE_SYSTEM_PROCESSOR | Target CPU architecture | arm, aarch64, x86_64 |
CMAKE_C_COMPILER | C cross-compiler path | arm-none-eabi-gcc |
CMAKE_CXX_COMPILER | C++ cross-compiler path | arm-none-eabi-g++ |
CMAKE_SYSROOT | Target system root filesystem | /opt/sysroot-armhf |
CMAKE_FIND_ROOT_PATH | Search prefix for target libraries | /opt/arm-libs |
flowchart LR
subgraph Host["Host Machine (x86_64 Linux)"]
A[CMakeLists.txt]
B[arm-none-eabi-gcc]
C[cmake + ninja]
end
subgraph Target["Target Device (ARM Cortex-M4)"]
D[firmware.elf]
E[ARM CPU]
end
A -->|toolchain file| B
B -->|cross-compiles| D
D -->|flashed to| E
C -->|orchestrates| B
Writing Toolchain Files
Basic Toolchain Structure
A toolchain file for cross-compiling to Linux on ARM64 (e.g., Raspberry Pi):
# cmake/toolchains/aarch64-linux-gnu.cmake
# Cross-compile for ARM64 Linux (Raspberry Pi, Jetson, etc.)
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
# Cross-compiler toolchain
set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++)
# Sysroot containing target libraries and headers
set(CMAKE_SYSROOT /opt/sysroot-aarch64)
# Search paths for target libraries
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
# Search policies:
# ONLY = search only in sysroot/find_root_path
# NEVER = search only on host
# BOTH = search both
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) # Use host tools (python, etc.)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) # Use target libraries
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) # Use target headers
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) # Use target CMake packages
ARM Bare-Metal Toolchain
For embedded targets without an OS (bare-metal), use CMAKE_SYSTEM_NAME Generic:
# cmake/toolchains/arm-none-eabi.cmake
# Bare-metal ARM Cortex-M cross-compilation
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm)
# ARM bare-metal toolchain (no OS, no libc by default)
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
set(CMAKE_ASM_COMPILER arm-none-eabi-gcc)
# Utility tools
set(CMAKE_OBJCOPY arm-none-eabi-objcopy)
set(CMAKE_OBJDUMP arm-none-eabi-objdump)
set(CMAKE_SIZE arm-none-eabi-size)
# Cortex-M4 with hardware FPU
set(CPU_FLAGS "-mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard")
set(CMAKE_C_FLAGS_INIT "${CPU_FLAGS}")
set(CMAKE_CXX_FLAGS_INIT "${CPU_FLAGS}")
set(CMAKE_ASM_FLAGS_INIT "${CPU_FLAGS}")
# Linker flags: use newlib-nano, no standard startup
set(CMAKE_EXE_LINKER_FLAGS_INIT
"--specs=nosys.specs --specs=nano.specs -Wl,--gc-sections")
# Disable try_run (can't execute target binaries on host)
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
# Cross-compile for ARM Cortex-M4
cmake -S . -B build-arm \
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/arm-none-eabi.cmake \
-DCMAKE_BUILD_TYPE=MinSizeRel
cmake --build build-arm
# Convert ELF to flashable binary
arm-none-eabi-objcopy -O binary build-arm/firmware.elf firmware.bin
Cross-Compile a Blinky Firmware
# CMakeLists.txt for STM32 bare-metal
cmake_minimum_required(VERSION 3.21)
project(Blinky LANGUAGES C ASM)
set(LINKER_SCRIPT ${CMAKE_SOURCE_DIR}/STM32F411RE.ld)
add_executable(firmware
src/main.c
src/startup_stm32f411xe.s
src/system_stm32f4xx.c
)
target_include_directories(firmware PRIVATE include)
target_link_options(firmware PRIVATE -T${LINKER_SCRIPT} -Wl,-Map=firmware.map)
# Post-build: print size and generate .hex
add_custom_command(TARGET firmware POST_BUILD
COMMAND ${CMAKE_SIZE} firmware
COMMAND ${CMAKE_OBJCOPY} -O ihex firmware firmware.hex
)
cmake -S . -B build \
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/arm-none-eabi.cmake
cmake --build build
# Output: firmware.hex ready to flash
FIND Policies
CMAKE_FIND_ROOT_PATH
The CMAKE_FIND_ROOT_PATH tells CMake where to look for target-platform libraries and headers:
# Multiple search paths (searched in order)
set(CMAKE_FIND_ROOT_PATH
${CMAKE_SYSROOT}
/opt/cross-libs/arm
${CMAKE_CURRENT_SOURCE_DIR}/third_party/arm
)
Search Mode Policies
Control where find_library(), find_path(), and find_package() search:
# NEVER — Don't search FIND_ROOT_PATH (use for host tools)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
# ONLY — Search ONLY within FIND_ROOT_PATH (use for target libs)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
# BOTH — Search both host and target paths
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH)
Sysroot Configuration
A sysroot is a directory tree containing the target's entire root filesystem (headers, libraries, pkg-config files):
# Point to the target's root filesystem
set(CMAKE_SYSROOT /opt/sysroots/cortexa53-poky-linux)
# The compiler will automatically use:
# - ${CMAKE_SYSROOT}/usr/include for headers
# - ${CMAKE_SYSROOT}/usr/lib for libraries
# Equivalent to: --sysroot=/opt/sysroots/cortexa53-poky-linux
# Create sysroot from a Raspberry Pi SD card
sudo rsync -avz pi@raspberrypi:/{lib,usr} /opt/sysroot-rpi/
# Or extract from a Docker image
docker create --name temp arm64v8/ubuntu:22.04
docker cp temp:/usr /opt/sysroot-arm64/usr
docker cp temp:/lib /opt/sysroot-arm64/lib
docker rm temp
try_compile and try_run in Cross-Compilation
The try_compile() command works during cross-compilation (the cross-compiler can compile). But try_run() fails because you can't execute target binaries on the host. CMake provides workarounds:
# Option 1: Skip try_run by setting result variables in toolchain
set(HAVE_CLOCK_GETTIME_EXITCODE 0 CACHE STRING "")
set(HAVE_CLOCK_GETTIME_EXITCODE__TRYRUN_OUTPUT "" CACHE STRING "")
# Option 2: Disable try_compile entirely for static checks
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
# Option 3: Use an emulator (see next section)
Cross-Compilation Emulator
The CMAKE_CROSSCOMPILING_EMULATOR variable lets CMake run target binaries through an emulator (QEMU, Wine) during try_run() and testing:
# In toolchain file: use QEMU to run ARM binaries
set(CMAKE_CROSSCOMPILING_EMULATOR
"qemu-arm;-L;${CMAKE_SYSROOT}"
)
# Now try_run() and ctest work!
# CMake will prefix every executable invocation with:
# qemu-arm -L /opt/sysroot-arm <binary>
# Install QEMU user-mode emulation
sudo apt install qemu-user qemu-user-static
# Verify it works
qemu-arm -L /opt/sysroot-arm ./build-arm/my_test
# Runs ARM binary on x86_64 host via emulation
Run Cross-Compiled Tests with QEMU
# Toolchain excerpt with emulator
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)
set(CMAKE_SYSROOT /opt/sysroot-armhf)
set(CMAKE_CROSSCOMPILING_EMULATOR "qemu-arm;-L;${CMAKE_SYSROOT}")
# Now in CMakeLists.txt, tests work normally:
# add_test(NAME unit_tests COMMAND my_tests)
# ctest runs: qemu-arm -L /opt/sysroot-armhf ./my_tests
cmake -S . -B build-arm \
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/arm-linux-qemu.cmake
cmake --build build-arm
cd build-arm && ctest --output-on-failure
# Tests execute via QEMU emulation
Integrating Toolchains with Presets
Combine toolchain files with CMake presets for the best developer experience:
{
"version": 6,
"configurePresets": [
{
"name": "host-debug",
"displayName": "Host Debug (native)",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/host-debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "arm-cortex-m4",
"displayName": "ARM Cortex-M4 (bare-metal)",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/arm-cm4",
"toolchainFile": "${sourceDir}/cmake/toolchains/arm-none-eabi.cmake",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "MinSizeRel",
"TARGET_MCU": "STM32F411RE"
}
},
{
"name": "arm-linux",
"displayName": "ARM Linux (Raspberry Pi)",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/arm-linux",
"toolchainFile": "${sourceDir}/cmake/toolchains/aarch64-linux-gnu.cmake",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
}
],
"buildPresets": [
{"name": "host-debug", "configurePreset": "host-debug"},
{"name": "arm-cortex-m4", "configurePreset": "arm-cortex-m4"},
{"name": "arm-linux", "configurePreset": "arm-linux"}
]
}
# List all presets (including cross-compilation)
cmake --list-presets
# Cross-compile for ARM bare-metal
cmake --preset arm-cortex-m4
cmake --build --preset arm-cortex-m4
# Cross-compile for Raspberry Pi
cmake --preset arm-linux
cmake --build --preset arm-linux
flowchart TD
A[CMakePresets.json] --> B[host-debug]
A --> C[arm-cortex-m4]
A --> D[arm-linux]
B -->|"native compiler"| E[build/host-debug/]
C -->|"arm-none-eabi-gcc"| F[build/arm-cm4/]
D -->|"aarch64-linux-gnu-gcc"| G[build/arm-linux/]
E --> H[x86_64 binary]
F --> I[firmware.elf]
G --> J[ARM64 Linux binary]
Build for Host + Two Cross-Targets
# Build all targets from presets
cmake --preset host-debug && cmake --build --preset host-debug
cmake --preset arm-cortex-m4 && cmake --build --preset arm-cortex-m4
cmake --preset arm-linux && cmake --build --preset arm-linux
# Verify binaries
file build/host-debug/app # ELF 64-bit x86-64
file build/arm-cm4/firmware.elf # ELF 32-bit ARM, EABI5
file build/arm-linux/app # ELF 64-bit ARM aarch64
Conclusion & Next Steps
Cross-compilation with CMake is powerful once you understand the toolchain file model. Key takeaways:
- Toolchain files run before
project()— they set up the entire cross-compilation environment CMAKE_SYSTEM_NAME Genericfor bare-metal; actual OS name for hosted targets- FIND policies (
NEVER/ONLY/BOTH) prevent host/target library confusion CMAKE_CROSSCOMPILING_EMULATORenables testing via QEMU without hardware- Combine toolchains with CMake presets for one-command cross-builds