Table of Contents

  1. Toolchain Overview
  2. Writing Toolchain Files
  3. FIND Policies
  4. Sysroot Configuration
  5. try_compile & try_run
  6. Cross-Compilation Emulator
  7. Toolchains with Presets
  8. Conclusion & Next Steps
Back to CMake Mastery Series

Part 25: Cross-Compilation and Toolchains

June 4, 2026 Wasil Zafar 40 min read

Write CMake toolchain files to cross-compile for ARM, embedded targets, and foreign platforms. Configure sysroots, FIND policies, emulators, and integrate toolchains with CMake presets for reproducible cross-builds.

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.

Key Insight: A toolchain file is processed BEFORE the project() command — it sets up the cross-compilation environment before CMake detects compilers. This means you cannot use project variables inside toolchain files, but you can set any CMAKE_* variable that affects compiler detection and package searching.

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):

VariablePurposeExample
CMAKE_SYSTEM_NAMETarget OSLinux, Windows, Generic (bare-metal)
CMAKE_SYSTEM_PROCESSORTarget CPU architecturearm, aarch64, x86_64
CMAKE_C_COMPILERC cross-compiler patharm-none-eabi-gcc
CMAKE_CXX_COMPILERC++ cross-compiler patharm-none-eabi-g++
CMAKE_SYSROOTTarget system root filesystem/opt/sysroot-armhf
CMAKE_FIND_ROOT_PATHSearch prefix for target libraries/opt/arm-libs
Cross-Compilation Architecture
        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
Hands-On ARM Cross-Compilation
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
ARM bare-metal STM32

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)
Critical Rule: Programs (compilers, generators, code formatters) should use NEVER — you want the host's tools. Libraries and headers should use ONLY — you want the target's binaries. Getting this wrong causes "wrong architecture" linker errors.

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
Hands-On QEMU Emulator Integration
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
QEMU emulator ctest

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
Preset-Driven Multi-Target Builds
        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]
    
Hands-On Multi-Target CI Pipeline
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
presets multi-target CI

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 Generic for bare-metal; actual OS name for hosted targets
  • FIND policies (NEVER/ONLY/BOTH) prevent host/target library confusion
  • CMAKE_CROSSCOMPILING_EMULATOR enables testing via QEMU without hardware
  • Combine toolchains with CMake presets for one-command cross-builds
Official Reference: See the comprehensive cmake-toolchains(7) manual for all cross-compilation variables and platform-specific guidance.