CMake syntax
Questions
How can we achieve more control over the build system generated by CMake?
Is it possible to let the user decide what to generate?
Objectives
Learn how to define variables with
set
and use them with the${}
operator for variable references.Learn the syntax for conditionals in CMake:
if
-elseif
-else
-endif
Learn the syntax for loops in CMake:
foreach
Learn how CMake structures build artifacts.
Learn how to print helpful messages.
Learn how to handle user-facing options:
option
and the role of the CMake cache.
CMake offers a domain-specific language (DSL) to describe how to generate a build system native to the specific platform you might be running on. In this episode, we will get acquainted with its syntax.
The CMake DSL
Remember that the DSL is case-insensitive. We will now have a look at its main elements.
Variables
These are either CMake- or user-defined. You can obtain the list of CMake-defined variables with:
$ cmake --help-variable-list
You can create a new variable with the set
command:
set(<variable> <value>... [PARENT_SCOPE])
Variables in CMake are always of string type, but certain commands can interpret
them as other types. If you want to declare a list variable, you will have to
provide it as a ;-separated string. Lists can be manipulated with the list
family of commands.
You can inspect the value of any variable by dereferencing it with the ${}
operator, as in bash shell. For example, the following snippet sets the content
of the hello
variable and then prints it:
set(hello "world")
message("hello ${hello}")
Two things to note about variable references:
if the variable within the
${}
operator is not set, you will get an empty string.you can nest variable references:
${outer_${inner_variable}_variable}
. They will be evaluated from the inside out.
One of the most confusing aspects in CMake is the scoping of variables. There are three variable scopes in the DSL:
- Function
In effect when a variable is
set
within a function: the variable will be visible within the function, but not outside.- Directory
In effect when processing a
CMakeLists.txt
in a directory: variables in the parent folder will be available, but any that isset
in the current folder will not be propagated to the parent.- Cache
These variables are persistent across calls to
cmake
and available to all scopes in the project. Modifying a cache variable requires using a special form of theset
function:set(<variable> <value>... CACHE <type> <docstring> [FORCE])
Here is a list of few CMake-defined variables:
PROJECT_BINARY_DIR
. This is the build folder for the project.PROJECT_SOURCE_DIR
. This is the location of the rootCMakeLists.txt
in the project.CMAKE_CURRENT_LIST_DIR
. This is the folder for theCMakeLists.txt
currently being processed.
Help on a specific built-in variable can be obtained with:
$ cmake --help-variable PROJECT_BINARY_DIR
Commands
These are provided by CMake and are the essential building blocks of the DSL, as they allow you to manipulate variables.
They include control flow constructs and the target_*
family of commands.
You can find a complete list of available commands with:
$ cmake --help-command-list
Functions and macros are built on top of the basic built-in commands and are either CMake- or user-defined. These prove useful to avoid repetition in your CMake scripts. The difference between a function and a macro is their scope:
Functions have their own scope: variables defined inside a function are not propagated back to the caller.
Macros do not have their own scope: variables from the parent scope can be modified and new variables in the parent scope can be set.
Help on a specific built-in command, function or macro can be obtained with:
$ cmake --help-command target_link_libraries
Modules
These are collections of functions and macros and are either CMake- or user-defined. CMake comes with a rich ecosystem of modules and you will probably write a few of your own to encapulate frequently used functions or macros in your CMake scripts. You will have to include the module to use its contents, for example:
include(CMakePrintHelpers)
The full list of built-in modules is available with:
$ cmake --help-module-list
Help on a specific built-in module can be obtained with:
$ cmake --help-module CMakePrintHelpers
Flow control
The if
and foreach
commands are available as flow control constructs in the
CMake DSL and you are surely familiar with their use in other programming
languages.
Since all variables in CMake are strings, the syntax for if
and foreach
appears in a few different variants.
if(<condition>)
# <commands>
elseif(<condition>) # optional block, can be repeated
# <commands>
else() # optional block
# <commands>
endif()
The truth value of the conditions in the if
and elseif
blocks is
determined by boolean operators. In the CMake DSL:
True is any expression evaluating to:
1
,ON
,TRUE
,YES
, andY
.False is any expression evaluating to:
0
,OFF
,FALSE
,NO
,N
,IGNORE
, andNOTFOUND
.
CMake offers boolean operator for string comparisons, such as STREQUAL
for
string equality, and for version comparisons, such as VERSION_EQUAL
.
Variable expansions in conditionals
The if
command expands the contents of variables before evaluating their
truth value.
See the official documentation
for further details.
Exercise 2: Conditionals in CMake
Modify the CMakeLists.txt
from the previous exercise to build either a
static or a shared library depending on the value of the boolean
MAKE_SHARED_LIBRARY
:
Define the
MAKE_SHARED_LIBRARY
variable.Write a conditional checking the variable. In each branch call
add_library
appropriately.
You can find a scaffold project in the
content/code/day-1/02_conditionals-cxx
folder.
A working solution is in the solution
subfolder.
You can find a scaffold project in the
content/code/day-1/02_conditionals-f
folder.
A working solution is in the solution
subfolder.
You can perform the same operation on a collection of items with foreach
:
foreach(<loop_var> <items>)
# <commands>
endforeach()
The list of items is either space- or ;-separated. break()
and
continue()
are also available.
Loops in CMake
In this typealong, we will show how to use foreach
and lists in CMake. We
will work from a scaffold project in the content/code/day-1/03_loops-cxx
folder.
The goal is to compile a library from a bunch of source files: some of them
are to be compiled with -O3
optimization level, while some others with
-O2
.
We will set the compilation flags as properties on the library target.
Targets and properties will be discussed at greater length in Target-based build systems with CMake.
A working solution is in the solution
subfolder.
It is instructive to browse the build folder for the project:
$ tree -L 2 build
build
├── CMakeCache.txt
├── CMakeFiles
│ ├── 3.18.4
│ ├── cmake.check_cache
│ ├── CMakeDirectoryInformation.cmake
│ ├── CMakeOutput.log
│ ├── CMakeTmp
│ ├── compute-areas.dir
│ ├── geometry.dir
│ ├── Makefile2
│ ├── Makefile.cmake
│ ├── progress.marks
│ └── TargetDirectories.txt
├── cmake_install.cmake
├── compute-areas
├── libgeometry.a
└── Makefile
We note that:
The project was configured with
Makefile
generator.The cache is a plain-text file
CMakeCache.txt
.For every target in the project, CMake will create a subfolder
<target>.dir
underCMakeFiles
. The intermediate object files are stored in these folders, together with compiler flags and link line.The build artifacts,
compute-areas
andlibgeometry.a
, are stored at the root of the build tree.
Printing messages
You will most likely have to engage in debugging your CMake scripts at one point
or another. Print-based debugging is the most effective way to
do so and the main workhorse for this will be the message
command:
Parameters
<mode>
What type of message to display, for example:
STATUS
, for incidental information.FATAL_ERROR
, to report an error that prevents further processing and generation.
message
can be a bit awkward to work with, especially when you want to print
the name and value of a variable. Including the built-in module
CMakePrintHelpers
will make your life easier when debugging, since it
provides the cmake_print_variables
function:
cmake_print_variables(var1 var2 ... varN)
This command accepts an arbitrary number of variables and prints their name and value to standard output. For example:
include(CMakePrintHelpers)
cmake_print_variables(CMAKE_C_COMPILER CMAKE_MAJOR_VERSION DOES_NOT_EXIST)
gives:
-- CMAKE_C_COMPILER="/usr/bin/gcc" ; CMAKE_MAJOR_VERSION="2" ; DOES_NOT_EXIST=""
Controlling the build with options
We mentioned earlier that the -D
switch in the command-line interface (CLI)
of the cmake
command can be used to pass options, but how do we define these
options in our CMakeLists.txt
?
That is where the option
comes into play!
option(<variable> "<help_text>" [value])
With this, you can provide an ON/OFF toggle controllable from the CLI.
By importing the CMakeDependentOption
module, you can handle cases where
options are only relevant if other options are already set to specific values:
cmake_dependent_option(USE_FOO "Use Foo" ON "USE_BAR;NOT USE_ZOT" OFF)
If the option USE_BAR
is true and the option USE_ZOT
is false, then
an option USE_FOO
will be presented to the user and it will be true by
default. If the condition on USE_BAR
and USE_ZOT
is not realized, the
option is set to false.
Keypoints
CMake offers a full-fledged DSL which empowers you to write complex
CMakeLists.txt
.Variables have scoping rules.
The structure of the project is mirrored in the build folder.