In this post I’ll describe the basic structure of generator-expressions and provide some context for their use.
CMake Processing Stages
The first step to understanding generator expressions is to get a solid grasp on the CMake build process. Most of the time this is simplified to two distinct stages:
- configuration – when cmake is executed on the project
- build – when the generated build system is ran in the build directory
These two stages are obvious, as they are two separate actions that the user must take. However, if on closer inspection of cmake’s output during the configuration stage it is apparent that another distinct stage is present – the generation stage:
$ cmake -B build -S .
...
-- Configuring done
-- Generating done
This is the stage where cmake actually generates the specified (or default for the platform, if omitted) build system. As the name might suggest generator-expressions are evaluated during the generation stage. As a result, they can not be used before that – i.e. in the configuration stage.
So the actual cmake build process stages are:
- configuration – can’t use generator-expressions here
- generation – generator-expressions are OK here
- build – generator-expressions are OK here
That’s great, but what does this actually mean in practical terms? With which specific commands can I, or can I not use generator-expressions?
Well, broadly speaking generator-expressions, much like most of modern CMake, work with targets and properties – if a command deals with a target and modifies its properties in some way, chances are you’ll be able to use generator-expressions with that command. Expect generator-expressions to be usable with all of the commands prefixed with target_ (target_include_directories etc.) and commands that operate on properties directly (set_target_properties, etc.). On the other hand if you try using generator-expressions on plain variables you’re bound to have a bad time. There are exceptions to this of course – if a variable is later used by cmake to initialize properties on targets, then you’ll get the desired effect. You’ll develop a feel for it with time, just consult the documentation for specifics as you go.
Now that you know where generator expressions can be used, let’s see how to use them.
Generator Expression Syntax
In its most general form a generator expression is specified with a dollar-sign and a pair of angle brackets:
$<...>
The part between the angle brackets can vary quite widely. Most notably it can be one of the following:
- Conditional expression
- Variable or Target querry
- Output Related expression
Let’s have a brief look at each of these.
Conditional expressions
Most generator-expressions in the wild will contain a conditional component. This is due to the fact that generator-expressions can replace if-else statements in properties context.
A conditional expression boils down to the following form:
$<condition:value-if-true>
Where the condition is an arbitrary expression evaluated to 0 or 1. The result of the entire conditional expression is value-if-true if the condition evaluates to 1, and an empty string otherwise.. This is probably one of the most important properties of generator expression syntax to understand. The other is that generator-expressions can be nested arbitrarily, meaning that both condition and value-if-true can themselves be generator-expressions.
There’s a ton of uses for this. As a rule of thumb everytime you’re about to use an if-else statement consider if a conditional generator-expression wouldn’t be a more succinct and readable alternative. The most common example is specifying compiler or linker flags:
add_library(Foo ...)
target_link_options(Foo
PRIVATE
$<$<CONFIG:Debug>:--coverage>
)
Here the target Foo is linked with additional instrumentation providing code-coverage, but only if the build configuration is equal to Debug.
The above example also shows the mentioned nesting – the conditional part of the expression is in this case a generator-expression itself – $<CONFIG:Debug> – this is a variable-querry conditional expression – it compares the actual configuration the target is being built with (what CONFIG evaluates to) to the value specified after the colon (Debug). If the entire expression is too difficult to unpack at first, just consider these two cases:
If a Debug configuration is being built the nested $<CONFIG:Debug> expression evaluates to 1, thus the outer expression becomes
$<1:--coverage>
which is a simple conditional expression form, described at the start of this paragraph. Since the condition == 1 the entire expression evaluates to “–coverage”.
Now, if a different configuration is being built – Release, for example – the nested $<CONFIG:Debug> evaluates to 0, and the outer expression becomes
$<0:--coverage>
which again is just a simple conditional expression form. Since the condition == 0, the entire expression evaluates to an empty string.
This singly-nested conditional generator expression probably covers most of the use cases you’ll need day-to-day. There are many more conditional queries available, allowing for checking things like the compiler, compiler-version, language the target is being compiled with, and much more. Check out the documentation for a complete list.
Variable and Target querries
In addition to making conditional decisions, generator-expressions also provide convenient variable access. These take the following form:
$<Variable>
In some cases, these may seem redundant, since often a corresponding global CMake variable providing the same value exists. However, don’t forget that generator-expressions are evaluated at generation time and therefore may provide information otherwise inaccurate or unavailable.
By the example of the already mentioned $<CONFIG> expression. One might think that it would be equivalent to use the ${CMAKE_BUILD_TYPE} variable instead. Which would be true for single-configuration generators like make, but will not be true for multi-configuration generators like Ninja Multi-Config or Visual Studio. In case of multi-config generators the configuration is specified when the project is being built, long after configuration time.
As a rule of thumb, I’d suggest using the variable query generator-expressions over simple variables in all contexts which allow generator-expressions.
Similar to the variable query is the target query generator-expression. The syntax is a little different:
$<Query:Target>
Where Query is a specific property we’re querying for, and Target is the subject of the query. This is a little vague, so here’s an example:
$<TARGET_FILE:Foo>
The above generator-expression returns a full path to the binary file resulting from the specified target. This is much more convenient than manually trying to account for all possibilities – different prefixes/suffixes/extensions the file might have, possibly different output directories (e.g. based on the configuration). The generator-expression is very succinct and covers all variations on all platforms with zero effort.
There are many more target-query generator-expressions. Checkout the documentation for the full list. I’ll mention just one more, since the syntax is a little different:
$<TARGET_PROPERTY:target,property>
The above results in the value of the property on the target. This again is very convenient, as it already takes into account all the possible variations, including the ones possibly resulting from other generator expressions.
Output Related Expressions
This is probably the second most commonly used form of generator-expressions. These evaluate to the given value only in specific output-contexts. For example:
add_library(Foo ...)
target_include_directories(Foo
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
In the above example when the target Foo is being built or linked by another target in the same build system, the value of the BUILD_INTERFACE expression is used to provide the include directories. Otherwise, if the target Foo is being installed (e.g. after running make install) the value of the INTSTALL_INTERFACE expression is used. This is a pattern commonly seen in library development.
In addition to the described generator-expression types, there’s also a number of expressions meant for convenient string transformation and comparison. I’ll just mention briefly that it’s possible to convert strings to upper-case, lower-case, join, filter, and more. I’d encourage you to check out the documentation once you’re comfortable with the basic use of generator-expressions.
Debugging generator-expressions
All this is fine, but when it comes to debugging there are some surprises inevitably awaiting the unexpecting programmer. The first attempt at debugging a generator-expression might be to just print it:
add_library(Foo ...)
# ...
message("Foo is located at: $<TARGET_FILE:Foo>")
The will result in the following output:
$ cmake -B build -S .
...
"Foo is located at: $<TARGET_FILE:Foo>"
Well, that’s not very helpful, is it?
It’s important to remember that generator-expressions are evaluated only in the generation stage of the build process, and messages are printed during the configuration stage (which precedes generation). So unfortunately it’s impossible to simply print a generator-expression. One must use facilities that run at or after the generation stage:
Printing to file:
The file command offers a GENERATE mode, which, well, generates a file with the specified contents. Since, as one might guess, this occurs during the generation stage it can be used to debug generator-expressions:
file(GENERATE OUTPUT debug_genexpr CONTENT "$<TARGET_FILE:Foo>")
Now after running cmake the debug_genexpr file will contain the path to the binary resulting from the target Foo, which in my case is
$ cmake -B . -S build
$ cat ./build/foo/debug_genexpr
/home/jam/cmake_generator_expressions/build/foo/libFoo.a
Custom targets:
An alternative is to define a custom target which prints the evaluated generator expression to the console when invoked. Custom targets can be invoked after the project is configured and generated – generator-expressions are already evaluated at this point:
add_custom_target(genexdebug COMMAND ${CMAKE_COMMAND} -E echo "$<TARGET_FILE:Foo>")
Now to get the evaluated expression it is necessary to configure and build the project and then run the target:
$ cmake -B . -S build
$ cmake --build build
...
$ cmake build --target genexdebug
[100%] Built target Foo
Scanning dependencies of target genexdebug
/home/jam/cmake_generator_expressions/build/foo/libFoo.a
[100%] Built target genexdebug
Use whichever of these suits you. Both alternatives have their nuisances – generating a file requires us to get to the contents of the file and using a custom target more often than not requires building (rather than just configuring and generating) the project.
Summary
Generator expressions may be daunting, or at the very least unappealing at first, but once you get accustomed to them enough they become a valuble tool to succinctly express your intent. Achieving the same results using alternative methods is often much more verbose, or in some cases impossible altogether. I’d recommend considering using a generator-expression whenever you have an apparent need to conditionally configure a target – this applies to all commands which modify target properties – target_include_directories, target_link_libraries, target_compile_options, etc. You’ll quickly come to appreciate how much more readable the code is, compared to using if-else blocks.
great! I was not aware of the difference between configuration and generation.
Good post, way better than official documents!
“As a rule of thumb everytime you’re about to use an if-else statement consider if a conditional generator-expression wouldn’t be a more succinct and readable alternative.”
The answer is no. It’s never any more readable. It’s unreadable, undebuggable, inscrutable cmake magic and nobody should ever use it. If verbosity is the metric for readability, maybe one ought to consider whether writing directly in machine language wouldn’t be more readable, instead.