CMake presets schema versions and the inherits pattern for reproducible CI
Schema version mismatches and binaryDir surprises can break CMake presets. This guide walks through picking the right version, using inherits for shared CI configs, and verifying cacheVariables with a three-file example and user overrides.
08 Aug 2026, 07:44 UTC

CMake presets promise a single source of truth for configure, build, and test pipelines, but the first time you run cmake --preset=ci on a fresh machine and find the build tree in the wrong directory, or your option() defaults ignore the preset's cacheVariables, the promise feels more like a trap. The friction usually comes from three sources: schema version incompatibility, relative binaryDir paths that resolve against the wrong directory, and cacheVariables that apply before any option() block runs. This post distills the practical decisions you need to get presets working across CI and local development without hidden state leaks.
Pick a schema version that your whole team can run
CMake introduced presets experimentally in 3.19, stabilized them in 3.23, and added schema version 6 in 3.28. The version you set in cmake-presets.json is not just a preference -- it gates which features are accepted. If your minimum supported CMake is 3.23, using version 6 will cause CMake to abort with a parse error on any developer machine or CI runner still on 3.23. Conversely, locking to version 3 on a team running 3.28+ means you miss out on condition objects that can auto-select CI vs. local presets based on ${env.CI}.
| Schema version | Minimum CMake | Notes |
|---|---|---|
| 3 | 3.23 | Stable baseline; no condition support |
| 4 | 3.24 | Cache variable refinements |
| 6 | 3.28 | Native condition objects; older CMake will reject the file |
Share state across presets with inherits
One of the most useful but under-documented features is the inherits key. A preset can list other presets it pulls configuration from via inherits a base preset. The base preset typically defines the generator, toolchain file, and shared cacheVariables. Child presets then only need to override what changes -- often just binaryDir for a different build tree or a few cacheVariables for a specific configuration. This pattern lets you maintain a single ci-base preset that works for both GitHub Actions and local developer machines, while per-job presets drift only the variables that matter.
binaryDir and cacheVariables gotchas
- Relative
binaryDirresolves against the preset file's source directory, not your current working directory. If cmake-presets.json lives at /home/user/project, then binaryDir: build/dev places the build tree at /home/user/project/build/dev, regardless of where you run cmake from. cacheVariablesapply before the first CMakeLists.txt parse, so they can seed option() defaults, but they cannot override an option already set by a prior configure run in the same binaryDir. If you change a flag in an interactive configure and re-run cmake, the preset's variables will not retroactively flip that option -- a clean rebuild may be required.- Mixing Ninja Multi-Config and single-config generators (e.g., Makefile) in the same binaryDir corrupts the build tree -- always use distinct binaryDir per generator.
To verify your preset setup, run the following checks:
cmake --versionin the source directory; confirm the major version meets the minimum implied by your chosen schema (e.g., >= 3.23 for version 3).cmake --list-presetsin the source directory; verify the expected configure/build/test presets appear and inheritance chains resolve.cmake --preset=devand inspect the generatedCMakeCache.txt; confirm CMAKE_BINARY_DIR and your cacheVariables entries appear as intended.cmake --build --preset=devandctest --preset=devto validate the full configure-build-test pipeline.- Test the user-presets override: create cmake-user-presets.json (git-ignored) with a different binaryDir, re-run cmake --preset=dev, and ensure the new build tree is used.
A minimal three-file worked example
Commit these three files at the root of a C++ project. The committed file is shared; the user-specific file is git-ignored so each developer can tailor the build tree without touching shared config.
{ "version": 4, "presets": { "ci-base": { "generator": "Ninja", "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "CMAKE_CXX_STANDARD": "17" } }, "dev": { "inherits": ["ci-base"], "binaryDir": "build/dev", "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" } } } }Place this as cmake-user-presets.json in the source root and add it to .gitignore. When a developer runs cmake --preset=dev, CMake merges the user preset on top of the committed one, swapping the build directory while preserving the generator and cache variables from ci-base.
{ "presets": { "dev": { "binaryDir": "build/my-local-overrides" } } }Running cmake --preset dev configures the project using the dev preset, which inherits ci-base settings and applies CMAKE_BUILD_TYPE=Debug and CMAKE_CXX_STANDARD=17. The option default remains OFF because the preset's cacheVariables ran before the option() block. After the build, ctest --preset dev discovers and runs the test target defined in CMakeLists.txt.
Closing
If your team's minimum CMake is 3.23, start with schema version 3 and upgrade to 4 once everyone is on 3.24+. Use inherits to keep a ci-base preset that defines generator and shared variables, and let per-preset overrides handle only binaryDir and the few cacheVariables that actually change. Before committing a preset file, run the verification checklist above on a fresh clone and on your CI runner; the few minutes spent checking CMakeCache.txt save hours debugging misplaced build trees or silently ignored option defaults. With the three-file pattern committed and a user-presets override in place, your pipeline is ready to run identically on every developer laptop and every CI job.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.