Cross-Compilation and Toolchain Files
Learn to build for different operating systems and architectures using CMake's cross-compilation support and toolchain files.
We've now written CMake scripts that can adapt to different build configurations, operating systems, and hardware architectures. In all these cases, we've made an implicit assumption: the machine we are building on is the same type of machine we are building for.
But what if they're different? What if you want to use your powerful macOS desktop to build an application for a low-power embedded Linux device? This is the world of cross-compilation.
This lesson will introduce you to CMake's framework for cross-compilation. Most importantly, you'll learn the concept of a toolchain file, the standard mechanism for telling CMake everything it needs to know about the target platform you want to build for.
An Overview of Cross-Compiling
Cross-compilation is the process of using a compiler on one system to generate executable code for a different system. To talk about this, we need to be precise with our terminology:
- The Host is the machine where the build is performed. This is your development machine.
- The Target is the machine where the compiled code will eventually run.

You need to cross-compile in many common scenarios:
- Embedded Development: Building code for microcontrollers (like an Arduino) or single-board computers (like a Raspberry Pi) from a desktop PC.
- Mobile and Games Development: For example, building iOS/Android apps or console games from a Windows/Linux/Mac machine.
- Cross-OS Development: Building a Windows application from a Linux machine for your continuous integration server.
As we covered in the , compiling for a different architecture on the same OS (e.g., building a 32-bit app on a 64-bit machine) is technically a form of cross-compilation.
However, it's often a simpler case, as the compilers and system libraries for both architectures are usually installed side-by-side. In this lesson, we'll focus on the more complex case of targeting a completely different operating system. Later in the lesson, we'll walk through a practical example of creating a program targetting Windows from a macOS host, but let's first understand what a toolchain file is.
The Role of a Toolchain File
When you run cmake
without any special arguments, it probes the host system to figure everything out. It finds the default compiler, checks the OS name, and determines the architecture. This is fine for a native build, but for a cross-build, all of this information is wrong.
We don't want to use the host's compile; we want to use the cross-compiler for the target. We don't care that the host is macOS; we need to build for Windows. To solve this, we must explicitly tell CMake about the target system. The standard way to do this is with a toolchain file.
A toolchain file is a separate script, with a .cmake
extension, that you pass to CMake during the configure step. This file's job is to override CMake's default assumptions and "re-wire" it to understand the target platform. It sets a series of special CMAKE_
variables that define the target system's name, its processor, and, most importantly, which compiler to use.
Recommended Tools
To cross-compile, you first need a cross-compiler. This is a special version of a compiler (like GCC or Clang) that runs on your host but produces binaries for your target. Acquiring these toolchains is the first step. There are a few options for most combinations, but some recommendations are below:
Host Platform | Target Platform | Recommended Tool(s) |
---|---|---|
Any (Linux, macOS, Windows) | Linux (ARM, x86) | Dockcross provides Docker images with pre-built cross-compilers. |
macOS / Linux | Windows | MinGW-w64 is a port of GCC that runs on Unix-like systems and produces Windows executables. |
Windows | Linux | Windows Subsystem for Linux (WSL) with a native Linux GCC/Clang toolchain installed. |
Any | Embedded (ARM) | The chip vendor (e.g., ARM, Espressif) typically provides an official cross-compiling toolchain. |
Any | WebAssembly | Emscripten compiles C and C++ to WebAssembly for execution in web browsers |
Anatomy of a Toolchain File
A toolchain file is just a CMake script that sets several key variables. Let's look at the most important ones.
System Name
This is the most critical variable. It tells CMake the name of the target operating system. This is what makes if(WIN32)
or if(UNIX)
work correctly in the rest of the build scripts. If you don't set this, CMake assumes you're doing a native build. The list of system names that are known to CMake by default is available in the official documentation.
set(CMAKE_SYSTEM_NAME "Windows")
System Processor
This specifies the target CPU architecture. You can check what the supported values are in your cross compiler's documentation, but common options are x86_64
, arm
, aarch64
, etc.
set(CMAKE_SYSTEM_PROCESSOR "x86_64")
C and C++ Compiler
These must be the full, absolute paths to the cross-compiler executables for C and C++. CMake will use these compilers instead of the ones it would normally find on the host system.
set(CMAKE_C_COMPILER /path/to/cross-gcc)
set(CMAKE_CXX_COMPILER /path/to/cross-g++)
The value you set here will depend on the cross-compiler, and should be available in the documentation.
System Version
With some cross-compilers, we'll also need to set the CMAKE_SYSTEM_VERSION
variable. The value we set here depends on which compiler we're using, and should be explained in the documentation.
set(CMAKE_SYSTEM_VERSION 10.0)
Root Path for find
Commands
The next set of variables relate to topics we haven't covered, so don't worry if these sections don't entirely make sense yet. They relate to the find_*
family of commands, such as find_library()
, which we cover later in the course. These commands are how our project locates dependencies that are not built as part of your project but are expected to be already installed on the system.
When you're doing a native build, these commands search standard system locations on your host. However, in a cross-compilation scenario, this wouldn't work - it would find the host's version of a library, which will be incompatible with the target we're building for.
To address this, we set the CMAKE_FIND_ROOT_PATH
variable from our toolchain file. This controls where these find_*
commands search for things, ensuring that they find the target-compatible versions that you installed alongside your cross-compiler.
set(CMAKE_FIND_ROOT_PATH /path/to/target/sdk)
What we should set this value to depends on the cross compilation toolchain we're using, so we should check the documentation.
In some cases, we don't need to set this at all, as some toolchains are "integrated". This means they come bundled with the libraries and headers they need for their target, so they inherently know where to find them.
This is the case for MinGW-w64 so, if you want to follow along with our macOS-to-Windows example in the next section, you don't need to set the CMAKE_FIND_ROOT_PATH
manually.
Find Root Path Modes
Finally, we have a few variables that control when CMake uses our CMAKE_FIND_ROOT_PATH
override. The values shown here are pretty standard and used in the vast majority of toolchain files:
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
Let's walk through what each of them mean.
The CMAKE_FIND_ROOT_PATH_MODE_PROGRAM
variable relates to any secondary programs we need to run as part of our build process. This might include things like code analysis tools or documentation generators. Setting this value to NEVER
tells CMake "when looking for an executable program, NEVER search in the directories specified by CMAKE_FIND_ROOT_PATH
." Any programs in that location are designed for the target platform, and probably can't be run from our host.
The CMAKE_FIND_ROOT_PATH_MODE_LIBRARY
variable relates to libraries we may need to find and link against. We only ever want to link against libraries that are built for our target platform, so we should ONLY
search within the CMAKE_FIND_ROOT_PATH
. A library found in any other location would likely be designed for our host platform, and therefore be incompatible for our target.
The CMAKE_FIND_ROOT_PATH_MODE_INCLUDE
variable relates to header files and include directories. Again, we only ever want to use versions compatible with our target platform, so we should ONLY
search the CMAKE_FIND_ROOT_PATH
.
In summary:
When looking for... | Search in... | Because... |
---|---|---|
Programs (Executables) | The Host System (NEVER search target) | Build tools must run on the machine you're building on. |
Libraries (.lib , .so ) | The Target System (ONLY search target) | Your code must link against libraries built for the target. |
Includes (Headers) | The Target System (ONLY search target) | Your code must compile against headers that match the target libraries. |
In the rare circumstances where we want both the host and target locations to be searched, we'd set the value to BOTH
.
Again, don't worry if this is a little abstract and doesn't entirely make sense right now. We have a dedicated chapter on external dependencies and find_*
commands later in the course, which will help contextualise these configuration options with more concrete examples.
Practical Example: macOS to Windows
Let's use this toolchain file to cross-compile our Greeter
project from macOS to Windows.
Install the Cross-Compiler
On macOS, we can install the MinGW-w64 toolchain from their official site or, if you use Homebrew, by running the command:
brew install mingw-w64
Example Toolchain File
Here is a simple but complete toolchain file for cross-compiling to 64-bit Windows using the MinGW-w64 cross-compiler.
toolchains/mingw-windows-x64.cmake
# 1. Set the target system
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
# 2. Specify the cross-compilers
set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)
# 3. Configure the search behavior for external dependencies
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
The values we used here came from the official documentation of the cross-compiler we used. This configuration relies on the compilers (CMAKE_C_COMPILER
and CMAKE_CXX_COMPILER
) being discoverable by CMake, in just the same way the native compilers are.
This may work by default, or may require updating the system paths in the same way we covered when installing compilers back at the start of the course.
Alternatively, these variables can contain the full path to where the compilers are in our system. If you used Homebrew, you can get the installation directory using the brew info
command:
brew info mingw-w64
==> mingw-w64: stable 13.0.0 (bottled)
Minimalist GNU for Windows and GCC cross-compilers
https://sourceforge.net/projects/mingw-w64/
Installed
/opt/homebrew/Cellar/mingw-w64/13.0.0 (8,382 files, 1.3GB) *
The compiler binaries are within the /bin
directory in that location:
set(
CMAKE_C_COMPILER
/opt/homebrew/Cellar/mingw-w64/13.0.0/bin/x86_64-w64-mingw32-gcc
)
set(
CMAKE_CXX_COMPILER
/opt/homebrew/Cellar/mingw-w64/13.0.0/bin/x86_64-w64-mingw32-g++
)
Creating a Minimalist Windows Application
Before we cross-compile our application, let's update our code to a minimalist Windows application. Using the Windows API is beyond the scope of this course, but a minimalist example program would look something like this:
app/src/main.cpp
#include <windows.h>
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
MessageBox(
nullptr,
"I was cross-compiled from a Mac!",
"Hello Windows",
MB_OK
);
return 0;
}
Activating a Toolchain File
To specify the toolchain file we want to use during the cmake
configure step, we set the CMAKE_TOOLCHAIN_FILE
cache variable. This variable must be set on the very first configuration of a new, clean build directory.
CMake uses the toolchain file to perform its initial compiler checks and save the results to the cache. If you try to change the toolchain file on an existing build directory, CMake will ignore it, as the compiler information is already cached.
If you need to switch toolchains, you should delete your build directory and start fresh.
From a clean build directory, let's run the configure command, and then build the generated project:
cmake -DCMAKE_TOOLCHAIN_FILE=../toolchains/mingw-windows-x64.cmake ..
cmake --build .
If you inspect the build/app
directory, you'll see GreeterApp.exe
. To prove it's a Windows executable, we can use the file
command on macOS/Linux.
file ./app/GreeterApp.exe
./app/GreeterApp.exe: PE32+ executable (console) x86-64, for MS Windows
The output should confirm we've created a "PE32+ executable... for MS Windows". If we copy this executable to a Windows machine (or run it in a Windows emulator such as Wine) it would run and show our message box:

Summary
Cross-compilation is a feature that allows you to target a wide variety of platforms from a single development environment. CMake's toolchain file mechanism provides a standardized, portable way to manage this complexity.
- Host vs. Target: The host is where you build; the target is where the code runs.
- Toolchain Files: A
.cmake
script passed viaDCMAKE_TOOLCHAIN_FILE=...
that tells CMake about the target system. - Essential Variables: A toolchain file must set
CMAKE_SYSTEM_NAME
, theCMAKE_..._COMPILER
paths, and correctly configureCMAKE_FIND_ROOT_PATH
and associated variables to isolate the target environment. - Abstraction: By using toolchain files, your main
CMakeLists.txt
can remain largely unaware of the cross-compilation process. It can continue usingif(WIN32)
and other checks, which will correctly evaluate for the target system defined by the toolchain.