CodeGym /Courses /C++ SELF /CMake Presets: reproducible presets

CMake Presets: reproducible presets

C++ SELF
Level 32, Lesson 2
Available

1. What presets are and where they live

If you've ever heard the phrase “it works on my machine,” you already intuitively understand why CMake Presets were invented. The problem isn’t that someone is a bad developer; it’s that build settings often live in a dozen places: in the IDE, in environment variables, in the CMake cache, in different build folders and in the heads of team members (the most unstable storage of all).

Imagine a situation: you selected Debug in CLion, your friend somehow has Release in VS Code, and a third person builds from the console and once forgot to set the C++23 standard. The sources are the same, but behavior “jumps”: somewhere an assert fired, somewhere it didn’t; somewhere optimization reordered code so the debugger shows “variable unavailable”; and somewhere linking unexpectedly pulled a different library version.

CMake Presets is a way to say: “Here are the project's official build workflows. They are described in a file, stored in the repository, and we all use the same ones.” Not “the same in words,” but actually the same: identical key configuration variables, identical build directories, identical commands.

What a preset is: a small JSON that saves big nerves

A preset is a named workflow for CMake. The workflow typically splits into several stages: first we configure the project (CMake generates build files in the build folder), then we build, and sometimes we also run tests. CMake Presets let you describe these stages so you (or your instructor, or CI) can say: “Build the debug preset” — and not have to remember what flags, paths and parameters to use.

Keep a simple mental model in mind:

flowchart LR
    A["Sources (sourceDir)"] --> B["Configure preset
cmake --preset ..."] B --> C["Build directory (binaryDir)
build-debug/ or build-release/"] C --> D["Build preset
cmake --build --preset ..."] C --> E["Test preset
ctest --preset ... (if needed)"]

The important point here is: the configure preset controls how the build folder appears and what ends up in its CMakeCache.txt. The build preset controls building the already configured directory. The test preset runs tests (if present).

Yes, this is exactly what an IDE used to do “with a button.” The difference is that now it's not “configured somewhere in your UI,” but described in the repository and repeatable on any machine.

CMakePresets.json: file structure, no mystique

CMake Presets are described in the JSON file CMakePresets.json at the project root (next to CMakeLists.txt). It's plain JSON, so it's strict: quotes are required, commas matter, comments in // style are not allowed (and yes, that's unfortunate because comments would be handy, but JSON is strict).

Minimal file structure looks like this:

{
  "version": 6,
  "configurePresets": [],
  "buildPresets": [],
  "testPresets": []
}

The version field is the preset format version (not the CMake version or your project version). In practice you'll most often see versions like 4/5/6: the main thing is that your CMake can read it. If your CMake is old and “doesn't understand” the format, you'll get a very disappointing error before the build starts.

In this file you usually describe the project's “official” presets: debug, release, possibly asan (sanitizers will be a separate day), and sometimes ci. The idea is not to create 20 variants “just in case,” but to pin 2–4 working workflows that everyone uses.

3. Configure preset: build folder and key variables

A configure preset is, essentially, the answer to the question: “Where and with which parameters do we configure the project?” It sets binaryDir, can set the generator, and most practically — sets cacheVariables, i.e. the variables that will go into CMakeCache.txt and affect configuration.

Let's start with a base preset and inherit Debug and Release from it. This is not “pretty for the sake of it”: inheritance reduces copy‑paste and lowers the chance that you forgot to enable C++23 in one preset but not the other.

Example:

{
  "version": 6,
  "configurePresets": [
    {
      "name": "base",
      "hidden": true,
      "cacheVariables": {
        "CMAKE_CXX_STANDARD": "23"
      }
    },
    {
      "name": "debug",
      "inherits": "base",
      "binaryDir": "${sourceDir}/build-debug",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug"
      }
    },
    {
      "name": "release",
      "inherits": "base",
      "binaryDir": "${sourceDir}/build-release",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release"
      }
    }
  ]
}

Several important things happen here.

binaryDir firmly fixes where the build will live. This is discipline: Debug and Release don't interfere with each other, and you won't have the situation “I changed the build type, but it somehow didn't take effect.” In fact it did take effect — the old configuration remained in the cache, and you're living in the past. Presets help you not live in the past.

The ${sourceDir} variable is a CMake Presets variable that means “the directory with sources.” This is better than writing absolute paths like C:\Users\Alice\Projects\..., because absolute paths make a preset non-portable: on another machine it will turn into a pumpkin.

cacheVariables are exactly what will end up in the cache. The most popular example of the day is CMAKE_BUILD_TYPE, which sets Debug/Release (for single-configuration generators). Another sensible example is CMAKE_CXX_STANDARD, so the project builds with C++23 “by agreement,” not “because you have your IDE set that way.”

To connect this with the Debug/Release topic, you can add a tiny runtime check in code and see what is really being built:

#include <iostream>

int main() {
#ifdef NDEBUG
    std::cout << "Release-like build (NDEBUG is defined)\n";
#else
    std::cout << "Debug-like build (NDEBUG is NOT defined)\n";
#endif
}

The point isn't to write like this “in a real product,” but to practically feel: a preset changes configuration, configuration affects macros (for example, NDEBUG), and macros affect compilation.

4. Build and Test presets: build and tests as repeatable steps

Build preset: building is a separate step

A build preset is responsible for “how to build what is already configured.” It sounds boring until you catch yourself running the build 15 times from different places: from the IDE, from the terminal, or accidentally from another build folder because the terminal tab was the wrong one.

A build preset usually references a configure preset by name. Example:

{
  "version": 6,
  "configurePresets": [
    {
      "name": "debug",
      "binaryDir": "${sourceDir}/build-debug",
      "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" }
    }
  ],
  "buildPresets": [
    {
      "name": "debug",
      "configurePreset": "debug"
    }
  ]
}

The idea is: the build command “knows” which build directory to use, and you don't need to remember where it is. This is especially handy when there are multiple build folders and you really want them to be independent.

In practice a build preset can contain additional parameters: for example, the configuration (for multi-config generators), parallelism (how many jobs to build simultaneously), verbose output mode. But for our day it's enough to capture: configure preset creates and sets up the build folder, build preset builds.

Test preset: running tests can also be a “contract”

A test preset is a way to pin how tests are run. Important: don't confuse test preset with a “test framework” or “how to write tests.” It's simply a scenario to run ctest, which works if your project already added tests to CMake (for example, via enable_testing() and add_test(...)).

Yes, full unit tests will come much later. But right now it's useful to see the idea: if a project can be tested, then running tests should be reproducible too.

A minimal test preset can look like this:

{
  "version": 6,
  "configurePresets": [
    {
      "name": "debug",
      "binaryDir": "${sourceDir}/build-debug",
      "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" }
    }
  ],
  "testPresets": [
    {
      "name": "debug",
      "configurePreset": "debug"
    }
  ]
}

This preset says: “Run tests from the build directory that corresponds to debug.” In practice this removes a lot of confusion when someone runs ctest from build-release but expects results as if it were build-debug. Programming already has enough mysteries — don't add another one for no reason.

5. Using presets in real life: commands and splitting common/local files

Three short commands instead of “what did I click there”

Presets are especially satisfying when you use them the same way everywhere: on your machine, with an instructor, with a classmate, and in CI. The idea is very simple: you no longer keep the “right commands” in your head, you keep the preset names.

A typical cycle looks like this:

cmake --preset debug
cmake --build --preset debug
ctest --preset debug

At this point a beginner's brain usually asks: “Why not with a single command?” You can, but you don't need to. Separating configure/build/test is not bureaucracy but a reflection of reality: configuration creates the build project and caches parameters, build produces binaries, tests run checks.

If you changed CMakePresets.json, remember that the build directory may already have been configured with old values. Then you need to re-run the configure preset (the first command) so changes get into the cache and generation. Presets don't eliminate the existence of CMakeCache.txt; they just make its creation controlled.

CMakePresets.json and CMakeUserPresets.json: what to commit and what to keep local

A very typical story: you want presets to be shared, but each developer may have local specifics. For example, someone wants very verbose build logs, someone wants to point to a nonstandard compiler, and someone wants to build into a folder on a different drive because the system disk has little space.

For this purpose CMake has the idea of two files:

  • CMakePresets.json — the shared one, usually committed to the repository. These are the project's “official” presets.
  • CMakeUserPresets.json — the user one, usually not committed. These are “my local conveniences” that shouldn't break reproducibility for others.

Even if you're working alone right now, it's useful to get used to this discipline: “shared” workflows go in the shared file, “personal” ones go in the user file. Otherwise in a month you'll discover the project “builds on your machine only because you once manually added a setting in the IDE,” and no one else knows which setting.

6. Common mistakes when working with presets

In this topic mistakes are usually not “hard,” but insidiously everyday: you do almost everything correctly, but one small nuance turns the build into a lottery. The good news is that these traps are predictable, and once you understand them you'll recognize them by sound.

Mistake #1: one binaryDir for all purposes.
If two presets (for example, Debug and Release) use the same build folder, you return to the world of “why didn't settings apply.” The point of presets is also the discipline of separate build directories so configurations don't overwrite each other's cache. Make build-debug/ and build-release/ at minimum, and life becomes noticeably calmer.

Mistake #2: expecting the build preset to “apply new cacheVariables” itself.
A build preset does not “configure” anything — it just runs the build in an existing build directory. If you changed cacheVariables (for example, added C++23 or changed the build type) but didn't re-run the configure step, the build will continue with the old CMakeCache.txt. There's no magic here: it's just state.

Mistake #3: hard absolute paths in presets.
Absolute paths make presets tied to your computer. Today you are glad you quickly pointed to C:\..., and tomorrow you can't build the project on another machine, in CI, or even after moving the folder on your own machine. Use ${sourceDir} and other variables so presets remain portable.

Mistake #4: trying to make presets replace user input.
A preset is about building, not about “program runtime parameters.” You can't reliably pass things like “username” or “input file path” through presets as if they were runtime settings. Yes, technically you can define a macro and bake it into code, but you'll quickly end up with a project that needs recompilation every time a single value changes. Presets are about how to compile, not about how to use the program.

Mistake #5: ignoring that CMAKE_BUILD_TYPE doesn't always work as you expect.
In some environments CMake uses generators where Debug/Release are not chosen via CMAKE_BUILD_TYPE at configure time, but differently (this is generator-specific detail). For our lecture it's important to remember a practical rule: if you see a preset “Debug” somehow giving Release-like behavior, don't argue with reality — check which generator your environment uses and whether CMAKE_BUILD_TYPE is actually taken into account.

1
Task
C++ SELF, level 32, lesson 2
Locked
First preset
First preset
1
Task
C++ SELF, level 32, lesson 2
Locked
Build preset
Build preset
1
Task
C++ SELF, level 32, lesson 2
Locked
Two modes
Two modes
1
Task
C++ SELF, level 32, lesson 2
Locked
Test contract
Test contract
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION