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: becausemain_demois an executable target, CMake swaps in the real path to the built.exe, wherever it ended up. It also automatically makesrundepend onmain_demo, so buildingrunmeans "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.