The CMake logo, licensed under CC BY 2.0

Toolchain file in CMake

CMake supports toolchain files, but from the documentation 🗄️ it is not very clear how to use those, and how they can be helpful.

The main use case for a toolchain file is for defining an environment with everything that is strictly required for creating a binary for another platform.

Toolchain files give a lot of flexibility, and they can be used for many more things.

Not only for cross-compiling

Toolchain files can be used also for compiling for the host platform, not only cross-compiling.

This might not seem extremely useful, but it can help to simplify the development process.

In fact, most of the time I am using a toolchain file it is not for cross-compiling.

Define CMake variables

A toolchain is simply a CMake file that defines a set of variables, which are used for determining how to create a library or executable

A minimal exmaple 🗄️ could look like the following one:

set(CMAKE_C_COMPILER gcc-11)
set(CMAKE_CXX_COMPILER g++11)

This toolchain file can be used with the --toolchain flag when generating a project, for example

cmake --toolchain=gcc11-toolchain.txt -Ssource -Bbuild
Note 📝
an empty toolchain file would be valid too, but is generally not that much useful.

With a minimal project, it is possible to see that CMake can determine by itself the compiler version number and ID:

project(test)

message("C compiler is ${CMAKE_C_COMPILER}")
message("C++ compiler is ${CMAKE_CXX_COMPILER}")
message("Compiler id is ${CMAKE_CXX_COMPILER_ID}")
message("Compiler version is ${CMAKE_CXX_COMPILER_VERSION}")

add_executable(test main.cpp)

Such a minimal toolchain file can simply be replaced by setting the compiler with command-line arguments:

cmake -DCMAKE_C_COMPILER=/usr/bin/gcc-11 -DCMAKE_CXX_COMPILER=/usr/bin/g++-11 -Ssource -Bbuild

or by using environment variables

CC=/usr/bin/gcc-11 CXX=/usr/bin/g++-11 cmake -DCMAKE_C_COMPILER=/usr/bin/gcc-11 -DCMAKE_CXX_COMPILER=/usr/bin/g++-11 -Ssource -Bbuild

Note that CC (and CXX) can contain parameters 🗄️.

Another alternative would be setting the compiler in the CMakeLists.txt directly

Note 📝
Always try to avoid defining the compiler in your project. It is a (big) feature being able to pick a code base, not change anything, and build it with a (possibly slightly different) compiler installed on another system.
set(CMAKE_C_COMPILER gcc-11)
set(CMAKE_CXX_COMPILER g++11)

project(test)

message("C compiler is       ${CMAKE_C_COMPILER}")
message("C++ compiler is     ${CMAKE_CXX_COMPILER}")
message("Compiler id is      ${CMAKE_CXX_COMPILER_ID}")
message("Compiler version is ${CMAKE_CXX_COMPILER_VERSION}")

add_executable(test main.cpp)
Warning ⚠️
You need to set CMAKE_C_COMPILER and CMAKE_CXX_COMPILER before the first call to project or enable_language.

Define default compiler flags

CMake uses a set of variables 🗄️ for initializing the default compiler flags, as specifying the compiler alone, is generally not sufficient for creating a working executable.

Suppose, for example, that for the desired platform, it is necessary to use -funsigned-char.

set(CMAKE_C_COMPILER /usr/bin/gcc-12)
set(CMAKE_CXX_COMPILER /usr/bin/g++-12)

set(CMAKE_CXX_FLAGS_INIT "-funsigned-char")

Also, those values can be queried

project(test)


message("C compiler is       ${CMAKE_C_COMPILER}")
message("C++ compiler is     ${CMAKE_CXX_COMPILER}")
message("Compiler id is      ${CMAKE_CXX_COMPILER_ID}")
message("Compiler version is ${CMAKE_CXX_COMPILER_VERSION}")

message("CMAKE_CXX_FLAGS                ${CMAKE_CXX_FLAGS}")
message("CMAKE_CXX_FLAGS_DEBUG          ${CMAKE_CXX_FLAGS_DEBUG}")
message("CMAKE_CXX_FLAGS_MINSIZEREL     ${CMAKE_CXX_FLAGS_MINSIZEREL}")
message("CMAKE_CXX_FLAGS_RELEASE        ${CMAKE_CXX_FLAGS_RELEASE}")
message("CMAKE_CXX_FLAGS_RELWITHDEBINFO ${CMAKE_CXX_FLAGS_RELWITHDEBINFO}")

add_executable(test main.cpp)

Note that, for example, CMAKE_CXX_FLAGS_DEBUG does not contain the values defined in CMAKE_CXX_FLAGS. When CMake invokes the compiler, the two variables are "merged" together. This can be observed by defining the environment variable VERBOSE=1 when building, for example:

rm -rf build 2>/dev/null;
cmake --toolchain=./gcc-11.toolchain -Ssource -Bbuild
VERBOSE=1 cmake --build build --target test --config Debug

Define compiler diagnostic in a toolchain file

Just like it is possible to define compiler flags that are relevant for creating a working binary, it is possible to define flags that do not affect the generated code, like compiler warnings.

set(CMAKE_C_COMPILER /usr/bin/gcc-12)
set(CMAKE_CXX_COMPILER /usr/bin/g++-12)

set(CMAKE_CXX_FLAGS_INIT "-Wall -Wextra -pedantic")

This makes it easy to define some rules to avoid a particular type of error through static analysis.

For example -Werror=null-dereference helps, where the compiler can determine it at compile-time, to avoid dereferencing a null pointer.

-Werror=uninitialized and -Werror=maybe-uninitialized are two flags for finding uninitialized variables.

Possible drawbacks

As long as the end-user is controlling the toolchain file defining compiler flags that are not strictly necessary, I do not think there are any particular drawbacks.

The main issue is a project defining a toolchain file with such flags.

It is a (big) feature being able to pick a code base, not change anything, and build it with a (possibly slightly different) compiler installed on the system.

— Me

If a project defines some flags, it might break other projects. This is one of the reasons why CMake makes it possible to use target_compile_options 🗄️, and specify PRIVATE and non-PRIVATE flags.

PRIVATE flags are for building the library, they are not used for compiling other targets that depend on it. This makes it the perfect place for adding compiler warnings.

Making -fshort-enum PRIVATE is possible, but asking for trouble. The caller needs to set -fshort-enum manually, which is easy to oversee when handling multiple libraries. For these use-cases, PUBLIC and INTERFACE (or a configuration error) should be preferred.

Using target_compile_options with PRIVATE is cumbersome, especially when developing multiple libraries.

Sometimes it is easier to define the warnings and errors globally, and eventually, disable them on a per-project basis. Trying to fix the offending code so that no changes are necessary when using stricter compiler settings would be a plus, but it is not always possible or easy.

Compose toolchain files

The main advantage of having a separate toolchain file is that it is relatively easy to reuse between projects. Without changing any CMakeLists.txt, it is possible to pass the toolchain file and have a saner development environment.

But what if the project already requires a toolchain file?

In that case, it is possible to chain them together (pun intended).

include(${CMAKE_CURRENT_LIST_DIR}/path/to/official/toolchain)
set(CMAKE_CXX_FLAGS_INIT "-Wall -Wextra -pedantic")

Note the usage of ${CMAKE_CURRENT_LIST_DIR}; avoid using a relative path, ${CMAKE_SOURCE_DIR} or ${CMAKE_HOME_DIRECTORY}. CMake runs some internal tests with some internal projects, so those locations will point to different folders.

${CMAKE_CURRENT_LIST_DIR}, in this context, specifies a location relative to the toolchain file itself 🗄️, so it does not change between those tests.

Alternative to toolchain files

User overrides

The main alternative to toolchain files is user overrides 🗄️. According to the documentation

It is loaded after CMake’s builtin compiler and platform information modules have been loaded but before the information is used

— CMake documentation of CMAKE_USER_MAKE_RULES_OVERRIDE

So it might be a more appropriate decision for defining compiler flags (as those depend on compiler type and version), even if it means that one needs to pass two arguments when cross-compiling

cmake -DCMAKE_USER_MAKE_RULES_OVERRIDE=$(pwd)/default_compiler_flags.txt --toolchain=gcc11-toolchain.txt -Ssource -Bbuild
Note 📝
Contrary to --toolchain the path passed to CMAKE_USER_MAKE_RULES_OVERRIDE needs to be absolute.

Most considerations I’ve written for the toolchain file also hold for the user overrides. It is a file that defines variables interpreted by CMake, and can include other files (again, use ${CMAKE_CURRENT_LIST_DIR}).

Environment variables

For some use cases, environment variables can replace a toolchain or CMAKE_USER_MAKE_RULES_OVERRIDE file.

CFLAGS 🗄️ (and CXXFLAGS 🗄️) can be used for defining flags to pass to the compiler.

The main advantage of using environment variables is that those are also supported by other build systems, in particular make 🗄️, so it seems very practical to have a predefined CFLAGS. In practice, as many flags are compiler-dependent, it is not as practical as having a CMake file that can detect which compiler (and which version) is in use and define variables accordingly.


Do you want to share your opinion? Or is there an error, some parts that are not clear enough?

You can contact me anytime.