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:

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 is set 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 the set function:

Here is a list of few CMake-defined variables:

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:

  1. Functions have their own scope: variables defined inside a function are not propagated back to the caller.

  2. 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.

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, and Y.

  • False is any expression evaluating to: 0, OFF, FALSE, NO, N, IGNORE, and NOTFOUND.

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:

  1. Define the MAKE_SHARED_LIBRARY variable.

  2. 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 perform the same operation on a collection of items with foreach:

The list of items is either space- or ;-separated. break() and continue() are also available.

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:

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:

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!

By importing the CMakeDependentOption module, you can handle cases where options are only relevant if other options are already set to specific values:

Exercise 4: User-facing options

In this exercise, we will work with option and cmake_dependent_option. We want to allow the user to decide whether to build a library and whether that should be static or shared.

  1. Add a USE_LIBRARY option

  2. Add dependent options MAKE_STATIC_LIBRARY and MAKE_SHARED_LIBRARY. They will only be presented if USE_LIBRARY is true.

  3. Use conditionals to orchestrate the build of the static/shared library.

You can find a scaffold project in the content/code/day-1/04_options-cxx folder. A working solution is in the solution subfolder.

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.