Here's the video walkthrough, if you'd rather watch it:

Why I Started Using This Tool

In the last part of this project, I reviewed my small C math library line by line, looking for edge cases to test. The code was fine. My workflow wasn't.

Every change meant typing this:

cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug
cmake --build build
.\build\main_demo.exe

I kept forgetting flags. Once I ran a stale executable after a failed build and spent ten minutes confused about why my change "didn't work." I also knew that anyone cloning my repo would have to guess the exact same flags.

I wanted CMake to remember my settings for me, and I wanted a single command that does everything. That's what led me to CMake presets.

What It Does

Quick background first: CMake doesn't compile your code. It's a build system generator. In the configure step it reads CMakeLists.txt and writes build files into a folder (build/ here). In the build step, a tool like Ninja reads those files and actually compiles and links. Presets save the settings for both steps, and there are two pieces to the setup.

1. A run target in CMakeLists.txt

Here's the whole file:

cmake_minimum_required(VERSION 3.25)
project(main_demo C)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)

add_executable(main_demo
    src/main.c
    src/arithmetic.c
    src/power_abs.c
    src/statistics.c
)

target_include_directories(main_demo PRIVATE src)

add_custom_target(run
    COMMAND main_demo
    DEPENDS main_demo
    USES_TERMINAL
)

The interesting part is the last block:

  • COMMAND main_demo: because main_demo is an executable target, CMake swaps in the real path to the built .exe, wherever it ended up. It also automatically makes run depend on main_demo, so building run means "compile if needed, then run."
  • DEPENDS main_demo: it turns out this is redundant because of that automatic dependency. It's harmless and reads clearly, so I left it in.
  • USES_TERMINAL: Ninja normally buffers a command's output. With this, the program's output streams straight to my console (and it could read keyboard input if it needed to).

A couple of other lines worth knowing: project(main_demo C) says the only language is C, so CMake doesn't go looking for a C++ compiler. CMAKE_C_STANDARD_REQUIRED ON makes CMake fail loudly if the compiler can't do C11, instead of quietly falling back to something older. (The last post's preview used C99. The code works under either, and I went with C11 here.)

2. A CMakePresets.json file

A preset is a named, saved set of CMake options in a JSON file next to CMakeLists.txt. It goes into Git, so everyone who clones the project gets the same settings. Mine has three kinds of presets.

The header and the configure preset, which stores all the flags I used to type:

"version": 6,
"cmakeMinimumRequired": { "major": 3, "minor": 25, "patch": 0 },
"configurePresets": [
  {
    "name": "default",
    "displayName": "Default (Ninja, Debug)",
    "generator": "Ninja",
    "binaryDir": "${sourceDir}/build",
    "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" }
  }
]

generator replaces -G Ninja, binaryDir replaces -B build (${sourceDir} is filled in with the project folder), and cacheVariables replaces the -D flags. The build type lives here because Ninja is a single-config generator: one build folder holds one configuration, chosen at configure time.

The build presets: default builds everything, and run builds the run target.

"buildPresets": [
  { "name": "default", "configurePreset": "default" },
  { "name": "run", "configurePreset": "default", "targets": ["run"] }
]

And a workflow preset that chains them: configure, then build-and-run.

"workflowPresets": [
  {
    "name": "default",
    "displayName": "Configure, build and run",
    "steps": [
      { "type": "configure", "name": "default" },
      { "type": "build", "name": "run" }
    ]
  }
]

The result: one command

cmake --workflow --preset default

Here's what it looks like on my machine:

Executing workflow step 1 of 2: configure preset "default"
-- The C compiler identification is GNU 15.2.0
...
-- Build files have been written to: C:/git/09_cmake/build

Executing workflow step 2 of 2: build preset "run"
[1/6] Building C object CMakeFiles/main_demo.dir/src/arithmetic.c.obj
[2/6] Building C object CMakeFiles/main_demo.dir/src/power_abs.c.obj
[3/6] Building C object CMakeFiles/main_demo.dir/src/main.c.obj
[4/6] Building C object CMakeFiles/main_demo.dir/src/statistics.c.obj
[5/6] Linking C executable main_demo.exe
[5/6] ... C:\git\09_cmake\build\main_demo.exe

add=10 sub=4 mul=21 div=3.50 mod=1
pow=1024 abs=5 min=3 max=8 clamp=10
sum=22 avg=4.40 count(4)=2 min=-2 max=9

One command, and my program's output appears at the bottom of the build log. If configure or compile fails, the workflow stops, so I never run an old binary again.

You can still run the pieces on their own:

cmake --preset default          # just configure
cmake --build --preset default  # just build
cmake --build --preset run      # build + run (after configuring once)

After the first time, cmake --build --preset run is the fast inner loop: edit, run, see the result. As a bonus, VS Code's CMake Tools extension picks up the presets automatically and offers "Default (Ninja, Debug)" in its preset picker, so the editor and the terminal use identical settings.

The one catch: workflow presets need presets format version 6, which requires CMake 3.25 or newer. My CMakeLists.txt asks for the same minimum, so the two files agree.

Final Verdict

If your CMake is 3.25 or newer, use presets. It's a small JSON file, it lives in Git, and it removes a whole category of "works on my machine" problems, plus the daily friction of retyping commands. The run target is a tiny trick, but it turned my edit–build–run loop into a single step.

What it doesn't do yet is test anything. There's no enable_testing() and no library split, so CTest has nothing to run. That's the next episode: split the math code into a library, add CTest, and add a test step to this same workflow, so one command configures, builds, runs and tests. If you're starting out with CMake, I'd set up presets before writing your first test. Everything after this gets easier.