Stop Sharing Build Flags in READMEs: Use CMakePresets.json Instead
CMakePresets.json replaces fragile command-line conventions with version-controlled, composable build configurations that work identically across developer machines and CI pipelines. Here's how to structure presets for real projects.
08 Oct 2025, 10:17 UTC

The Problem: Drifting Build Configurations
Every CMake project accumulates a tribal knowledge of command-line flags. One developer uses -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS=-Wall -Wextra -Werror, another forgets the warning flags, CI uses Ninja while local machines default to Makefiles, and the Windows contributor spends an hour figuring out why -G \"Visual Studio 17 2022\" doesn't match the Linux setup. These differences get documented in a README, then drift out of date.
CMake 3.19 introduced CMakePresets.json to solve exactly this: a version-controlled file that declares named configure, build, and test presets so cmake --preset=dev replaces the fragile oral tradition.
How Presets Work
A preset bundles generator choice, binary directory, cache variables, and environment variables into a single name. The file lives at the project root and uses a schema version that maps to a minimum CMake release. Schema version 2 (the most common today) requires CMake 3.20 or newer. If your team standardizes on CMake 3.25, declare \"version\": 3 in the file and add cmake_minimum_required(VERSION 3.25) to your top-level CMakeLists.txt so the requirement is enforced at configure time.
Composing Presets with Inheritance
The real power is composition. Define a hidden base preset with shared settings, then specialize per platform or sanitizer without duplication. Hidden presets (names starting with a hyphen) don't appear in cmake --list-presets but serve as mixins.
Worked Example: A Three-Preset Stack
Here's a practical CMakePresets.json for a cross-platform library that builds with Ninja, enables warnings-as-errors by default, and offers AddressSanitizer and MSVC static-analysis variants:
{
\"version\": 3,
\"configurePresets\": [
{
\"name\": \"-base\",
\"hidden\": true,
\"generator\": \"Ninja\",
\"binaryDir\": \"${sourceDir}/build/${presetName}\",
\"cacheVariables\": {
\"CMAKE_CXX_STANDARD\": \"20\",
\"CMAKE_CXX_STANDARD_REQUIRED\": \"ON\",
\"CMAKE_EXPORT_COMPILE_COMMANDS\": \"ON\"
},
\"environment\": {
\"CC\": \"clang\",
\"CXX\": \"clang++\"
}
},
{
\"name\": \"dev\",
\"inherits\": \"-base\",
\"displayName\": \"Developer Debug Build\",
\"description\": \"Debug build with warnings as errors\",
\"cacheVariables\": {
\"CMAKE_BUILD_TYPE\": \"Debug\",
\"CMAKE_CXX_FLAGS\": \"-Wall -Wextra -Werror -Wpedantic\"
},
\"condition\": {
\"type\": \"equals\",
\"lhs\": \"${hostSystemName}\",
\"rhs\": \"Linux\"
}
},
{
\"name\": \"dev-asan\",
\"inherits\": \"dev\",
\"displayName\": \"Developer Debug + AddressSanitizer\",
\"cacheVariables\": {
\"CMAKE_CXX_FLAGS\": \"-Wall -Wextra -Werror -Wpedantic -fsanitize=address -fno-omit-frame-pointer\",
\"CMAKE_EXE_LINKER_FLAGS\": \"-fsanitize=address\",
\"CMAKE_SHARED_LINKER_FLAGS\": \"-fsanitize=address\"
}
},
{
\"name\": \"ci-msvc\",
\"inherits\": \"-base\",
\"displayName\": \"CI Windows Static Analysis\",
\"generator\": \"Ninja\",
\"binaryDir\": \"${sourceDir}/build/${presetName}\",
\"cacheVariables\": {
\"CMAKE_BUILD_TYPE\": \"Release\",
\"CMAKE_CXX_FLAGS\": \"/W4 /WX /analyze\",
\"CMAKE_CXX_COMPILER\": \"cl.exe\"
},
\"environment\": {
\"CC\": \"cl.exe\",
\"CXX\": \"cl.exe\"
},
\"condition\": {
\"type\": \"equals\",
\"lhs\": \"${hostSystemName}\",
\"rhs\": \"Windows\"
}
}
],
\"buildPresets\": [
{
\"name\": \"dev\",
\"configurePreset\": \"dev\",
\"configuration\": \"Debug\"
},
{
\"name\": \"dev-asan\",
\"configurePreset\": \"dev-asan\",
\"configuration\": \"Debug\"
},
{
\"name\": \"ci-msvc\",
\"configurePreset\": \"ci-msvc\",
\"configuration\": \"Release\"
}
],
\"testPresets\": [
{
\"name\": \"dev\",
\"configurePreset\": \"dev\",
\"configuration\": \"Debug\",
\"output\": {\"outputOnFailure\": true}
}
]
}
Run cmake --list-presets in the project root to verify the file parses. You'll see dev, dev-asan, and ci-msvc (on Windows) but not -base. Configure with cmake --preset=dev and build with cmake --build --preset=dev. The same commands work identically in GitHub Actions, GitLab CI, or Azure Pipelines.
Personal Overrides Without Polluting the Repo
Developers often need local tweaks: a different install prefix, a custom toolchain path, or an IDE-specific generator like Xcode or Visual Studio. Create a CMakeUserPresets.json next to the checked-in file (add it to .gitignore). It uses the same schema and can inherit from project presets:
{
\"version\": 3,
\"configurePresets\": [
{
\"name\": \"my-dev\",
\"inherits\": \"dev\",
\"cacheVariables\": {
\"CMAKE_INSTALL_PREFIX\": \"/home/me/.local\"
}
}
]
}
Now cmake --preset=my-dev picks up the shared dev settings plus your personal prefix. The project file stays clean.
Trade-offs and Limitations
Presets standardize invocation but don't replace toolchain management. If a preset assumes clang and a contributor only has GCC, the configure step fails — presets don't install compilers. Document the required toolchain separately.
Older CMake versions silently ignore or misparse newer schema files. Pin the minimum version in documentation and in cmake_minimum_required(). A contributor running CMake 3.16 on an old distro will get a confusing error rather than a helpful message.
Over-stuffing presets with project logic hides configuration from CMakeLists.txt, making the build harder to reason about. Keep presets focused on how to invoke the build (generator, flags, environment), not what the build does (target definitions, find_package logic).
Verify Before You Commit
- Run
cmake --list-presetsin a clean checkout. Confirm all intended presets appear and hidden ones don't. - Configure with
cmake --preset=dev(or your primary preset) in a fresh build directory. Inspectbuild/dev/CMakeCache.txtto verify cache variables and generator match the preset. - Run the same preset in CI and locally, then diff the two
CMakeCache.txtfiles. Unexpected divergence usually means an environment variable or toolchain difference not captured in the preset. - Check the
versionfield againstcmake --versionon your oldest supported contributor machine. If you need to support CMake 3.20, use schema version 2.
Once verified, the preset becomes the single source of truth for how the project is built — locally, in CI, and in any IDE that supports the format (VS Code, CLion, Visual Studio 2022+).
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.