Step 3: Configuration and Cache Variables

CMake projects often have some project-specific configuration variables which users and packagers are interested in. CMake has many ways that an invoking user or process can communicate these configuration choices, but the most fundamental of them are -D flags.

In this step we'll explore the ins and out of how to provide project configuration options from within a CML, and how to invoke CMake to take advantage of configuration options provided by both CMake and individual projects.

Background

If we had a CMake project for compression software which supported multiple compression algorithms, we might want to let the packager of the project decide which algorithms to enable when they build our software. We can do so by consuming variables set via -D flags.

if(COMPRESSION_SOFTWARE_USE_ZLIB)
  message("I will use Zlib!")
  # ...
endif()

if(COMPRESSION_SOFTWARE_USE_ZSTD)
  message("I will use Zstd!")
  # ...
endif()
$ cmake -B build \
    -DCOMPRESSION_SOFTWARE_USE_ZLIB=ON \
    -DCOMPRESSION_SOFTWARE_USE_ZSTD=OFF
...
I will use Zlib!

Of course, we will want to provide reasonable defaults for these configuration choices, and a way to communicate the purpose of a given option. This function is provided by the option() command.

option(COMPRESSION_SOFTWARE_USE_ZLIB "Support Zlib compression" ON)
option(COMPRESSION_SOFTWARE_USE_ZSTD "Support Zstd compression" ON)

if(COMPRESSION_SOFTWARE_USE_ZLIB)
  # Same as before
# ...
$ cmake -B build \
    -DCOMPRESSION_SOFTWARE_USE_ZLIB=OFF
...
I will use Zstd!

The names created by -D flags and option() are not normal variables, they are cache variables. Cache variables are globally visible variables which are sticky, their value is difficult to change after it is initially set. In fact they are so sticky that, in project mode, CMake will save and restore cache variables across multiple configurations. If a cache variable is set once, it will remain until another -D flag preempts the saved variable.

Note

CMake itself has dozens of normal and cache variables used for configuration. These are documented at cmake-variables(7) and operate in the same manner as project-provided variables for configuration.

set() can also be used to manipulate cache variables, but will not change a variable which has already been created.

set(StickyCacheVariable "I will not change" CACHE STRING "")
set(StickyCacheVariable "Overwrite StickyCache" CACHE STRING "")

message("StickyCacheVariable: ${StickyCacheVariable}")
$ cmake -P StickyCacheVariable.cmake
StickyCacheVariable: I will not change

Because -D flags are processed before any other commands, they take precedence for setting the value of a cache variable.

$ cmake \
  -DStickyCacheVariable="Commandline always wins" \
  -P StickyCacheVariable.cmake
StickyCacheVariable: Commandline always wins

While cache variables cannot ordinarily be changed, they can be shadowed by normal variables. We can observe this by set()'ing a variable to have the same name as a cache variable, and then using unset() to remove the normal variable.

set(ShadowVariable "In the shadows" CACHE STRING "")
set(ShadowVariable "Hiding the cache variable")
message("ShadowVariable: ${ShadowVariable}")

unset(ShadowVariable)
message("ShadowVariable: ${ShadowVariable}")
$ cmake -P ShadowVariable.cmake
ShadowVariable: Hiding the cache variable
ShadowVariable: In the shadows

Exercise 1 - Using Options

We can imagine a scenario where consumers really want our MathFunctions library, and the Tutorial utility is a "take it or leave it" add-on. In that case, we might want to add an option to allow consumers to disable building our Tutorial binary, building only the MathFunctions library.

With our knowledge of options, conditionals, and cache variables we have all the pieces we need to make this configuration available.

Goal

Add an option named TUTORIAL_BUILD_UTILITIES to control if the Tutorial binary is configured and built.

Note

CMake allows us to determine which targets are built after configuration. Our users could ask for the MathFunctions library alone without Tutorial. CMake also has mechanisms to exclude targets from ALL, the default target which builds all the other available targets.

However, options which completely exclude targets from the configuration are convenient and popular, especially if configuring those targets involves heavy-weight steps which might take some time.

It also simplifies install() logic, which we'll discuss in later steps, if targets the packager is uninterested in are completely excluded.

Helpful Resources

Files to Edit

  • CMakeLists.txt

Getting Started

The Help/guide/tutorial/Step3 folder contains the complete, recommended solution to Step1 and the relevant TODOs for this step. Take a minute to review and refamiliarize yourself with the Tutorial project.

When you feel you have an understanding of the current code, start with TODO 1 and complete through TODO 2.

Build and Run

We can now reconfigure our project. However, this time we want to control the configuration via -D flags. We again start by navigating to Help/guide/tutorial/Step3 and invoking CMake, but this time with our configuration options.

cmake -B build -DTUTORIAL_BUILD_UTILITIES=OFF

We can now build as usual.

cmake --build build

After the build we should observe no Tutorial executable is produced. Because cache variables are sticky even a reconfigure shouldn't change this, despite the default-ON option.

cmake -B build
cmake --build build

Will not produce the Tutorial executable, the cache variables are "locked in". To change this we have two options. First, we can edit the file which stores the cache variables between CMake configuration runs, the "CMake Cache". This file is build/CMakeCache.txt, in it we can find the option cache variable.

//Build the Tutorial executable
TUTORIAL_BUILD_UTILITIES:BOOL=OFF

We can change this from OFF to ON, rerun the build, and we will get our Tutorial executable.

Note

CMakeCache.txt entries are of the form <Name>:<Type>=<Value>, however the "type" is only a hint. All objects in CMake are strings, regardless of what the cache says.

Alternatively, we can change the value of the cache variable on the command line, because the command line runs before CMakeCache.txt is loaded its value take precedence over those in the cache file.

cmake -B build -DTUTORIAL_BUILD_UTILITIES=ON
cmake --build build

Doing so we observe the value in CMakeCache.txt has flipped from OFF to ON, and that the Tutorial executable is built.

Solution

First we create our option() to provide our cache variable with a reasonable default value.

TODO 1: Click to show/hide answer
TODO 1: CMakeLists.txt
option(TUTORIAL_BUILD_UTILITIES "Build the Tutorial executable" ON)

Then we can check the cache variable to conditionally enable the Tutorial executable (by way of adding its subdirectory).

TODO 2: Click to show/hide answer
TODO 2: CMakeLists.txt
if(TUTORIAL_BUILD_UTILITIES)
  add_subdirectory(Tutorial)
endif()

Exercise 2 - CMAKE Variables

CMake has several important normal and cache variables provided to allow packagers to control the build. Decisions such as compilers, default flags, search locations for packages, and much more are all controlled by CMake's own configuration variables.

Among the most important are language standards. As the language standard can have significant impact on the ABI presented by a given package. For example, it's quite common for libraries to use standard C++ templates on later standards, and provide polyfills on earlier standards. If a library is consumed under different standards then ABI incompatibilities between the standard templates and the polyfills can result in incomprehensible errors and runtime crashes.

Ensuring all of our targets are built under the same language standard is achieved with the CMAKE_<LANG>_STANDARD cache variables. For C++, this is CMAKE_CXX_STANDARD.

Note

Because these variables are so important, it is equally important that developers not override or shadow them in their CMLs. Shadowing CMAKE_<LANG>_STANDARD in a CML because the library wants C++20, when the packager has decided to build the rest of their libraries and applications with C++23, can lead to the aforementioned terrible, incomprehensible errors.

Do not set() CMAKE_ globals without very strong reasons for doing so. We'll discuss better methods for targets to communicate requirements like definitions and minimum standards in later steps.

In this exercise, we'll introduce some C++20 code into our library and executable and build them with C++20 by setting the appropriate cache variable.

Goal

Use std::format to format printed strings instead of stream operators. To ensure availability of std::format, configure CMake to use the C++20 standard for C++ targets.

Helpful Resources

Files to Edit

  • Tutorial/Tutorial.cxx

  • MathFunctions/MathFunctions.cxx

Getting Started

Continue to edit files from Step3. Complete TODO 3 through TODO 7. We'll be modifying our prints to use std::format instead of stream operators.

Ensure your cache variables are set such that the Tutorial executable will be built, using any of the methods discussed in the previous exercise.

Build and Run

We need to reconfigure our project with the new standard, we can do this using the same method as our TUTORIAL_BUILD_UTILITIES cache variable.

cmake -B build -DCMAKE_CXX_STANDARD=20

Note

Configuration variables are, by convention, prefixed with the provider of the variable. CMake configuration variables are prefixed with CMAKE_, while projects should prefix their variables with <PROJECT>_.

The tutorial configuration variables follow this convention, and are prefixed with TUTORIAL_.

Now that we've configured with C++20, we can build as usual.

cmake --build build

Solution

We need to include <format> and then use it.

TODO 3-5: Click to show/hide answer
TODO 3: Tutorial/Tutorial.cxx
#include <format>
#include <iostream>
#include <string>
TODO 4: Tutorial/Tutorial.cxx
if (argc < 2) {
  std::cout << std::format("Usage: {} number\n", argv[0]);
  return 1;
}
TODO 5: Tutorial/Tutorial.cxx
// calculate square root
double const outputValue = mathfunctions::sqrt(inputValue);
std::cout << std::format("The square root of {} is {}\n", inputValue,
                         outputValue);

And again for the MathFunctions library.

TODO 6-7: Click to show/hide answer
TODO 6: MathFunctions.cxx
#include <format>
#include <iostream>
TODO 7: MathFunctions.cxx
double delta = x - (result * result);
result = result + 0.5 * delta / result;

std::cout << std::format("Computing sqrt of {} to be {}\n", x, result);

Exercise 3 - CMakePresets.json

Managing these configuration values can quickly become overwhelming. In CI systems it is appropriate to record these as part of a given CI step. For example in a Github Actions CI step we might see something akin to the following:

- name: Configure and Build
  run: |
    cmake \
      -B build \
      -DCMAKE_BUILD_TYPE=Release \
      -DCMAKE_CXX_STANDARD=20 \
      -DCMAKE_CXX_EXTENSIONS=ON \
      -DTUTORIAL_BUILD_UTILITIES=OFF \
      # Possibly many more options
      # ...

    cmake --build build

When developing code locally, typing all these options even once might be error prone. If a fresh configuration is needed for any reason, doing so multiple times could be exhausting.

There are many and varied solutions to this problem, and your choice is ultimately up to your preferences as a developer. CLI-oriented developers commonly use task runners to invoke CMake with their desired options for a project. Most IDEs also have a custom mechanism for controlling CMake configuration.

It would be impossible to fully enumerate every possible configuration workflow here. Instead we will explore CMake's built-in solution, known as CMake Presets. Presets give us a format to name and express collections of CMake configuration options.

Note

Presets are capable of expressing entire CMake workflows, from configuration, through building, all the way to installing the software package.

They are far more flexible than can we have room for here. We'll limit ourselves to using them for configuration.

CMake Presets come in two standard files, CMakePresets.json, which is intended to be a part of the project and tracked in source control; and CMakeUserPresets.json, which is intended for local user configuration and should not be tracked in source control.

The simplest preset which would be of use to a developer does nothing more than configure variables.

{
  "version": 4,
  "configurePresets": [
    {
      "name": "example-preset",
      "cacheVariables": {
        "EXAMPLE_FOO": "Bar",
        "EXAMPLE_QUX": "Baz"
      }
    }
  ]
}

When invoking CMake, where previously we would have done:

cmake -B build -DEXAMPLE_FOO=Bar -DEXAMPLE_QUX=Baz

We can now use the preset:

cmake -B build --preset example-preset

CMake will search for files named CMakePresets.json and CMakeUserPresets.json, and load the named configuration from them if available.

Note

Command line flags can be mixed with presets. Command line flags have precedence over values found in a preset.

Presets also support limited macros, variables that can be brace-expanded inside the preset. The only one of interest to us is the ${sourceDir} macro, which expands to the root directory of the project. We can use this to set our build directory, skipping the -B flag when configuring the project.

{
  "name": "example-preset",
  "binaryDir": "${sourceDir}/build"
}

Goal

Configure and build the tutorial using a CMake Preset instead of command line flags.

Helpful Resources

Files to Edit

  • CMakePresets.json

Getting Started

Continue to edit files from Step3. Complete TODO 8 and TODO 9.

Note

TODOs inside CMakePresets.json need to be replaced. There should be no TODO keys left inside the file when you have completed the exercise.

You can verify the preset is working correctly by deleting the existing build folder before you configure, this will ensure you're not reusing the existing CMake Cache for configuration.

Note

On CMake 3.24 and newer, the same effect can be achieved by configuring with cmake --fresh.

All future configuration changes will be via the CMakePresets.json file.

Build and Run

We can now use the preset file to manage our configuration.

cmake --preset tutorial

Presets are capable of running the build step for us, but for this tutorial we'll continue to run the build ourselves.

cmake --build build

Solution

There are two changes we need to make, first we want to set the build directory (also called the "binary directory") to the build subdirectory of our project folder, and second we need to set the CMAKE_CXX_STANDARD to 20.

TODO 8-9: Click to show/hide answer
TODO 8-9: CMakePresets.json
{
  "version": 4,
  "configurePresets": [
    {
      "name": "tutorial",
      "displayName": "Tutorial Preset",
      "description": "Preset to use with the tutorial",
      "binaryDir": "${sourceDir}/build",
      "cacheVariables": {
        "CMAKE_CXX_STANDARD": "20"
      }
    }
  ]
}