Introduction
CMake generator expressions are special expressions that are evaluated during the generation phase of CMake. These expressions are typically used in CMake commands to allow for context-specific decisions, such as setting properties, linking libraries, or generating files.
The main advantage of generator expressions in comparison with the old way of CMake is the support of Multi-Config generators where different builds Debug, Release, and so on coexist in the same build directory. They also make the script cleaner and more readable.
Stages of CMake
We run cmake in a terminal as:
cmake -S /path/to/source -B path/to/build
or if build folder is in the project directory, in a terminal in the build directory, we run:
cmake ..
This command runs two stages of CMake (1)configuration and (2)generation. If you use CMake GUI, you can run configuration and generation separately.
Finally, we build the project, (3) build stage, via
cmake --build . --config Debug
There are more stages like install and packaging.
Configuration stage
During configuration the CMakeLists.txt files in the project are parsed, variables, targets, commands, dependencies are defined and packages are found. Finally, a CMake cache file, CMakeCache.txt
, is created that stores configuration results and user-defined options.
Generation stage
At this stage, build system files are generated compatible with a specific build tool like Makefiles, Ninja files, Visual Studio projects. Also, the necessary build files are created, for example, Makefile, .ninja files, or .sln files for Visual Studio.
Debug
In this post, I explain generator expressions with examples. Because message()
only runs during configuration stage, we cannot use it to print the result of these expressions. To do so, we use a custom target as
cmake_minimum_required(VERSION 3.23)
project(MyDebugExample LANGUAGES CXX)
add_custom_target(printexpr ALL COMMAND ${CMAKE_COMMAND} -E echo $<..>)
where $<...>
is a generator expression. When you build this project, the result of the expression is displayed in the terminal.
If you are interested in how add_custom_target()
works, see my post.
Another way is to use:
file(GENERATE OUTPUT myFileName CONTENT "$<...>")
Some expressions like CXX_COMPILER_ID
are defined only with a binary target, for them use:
file(GENERATE OUTPUT myFileName CONTENT "$<...>" TARGET myTargetName)
Types of Generator Expressions
They fall into several categories:
Conditions
Used to evaluate logical conditions.
$<condition:true_string>
Evaluates to true_string
if condition is 1, or an empty string if condition evaluates to 0. Any other value for condition results in an error.
$<1:book> # evaluates to string book
$<0:book> # evaluates to empty string
$<true:book> # error, only 0 or 1 accepted.
$<BOOL:condition>
Evaluates to 1
if condition
is true, otherwise 0
. We use this to convert false expressions like empty string and case insensitive of FALSE, N, NO, OFF, NOTFound and IGNORE to 0 and otherwise to 1.
All of these evaluate to 0:
$<BOOL:FALSE>
$<BOOL:>
$<BOOL:off>
$<BOOL:something-NOTFOUND> # exception that is case sensitive
Anything that is not false expression evaluates to 1:
$<BOOL:true>
$<BOOL:anyWordExceptFalseOnes>
Expressions can be nested where the result of one injected to another one:
$<$<BOOL:false>:book> # evaluates to empty string
$<$<BOOL:true>:book> # evaluates to string book
$<IF:condition,true-value,false-value>
Returns true-value
if condition
is 1; if 0, returns false-value
.
$<IF:0,IamTrue,IamFalse> # gives IamFalse
$<IF:$<BOOL:TomAndJerry>,IamTrue,IamFalse> # gives IamTrue
Logics
For AND
and OR
, the expressions are $<AND:conditions>
and $<OR:conditions>
. The conditions are comma separated 0 and 1s. For example:
# For AND, result is 1, if all are 1.
$<AND:1,1,1> # evaluates to 1
$<AND:1,1,0> # evaluates to 0
$<AND:1> # evaluates to 1
$<AND:> # Erorr, at least a 0/1 needed.
# For OR, result is 1, if at least one 1 present.
$<OR:1,0,0> # evaluates to 1
$<OR:0,0,0> # evaluates to 0
$<OR:1,1> # evaluates to 1
$<OR:0> # evaluates to 0
# Mixing expressions
$<IF:$<AND:$<BOOL:false>,1>,IamTrue,IamFalse> # gives IamFalse
Also we have $<NOT:condition>
to flip 0 and 1.
$<NOT:1> # evaluates to 0
$<NOT:$<BOOL:OFF>> # evaulates to 1
There are a lot of more useful expressions to help with conditions like STREQUAL
, EQUAL
, LOWER_CASE
and there are many handy ones to work with versions, lists and paths. For them refer to CMake manual.
Configuration
These are used to differentiate settings for build configurations (e.g., Debug, Release).
$<CONFIG>
: Evaluates to the current configuration name.
$<CONFIG:configs>
: 1 if the current configuration matches one of configs
, which is one configuration or a comma separated list of configurations.
# if you compile in Release mode
$<CONFIG> # evaluates to string Release
$<CONFIG:Release> # evaluates to 1
$<CONFIG:Debug> # evaluates to 0
$<CONFIG:RelWithDebInfo> # evaluates to 0
$<CONFIG:Release,RelWithDebInfo> # evaluates to 1
Target
These are used to retrieve or set properties for specific targets.
$<TARGET_FILE:tgt>
Full path to the file produced by target tgt
. In the example below, the executable file of target circle, for example circle.exe on Windows, is copied to the target destination.
add_executable(circle main.cpp)
add_custom_target(collectExes ALL
COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_FILE:circle> path/to/destination)
$<TARGET_PROPERTY:target,property>
Retrieves the value of a property for target
. The example below, prints the the source files of target circle
on screen when you build the project:
add_executable(circle main.cpp)
add_custom_target(printExpr ALL
COMMAND ${CMAKE_COMMAND} -E echo $<TARGET_PROPERTY:circle,SOURCES>)
Platform
These are used to apply settings depending on the operating system or platform.
$<PLATFORM_ID>
: gives the name of operating system. On Windows 11, I get Windows
, on Ubuntu I get Linux
.
$<PLATFORM_ID:platform_ids>
: 1 if the platform matches any of platform_ids which is a comma separated list.
Compiler
Compiler expressions give information about the compiler but they are only valid with a binary target.
$<CXX_COMPILER_ID>
: turns to MSVC, GNU, Clang, and so on.
$<CXX_COMPILER_VERSION>
: For my MSVC, I get 19.39.33523.0.
$<COMPILE_LANGUAGE>
: evaluates to C, CXX, and so on.
There are many other expressions for compiler, language, and linker in the CMake Manual.
Build and Install
For a project usually we consider two scenarios. The project is build and other prjects in the same build use its files in the source/binary tree to be built. There is another scenario, that our project is already built and installed, then another project tries to link to it by reading CMake config files. Generator expressions can help use to differntiate these situations easily.
$<BUILD_LOCAL_INTERFACE:rhs>
This expression evaluates to rhs
when the target is utilized by another target within the same build system. Otherwise, it evaluates to an empty string.
$<INSTALL_INTERFACE:rhs>
This expression evaluates to rhs
if the property is exported using the install(EXPORT)
command. If not, it evaluates to an empty string.
I found the code below from pybind11
project on Github (I slightly modified it):
target_include_directories(
pybind11_headers ${pybind11_system} INTERFACE
$<BUILD_LOCAL_INTERFACE:${pybind11_INCLUDE_DIR}>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>)
pybind11
is a header only project. Therefore, keyword INTERFACE
is used for including directories. Other projects that are in the same buildsystem, use ${pybind11_INCLUDE_DIR}
which is a local directory in the source tree. However, if pybind11 is installed somewhere, ${CMAKE_INSTALL_INCLUDEDIR}
will be the include directory property of installed pybind11.
Example 1
In this example, generator expressions are used to set compile options for a target depending on the compiler (GNU or MSVC) and build type (Debug or Release):
target_compile_options(my_target PRIVATE
$<$<AND:$<CXX_COMPILER_ID:GNU>,$<CONFIG:Debug>>:-Og>
$<$<AND:$<CXX_COMPILER_ID:GNU>,$<CONFIG:Release>>:-O2>
$<$<AND:$<CXX_COMPILER_ID:MSVC>,$<CONFIG:Debug>>:/Od>
$<$<AND:$<CXX_COMPILER_ID:MSVC>,$<CONFIG:Release>>:/O2>
)
We can also define C/C++ preprocessor macro for different configs and compilers:
target_compile_definitions(my_target PRIVATE
$<$<CONFIG:Debug>:DEBUG_MODE>
$<$<CONFIG:Release>:NDEBUG>
$<$<CXX_COMPILER_ID:GNU>:GNU>
$<$<CXX_COMPILER_ID:MSVC>:MSVC>
)
Example 2
In the example below, circle
executable is built which depends on shared library geo
.
The platform is Windows and we’d like to copy geo.dll
file to the same folder that circle.exe
is built in.
By that, we can run circle.exe
in its folder.
add_executable(circle main.cpp)
target_link_libraries(circle geo)
add_custom_command(
TARGET circle
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_FILE:geo> $<TARGET_FILE_DIR:circle>/
)
$<TARGET_FILE:geo>
: the full path togeo.dll
. If you need only the name, geo.dll, use$<TARGET_FILE_NAME:geo>
.$<TARGET_FILE_DIR:circle>
: the directory thatcircle.exe
is built in. Don’t forget the/
after, otherwise it won’t be a successful copy as the desitination can be a file or a directory.