C++ Module 15 – Build systems, testing and tooling
CMake for C++: targets, properties and presets
Build a C++ project with CMake 4 and Ninja: libraries and programs as targets, PRIVATE and PUBLIC settings, C++23 per target and presets for every build.
What you will learn
- Build a project with CMake and Ninja in a separate build folder
- Set the C++ standard and the warnings per target, and pass them on with PUBLIC and PRIVATE
- Store debug, release and sanitizer builds in CMakePresets.json for everyone and for CI
- Fix a project that CMake 4 refuses because of an old cmake_minimum_required
Before you start
What you’ll learn · Before you start
What you will learn
- Build a project with CMake and Ninja in a separate build folder
- Set the C++ standard and the warnings per target, and pass them on with PUBLIC and PRIVATE
- Store debug, release and sanitizer builds in CMakePresets.json for everyone and for CI
- Fix a project that CMake 4 refuses because of an old cmake_minimum_required
Before you start
On this page
Building even a small C++ project by hand takes care. The library and the program of this lesson need four commands
in a fixed order, each compile with the same five flags, and a team wants that build again in debug, release and
sanitizer versions, on more than one compiler. CMake writes those commands for you. It is a
build system generator: A tool that writes the files a build tool runs, instead of compiling anything itself. CMake is one: from CMakeLists.txt it writes build.ninja for Ninja, Makefiles for Make or a solution for Visual Studio.: it compiles nothing itself, but reads the project’s
CMakeLists.txt files and writes the files that a build tool such as Ninja, Make or Visual Studio needs, and that
tool then runs the compiler.
You will configure and build in a folder of your own, describe the code as targets whose settings pass along links, build it with two compilers, name every build in a preset, and fix a project that CMake 4 refuses. Every command ran with CMake 4.4.4 and Ninja 1.13.2, on Apple clang 21 and GCC 16, and the page shows what each one printed.
Two steps: configure a build folder, then build in it
Three commands make a complete project. cmake_minimum_required says which CMake versions the file was written for,
project names the project and its languages, and add_executable declares a program and its source files:
cmake_minimum_required(VERSION 3.28...4.4)
project(hello LANGUAGES CXX)
add_executable(hello main.cpp)
target_compile_features(hello PRIVATE cxx_std_23) #include <print>
int main() {
std::println("Hello from a CMake build, C++ {}", __cplusplus);
} target_compile_features(hello PRIVATE cxx_std_23) asks for at least C++23 on this one target, and CMake turns that
into the right flag for whichever compiler it finds. The version range 3.28...4.4 means the file needs CMake 3.28
or later and has been checked with versions up to 4.4, so CMake applies the newer behaviour of every release up to
4.4.
CMake is then used in two steps. cmake -S . -B build -G Ninja configures: it reads the project in . and
writes the build files into a separate folder, build. cmake --build build builds in that folder. Configure,
build and run:
from session import run
# Configure into a separate build folder, build and run; then the compiler command CMake chose, and the build folder.
run("""
cd hello
cmake -S . -B build -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
cmake --build build
./build/hello
grep '"command"' build/compile_commands.json
ls build
""") Output
$ cd hello $ cmake -S . -B build -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -- The CXX compiler identification is AppleClang 21.0.0.21000334 -- Detecting CXX compiler ABI info -- Detecting CXX compiler ABI info - done -- Check for working CXX compiler: /usr/bin/c++ - skipped -- Detecting CXX compile features -- Detecting CXX compile features - done -- Configuring done (…s) -- Generating done (…s) -- Build files have been written to: build $ cmake --build build [1/2] Building CXX object CMakeFiles/hello.dir/main.cpp.o [2/2] Linking CXX executable hello $ ./build/hello Hello from a CMake build, C++ 202302 $ grep '"command"' build/compile_commands.json "command": "/usr/bin/c++ -std=gnu++23 -o CMakeFiles/hello.dir/main.cpp.o -c main.cpp", $ ls build build.ninja cmake_install.cmake CMakeCache.txt CMakeFiles compile_commands.json hello
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 hello_session.py
Here is that session again, stage by stage, with what each command left in build:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The project.
CMakeLists.txtnames the CMake versions it was written for, the project and its language, and one program,hello, built frommain.cppwith at least C++23. - Configure.
cmake -S . -B build -G Ninjareads the project in.and works inbuild; the session also passed-DCMAKE_EXPORT_COMPILE_COMMANDS=ON. It found the compiler, AppleClang 21, and checked what it supports. None of the project's files is compiled yet. - The build files. Configuring wrote
build.ninja, the script Ninja runs, andCMakeCache.txt, the answers it found, such as the compiler, pluscompile_commands.json, which that option asked for. Nothing was written next to the sources. - Build.
cmake --build buildruns the build tool the folder was configured for, Ninja here. The same command builds a folder configured for Make or Visual Studio. - Compile and link. Ninja ran two steps:
[1/2]compiledmain.cppwith-std=gnu++23, the GNU dialect of C++23, and[2/2]linked the programhello. - Run. The program printed
Hello from a CMake build, C++ 202302, and 202302 is the value of__cplusplusin C++23. To build again from nothing, delete the folderbuildand configure again.
The configure step found the compiler (AppleClang 21), checked what it supports and wrote the build files into
build. The build step ran Ninja, which compiled main.cpp and linked hello. CMakeCache.txt keeps the answers
of the configure step, such as which compiler to use, so later configures are quick; build.ninja is the generated
build script. Nothing was written next to the sources, which is why this is called an
out-of-source build: A build whose generated files, object files and programs all go into a folder of their own, such as build, and never next to the sources. Deleting that folder starts the build again from nothing.: to start again, delete the build folder, and never commit
it.
Two details of the recorded output matter later. The compile command says -std=gnu++23, not -std=c++23: unless a
project says otherwise, CMake asks for the compiler’s GNU dialect: A compiler mode named gnu++ and an edition, such as gnu++20: that edition of standard C++ plus some of GCC's extensions, which Clang accepts too. -std=c++20 asks for the standard alone. More in “Install a C++23 compiler on Windows, macOS or Linux”, which accepts
extensions such as variable-length arrays. And without -G, CMake falls back to a default generator: on macOS that
is Unix Makefiles, the one cmake --help marks with an asterisk. Ninja’s files are meant to be generated by tools
like CMake rather than written by hand; it starts quickly and runs builds in parallel, with as many jobs as the
number of CPUs suggests.
Checkpoint
Check: Configure, then build, in a folder of its own
PUBLIC settings travel along a link, PRIVATE ones stay
A real project splits its code into libraries. In CMakeLists.txt each library and each program is a
target (CMake): A library or program that a CMakeLists.txt declares with add_library or add_executable, together with its properties: its sources, include folders, C++ standard, flags and the targets it links., and each target carries properties such as its include folders, its C++
standard and its warning flags. This project has a library, textstats, that counts words, and a program,
wordfreq, that prints the most frequent words of a file:
cmake_minimum_required(VERSION 3.28...4.4)
project(wordfreq VERSION 1.0 LANGUAGES CXX)
# Standard C++ (-std=c++23) rather than the GNU dialect (-std=gnu++23) when a target asks for C++23.
set(CMAKE_CXX_EXTENSIONS OFF)
# The warnings this project's own code is built with. An INTERFACE library builds nothing: it only carries settings
# to the targets that link it.
add_library(wordfreq_warnings INTERFACE)
if(MSVC)
target_compile_options(wordfreq_warnings INTERFACE /W4 /permissive-)
else()
target_compile_options(wordfreq_warnings INTERFACE -Wall -Wextra -Wpedantic)
endif()
add_subdirectory(textstats)
add_subdirectory(app) add_library(textstats src/textstats.cpp)
# PUBLIC: textstats needs these, and so does every target that links textstats.
target_include_directories(textstats PUBLIC include)
target_compile_features(textstats PUBLIC cxx_std_23)
# PRIVATE: only textstats itself is built with these warnings.
target_link_libraries(textstats PRIVATE wordfreq_warnings) add_executable(wordfreq main.cpp)
target_link_libraries(wordfreq PRIVATE textstats wordfreq_warnings) C++ header · wordfreq/textstats/include/textstats/textstats.hpp
#pragma once
#include <cstddef>
#include <map>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace textstats {
// How often each word occurs in text, ignoring case. A word is a run of ASCII letters.
std::map<std::string, int> count_words(std::string_view text);
// The n most frequent words, most frequent first; words with the same count stay in alphabetical order.
std::vector<std::pair<std::string, int>> top_words(const std::map<std::string, int>& counts, std::size_t n);
} // namespace textstats C++ (textstats.cpp) · wordfreq/textstats/src/textstats.cpp
#include "textstats/textstats.hpp"
#include <algorithm>
#include <cctype>
namespace textstats {
std::map<std::string, int> count_words(std::string_view text) {
std::map<std::string, int> counts;
std::string word;
for (char c : text) {
const auto u = static_cast<unsigned char>(c);
if (std::isalpha(u)) {
word += static_cast<char>(std::tolower(u));
} else if (!word.empty()) {
++counts[word];
word.clear();
}
}
if (!word.empty()) ++counts[word];
return counts;
}
std::vector<std::pair<std::string, int>> top_words(const std::map<std::string, int>& counts, std::size_t n) {
std::vector<std::pair<std::string, int>> words(counts.begin(), counts.end());
std::ranges::stable_sort(words, [](const auto& a, const auto& b) { return a.second > b.second; });
if (words.size() > n) words.resize(n);
return words;
}
} // namespace textstats C++ (main.cpp) · wordfreq/app/main.cpp
#include <fstream>
#include <print>
#include <sstream>
#include "textstats/textstats.hpp"
int main(int argc, char* argv[]) {
if (argc != 2) {
std::println(stderr, "usage: wordfreq <file>");
return 2;
}
std::ifstream in(argv[1]);
if (!in) {
std::println(stderr, "wordfreq: cannot open {}", argv[1]);
return 1;
}
std::stringstream text;
text << in.rdbuf();
const auto counts = textstats::count_words(text.str());
std::println("{} different words", counts.size());
for (const auto& [word, n] : textstats::top_words(counts, 3)) {
std::println("{:>3} {}", n, word);
}
} Every target_* command takes a keyword that says who the setting is for:
| Keyword | The target itself | Targets that link it | Typical use |
|---|---|---|---|
PRIVATE |
Yes | No | Warnings, source-only include folders, libraries used only inside the .cpp files |
PUBLIC |
Yes | Yes | The folder of the library’s own headers, a C++ standard its headers need |
INTERFACE |
No | Yes | Header-only libraries, and settings-only targets such as wordfreq_warnings |
What each target carries, and what a link passes on
Text description of the diagram
The diagram shows the three targets of the wordfreq project and the links between them.
- wordfreq_warnings is an INTERFACE library. It builds nothing; it only carries the warning flags -Wall, -Wextra and -Wpedantic to whatever links it.
- textstats is a static library. It declares its include/ folder and C++23 as PUBLIC, so they apply to textstats and to everything that links it. It links wordfreq_warnings PRIVATE, so it is built with the warnings.
- wordfreq is the executable. It links textstats PRIVATE and receives textstats's public settings, the include/ folder and C++23. It also links wordfreq_warnings PRIVATE, so it is built with the warnings too.
Only PUBLIC and INTERFACE settings travel along a link. The warnings reach both targets because each one links wordfreq_warnings itself.
The settings that pass along a link are the target’s usage requirements: The settings a CMake target passes to every target that links it: its PUBLIC and INTERFACE include folders, definitions, compile options and features. Its PRIVATE settings stay with the target itself..
textstats declares its include folder and C++23 as PUBLIC, because any code that includes
textstats/textstats.hpp needs both. Its warnings are a private matter: they come from wordfreq_warnings, an
INTERFACE library: A CMake target that builds nothing of its own and only carries settings, such as warning flags or the include folder of a header-only library, to the targets that link it. that builds nothing and only carries flags to whatever links
it. Here is the build, with the compiler command of each file read from compile_commands.json, the
compilation database: The file compile_commands.json: the exact compiler command of every source file of a build. CMake writes it when CMAKE_EXPORT_COMPILE_COMMANDS is ON, and tools such as clang-tidy and editors read it. that -DCMAKE_EXPORT_COMPILE_COMMANDS=ON asks for:
from session import run
# Configure with Apple clang, writing compile_commands.json too, then build and run.
run("""
cd wordfreq
cmake -S . -B build -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
""", keep=r"^(?!-- (?:Detecting|Check for working))")
run("""
cmake --build build
grep '"command"' build/compile_commands.json
./build/app/wordfreq sample.txt
""") Output
$ cd wordfreq $ cmake -S . -B build -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -- The CXX compiler identification is AppleClang 21.0.0.21000334 -- Configuring done (…s) -- Generating done (…s) -- Build files have been written to: build [5 lines not shown] $ cmake --build build [1/4] Building CXX object textstats/CMakeFiles/textstats.dir/src/textstats.cpp.o [2/4] Linking CXX static library textstats/libtextstats.a [3/4] Building CXX object app/CMakeFiles/wordfreq.dir/main.cpp.o [4/4] Linking CXX executable app/wordfreq $ grep '"command"' build/compile_commands.json "command": "/usr/bin/c++ -Itextstats/include -std=c++23 -Wall -Wextra -Wpedantic -o textstats/CMakeFiles/textstats.dir/src/textstats.cpp.o -c textstats/src/textstats.cpp", "command": "/usr/bin/c++ -Itextstats/include -std=c++23 -Wall -Wextra -Wpedantic -o app/CMakeFiles/wordfreq.dir/main.cpp.o -c app/main.cpp", $ ./build/app/wordfreq sample.txt 26 different words 8 the 3 and 3 tea
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 targets_session.py
The command for app/main.cpp received -Itextstats/include and -std=c++23 from textstats, without the app’s
CMakeLists.txt naming either; it got the warning flags because it links wordfreq_warnings itself. The standard is
-std=c++23 this time because the top-level file sets CMAKE_CXX_EXTENSIONS to OFF, which makes every target ask
for standard C++ instead of the GNU dialect. Ninja also built the targets in order: the library had to exist before the
program could link against libtextstats.a. Step through how each flag of the two commands got there:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Three targets.
wordfreq_warningsis an INTERFACE library: it builds nothing and carries-Wall -Wextra -Wpedanticto whatever links it.textstatsis the library that counts words,wordfreqthe program. - PUBLIC settings.
textstatsdeclares itsincludefolder and C++23 asPUBLIC. Its own source file gets both:-Itextstats/include -std=c++23. - Warnings for the library.
textstatslinkswordfreq_warningsPRIVATE, so its own command adds the three warning flags.PRIVATEmeans they stop there. - A link passes PUBLIC on.
wordfreqlinkstextstats. ThePUBLICsettings travel along the link, somain.cppgets-Itextstats/include -std=c++23without the app'sCMakeLists.txtnaming either. - The program's own warnings. The warnings of
textstatsdid not travel: its link to them isPRIVATE.main.cpphas them becausewordfreqlinkswordfreq_warningsitself. - One keyword changed. With the folder made
PRIVATEtotextstats, as the next session does, the library still compiles, but themain.cppcommand has no-Iat all and stops attextstats/textstats.hppfile not found.
One wrong keyword, reproduced
Usage requirements are easiest to understand when one is missing. This session changes the library’s include folder
from PUBLIC to PRIVATE and builds again:
from session import run
# Make the include folder PRIVATE to textstats, then build again: the app can no longer find the header.
run("""
cd wordfreq
sed -i.orig 's/textstats PUBLIC include/textstats PRIVATE include/' textstats/CMakeLists.txt
grep -n 'include)' textstats/CMakeLists.txt
cmake -S . -B build -G Ninja
cmake --build build
""", keep=r"^(?!-- (?:Detecting|Check for working|The CXX|Configuring|Generating|Build files))") Output
$ cd wordfreq
$ sed -i.orig 's/textstats PUBLIC include/textstats PRIVATE include/' textstats/CMakeLists.txt
$ grep -n 'include)' textstats/CMakeLists.txt
4:target_include_directories(textstats PRIVATE include)
$ cmake -S . -B build -G Ninja
[9 lines not shown]
$ cmake --build build
[1/4] Building CXX object textstats/CMakeFiles/textstats.dir/src/textstats.cpp.o
[2/4] Linking CXX static library textstats/libtextstats.a
[3/4] Building CXX object app/CMakeFiles/wordfreq.dir/main.cpp.o
FAILED: [code=1] app/CMakeFiles/wordfreq.dir/main.cpp.o
/usr/bin/c++ -std=c++23 -Wall -Wextra -Wpedantic -MD -MT app/CMakeFiles/wordfreq.dir/main.cpp.o -MF app/CMakeFiles/wordfreq.dir/main.cpp.o.d -o app/CMakeFiles/wordfreq.dir/main.cpp.o -c app/main.cpp
app/main.cpp:5:10: fatal error: 'textstats/textstats.hpp' file not found
5 | #include "textstats/textstats.hpp"
| ^~~~~~~~~~~~~~~~~~~~~~~~~
1 error generated.
ninja: build stopped: subcommand failed.
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 private_session.py
The library still compiled, because a PRIVATE folder is still used for the library’s own sources. The program
failed: its compile command has no -I at all, so #include "textstats/textstats.hpp" finds nothing. When a header
“is not found” only in the code that uses a library, look first at how the library declares its include folder.
Key idea
A setting that a library’s headers need is PUBLIC, one that only its .cpp files need is PRIVATE, and one that
only its users need is INTERFACE. A PUBLIC setting that is missing shows up as a missing header in the users’
code, never in the library’s.
Checkpoint
Check: What PRIVATE, PUBLIC and INTERFACE pass on
A second build folder gives a second compiler
The compiler is chosen when a build folder is configured, so a second folder gives a second compiler with no change to the project:
from session import run
# The same CMakeLists.txt with GCC 16, in a second build folder: the compiler is chosen when a folder is configured.
run("""
cd wordfreq
cmake -S . -B build-gcc -G Ninja -DCMAKE_CXX_COMPILER=g++-16
cmake --build build-gcc
./build-gcc/app/wordfreq sample.txt
""", keep=r"^(?!-- (?:Detecting|Check for working))") Output
$ cd wordfreq $ cmake -S . -B build-gcc -G Ninja -DCMAKE_CXX_COMPILER=g++-16 -- The CXX compiler identification is GNU 16.2.0 -- Checking whether CXX compiler has -isysroot -- Checking whether CXX compiler has -isysroot - yes -- Checking whether CXX compiler supports OSX deployment target flag -- Checking whether CXX compiler supports OSX deployment target flag - yes -- Configuring done (…s) -- Generating done (…s) -- Build files have been written to: build-gcc [5 lines not shown] $ cmake --build build-gcc [1/8] Scanning textstats/src/textstats.cpp for CXX dependencies [2/8] Generating CXX dyndep file textstats/CMakeFiles/textstats.dir/CXX.dd [3/8] Scanning app/main.cpp for CXX dependencies [4/8] Building CXX object textstats/CMakeFiles/textstats.dir/src/textstats.cpp.o [5/8] Generating CXX dyndep file app/CMakeFiles/wordfreq.dir/CXX.dd [6/8] Linking CXX static library textstats/libtextstats.a [7/8] Building CXX object app/CMakeFiles/wordfreq.dir/main.cpp.o [8/8] Linking CXX executable app/wordfreq $ ./build-gcc/app/wordfreq sample.txt 26 different words 8 the 3 and 3 tea
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 gcc_session.py
Both builds printed the same three lines. GCC’s build had 8 steps instead of 4, though, and the extra ones are worth recognising:
| Build steps | Apple clang 21 | GCC 16 |
|---|---|---|
Scans of a source file for import |
0 | 2, one per .cpp file |
Dependency files (CXX.dd) |
0 | 2, one per target |
| Compiles, the archive and the link | 4 | 4 |
| All steps | 4 | 8 |
Since CMake 3.28 (policy CMP0155), sources of targets that use C++20 or later are scanned for import statements
before they are compiled, when the compiler and the generator support it. CMake’s list of compilers it can scan with
names GCC 14 and newer but not Apple clang, so the GCC build scanned every file and wrote a dependency file, while the
Apple clang build had no scan steps. A project that uses no C++ modules can switch the scan off with
set(CMAKE_CXX_SCAN_FOR_MODULES OFF) near the top of its CMakeLists.txt, or -DCMAKE_CXX_SCAN_FOR_MODULES=OFF for
one build folder, as the exercise’s GCC build below does, and goes back to 4 steps.
Checkpoint
Check: One project, two compilers
Settings belong on targets, not on the whole build
Older CMake code sets flags for everything at once. The target_* commands replace each of those commands:
| Older, for every target in the folder and below | Newer, for one target and what links it |
|---|---|
include_directories(include) |
target_include_directories(textstats PUBLIC include) |
add_compile_options(-Wall) or set(CMAKE_CXX_FLAGS "-Wall") |
target_compile_options(textstats PRIVATE -Wall) |
add_definitions(-DUSE_FAST_PATH) |
target_compile_definitions(textstats PRIVATE USE_FAST_PATH) |
set(CMAKE_CXX_STANDARD 23) |
target_compile_features(textstats PUBLIC cxx_std_23) |
link_libraries(m) |
target_link_libraries(textstats PRIVATE m) |
The difference shows up as soon as a project contains someone else’s code. A global -Werror or warning flag also
lands on a library added with add_subdirectory, whose warnings you cannot fix; a global include folder hides which
target needs it, so it survives long after the code that needed it is gone. With target commands, each setting lives
next to the code that needs it, and the keywords decide where it travels. set(CMAKE_CXX_STANDARD 23) remains
reasonable in a small program, but a library should state what its headers need with target_compile_features, so its
users get it automatically.
Where a setting goes
Some settings really do describe a whole build: the build type, the compiler, sanitizers, a CI-only -Werror.
Those belong on the command line or in a preset, not in CMakeLists.txt. Everything the code itself needs goes on
its target, with the keyword that says who it is for.
Checkpoint
Check: Settings on targets, not on the whole build
Presets give every build a name
Typing -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXPORT_COMPILE_COMMANDS=ON every time invites mistakes.
preset (CMake): A named set of options in CMakePresets.json or CMakeUserPresets.json, such as the generator, the build folder and cache variables, so that one command such as cmake --preset release runs that whole configure step., stored in CMakePresets.json, give those configure options names, so a
debug, release or sanitizer: A compiler option, such as -fsanitize=address or -fsanitize=undefined, that builds extra checks into a program, so that memory errors and undefined behaviour are reported, with their source line, while it runs. More in “Finding memory bugs with AddressSanitizer and UBSan” build is one command for everyone on the team and in CI:
{
"version": 6,
"cmakeMinimumRequired": { "major": 3, "minor": 28, "patch": 0 },
"configurePresets": [
{
"name": "base",
"hidden": true,
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"cacheVariables": { "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" }
},
{
"name": "debug",
"displayName": "Debug: no optimisation, debug information",
"inherits": "base",
"cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" }
},
{
"name": "release",
"displayName": "Release: optimised (-O3), no debug information",
"inherits": "base",
"cacheVariables": { "CMAKE_BUILD_TYPE": "Release" }
},
{
"name": "asan-ubsan",
"displayName": "Debug with AddressSanitizer and UndefinedBehaviorSanitizer",
"inherits": "debug",
"cacheVariables": {
"CMAKE_CXX_FLAGS": "-fsanitize=address,undefined -fno-omit-frame-pointer",
"CMAKE_EXE_LINKER_FLAGS": "-fsanitize=address,undefined"
}
}
],
"buildPresets": [
{ "name": "debug", "configurePreset": "debug" },
{ "name": "release", "configurePreset": "release" },
{ "name": "asan-ubsan", "configurePreset": "asan-ubsan" }
]
} {
"version": 6,
"configurePresets": [
{
"name": "gcc-debug",
"displayName": "Debug with GCC 16 (this computer only)",
"inherits": "debug",
"cacheVariables": { "CMAKE_CXX_COMPILER": "g++-16" }
}
],
"buildPresets": [
{ "name": "gcc-debug", "configurePreset": "gcc-debug" }
]
} "version": 6is the file format, which CMake 3.25 and later read; CMake 4.4 reads up to version 12.baseishidden, so it cannot be chosen by itself; the other configure presetsinheritsit and add a build type.${sourceDir}and${presetName}are macros, so each preset gets its own folder such asbuild/release.buildPresetsname the build step of a configure preset, socmake --build --preset releaseneeds no folder name.CMakeUserPresets.jsonholds what is true only on your computer, here a GCC 16 build. The CMake documentation says it should not be checked in; add it to.gitignore. It can inherit presets from the shared file.
from session import run
# The presets of CMakePresets.json (shared) and CMakeUserPresets.json (this computer only).
run("""
cd wordfreq
cmake --list-presets
cmake --preset release
cmake --build --preset release
./build/release/app/wordfreq sample.txt
grep -m1 '"command"' build/release/compile_commands.json
cmake --preset asan-ubsan
cmake --build --preset asan-ubsan
grep -m1 '"command"' build/asan-ubsan/compile_commands.json
otool -L build/asan-ubsan/app/wordfreq
cmake --preset gcc-debug
cmake --build --preset gcc-debug
./build/gcc-debug/app/wordfreq sample.txt
""", keep=r"^(?!-- (?:Detecting|Check|Configuring|Generating))") Output
$ cd wordfreq $ cmake --list-presets Available configure presets: "gcc-debug" - Debug with GCC 16 (this computer only) "debug" - Debug: no optimisation, debug information "release" - Release: optimised (-O3), no debug information "asan-ubsan" - Debug with AddressSanitizer and UndefinedBehaviorSanitizer $ cmake --preset release -- The CXX compiler identification is AppleClang 21.0.0.21000334 -- Build files have been written to: build/release [7 lines not shown] $ cmake --build --preset release [1/4] Building CXX object textstats/CMakeFiles/textstats.dir/src/textstats.cpp.o [2/4] Linking CXX static library textstats/libtextstats.a [3/4] Building CXX object app/CMakeFiles/wordfreq.dir/main.cpp.o [4/4] Linking CXX executable app/wordfreq $ ./build/release/app/wordfreq sample.txt 26 different words 8 the 3 and 3 tea $ grep -m1 '"command"' build/release/compile_commands.json "command": "/usr/bin/c++ -Itextstats/include -O3 -DNDEBUG -std=c++23 -Wall -Wextra -Wpedantic -o textstats/CMakeFiles/textstats.dir/src/textstats.cpp.o -c textstats/src/textstats.cpp", $ cmake --preset asan-ubsan -- The CXX compiler identification is AppleClang 21.0.0.21000334 -- Build files have been written to: build/asan-ubsan [7 lines not shown] $ cmake --build --preset asan-ubsan [1/4] Building CXX object textstats/CMakeFiles/textstats.dir/src/textstats.cpp.o [2/4] Linking CXX static library textstats/libtextstats.a [3/4] Building CXX object app/CMakeFiles/wordfreq.dir/main.cpp.o [4/4] Linking CXX executable app/wordfreq $ grep -m1 '"command"' build/asan-ubsan/compile_commands.json "command": "/usr/bin/c++ -Itextstats/include -fsanitize=address,undefined -fno-omit-frame-pointer -g -std=c++23 -Wall -Wextra -Wpedantic -o textstats/CMakeFiles/textstats.dir/src/textstats.cpp.o -c textstats/src/textstats.cpp", $ otool -L build/asan-ubsan/app/wordfreq build/asan-ubsan/app/wordfreq: /usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 2200.27.0) @rpath/libclang_rt.asan_osx_dynamic.dylib (compatibility version 0.0.0, current version 0.0.0) /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1359.0.0) $ cmake --preset gcc-debug -- The CXX compiler identification is GNU 16.2.0 -- Build files have been written to: build/gcc-debug [11 lines not shown] $ cmake --build --preset gcc-debug [1/8] Scanning textstats/src/textstats.cpp for CXX dependencies [2/8] Generating CXX dyndep file textstats/CMakeFiles/textstats.dir/CXX.dd [3/8] Scanning app/main.cpp for CXX dependencies [4/8] Building CXX object textstats/CMakeFiles/textstats.dir/src/textstats.cpp.o [5/8] Generating CXX dyndep file app/CMakeFiles/wordfreq.dir/CXX.dd [6/8] Linking CXX static library textstats/libtextstats.a [7/8] Building CXX object app/CMakeFiles/wordfreq.dir/main.cpp.o [8/8] Linking CXX executable app/wordfreq $ ./build/gcc-debug/app/wordfreq sample.txt 26 different words 8 the 3 and 3 tea
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 presets_session.py
cmake --list-presets showed all four configure presets, the user file’s first. The release build was compiled with
-O3 -DNDEBUG, CMake’s own release flags for GCC and Clang, and the debug-based sanitizer build with -g and the
sanitizer flags from the preset; otool -L confirms that the program loads AddressSanitizer’s run-time library. Step
through the family of presets, with what each build used:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The hidden base.
baseholds what every build shares: the Ninja generator, the folderbuild/${presetName}andCMAKE_EXPORT_COMPILE_COMMANDS. It is hidden, so it can only be inherited, andcmake --list-presetsdoes not show it. - debug.
debuginheritsbaseand setsCMAKE_BUILD_TYPEtoDebug. The macro${presetName}gives it a folder of its own,build/debug. - release.
releaseinheritsbasewith theReleasetype. Its compile commands carried-O3 -DNDEBUG, CMake's own release flags for GCC and Clang. - asan-ubsan.
asan-ubsaninheritsdebugand adds the sanitizer flags. Its commands carried-fsanitize=address,undefined -fno-omit-frame-pointer -g, and the program loads AddressSanitizer's run-time library. - gcc-debug.
gcc-debugis inCMakeUserPresets.json, on this computer only. It inheritsdebugfrom the shared file and setsCMAKE_CXX_COMPILERtog++-16; its build took 8 steps, with the module scans. - Build presets. Each entry of
buildPresetsnames a configure preset, socmake --build --preset releasebuildsbuild/releasewith no folder name. The list showed four configure presets, the user file's first.
Visual Studio and Visual Studio Code’s CMake Tools read the same CMakePresets.json, so an editor and a CI script run
exactly the builds you run in a terminal.
Checkpoint
Check: Presets for every build
CMake 4 refuses a minimum below 3.5
CMake 4.0 removed compatibility with CMake versions older than 3.5, and many older projects still begin with a line
such as cmake_minimum_required(VERSION 2.8.12). Here is what CMake 4.4.4 does with one:
cmake_minimum_required(VERSION 2.8.12)
project(legacy CXX)
add_executable(legacy main.cpp) from session import run
# A project written for CMake 2.8, configured with CMake 4.4.4: first as it is, then with the stopgap variable.
run("""
cd legacy
cmake --version
cmake -S . -B build -G Ninja
cmake -S . -B build -G Ninja -DCMAKE_POLICY_VERSION_MINIMUM=3.5
cmake --build build
./build/legacy
""", keep=r"^(?!-- (?:Detecting|Check for working))") Output
$ cd legacy $ cmake --version cmake version 4.4.4 CMake suite maintained and supported by Kitware (kitware.com/cmake). $ cmake -S . -B build -G Ninja CMake Error at CMakeLists.txt:1 (cmake_minimum_required): Compatibility with CMake < 3.5 has been removed from CMake. Update the VERSION argument <min> value. Or, use the <min>...<max> syntax to tell CMake that the project requires at least <min> but has been updated to work with policies introduced by <max> or earlier. Or, add -DCMAKE_POLICY_VERSION_MINIMUM=3.5 to try configuring anyway. -- Configuring incomplete, errors occurred! [exit status 1] $ cmake -S . -B build -G Ninja -DCMAKE_POLICY_VERSION_MINIMUM=3.5 CMake Warning (deprecated) at CMakeLists.txt:1 (cmake_minimum_required): Compatibility with CMake < 3.10 will be removed from a future version of CMake. Update the VERSION argument <min> value. Or, use the <min>...<max> syntax to tell CMake that the project requires at least <min> but has been updated to work with policies introduced by <max> or earlier. This warning is for project developers. Use -Wno-author or -Wno-deprecated to suppress it. -- The CXX compiler identification is AppleClang 21.0.0.21000334 -- Configuring done (…s) -- Generating done (…s) -- Build files have been written to: build [5 lines not shown] $ cmake --build build [1/2] Building CXX object CMakeFiles/legacy.dir/main.cpp.o [2/2] Linking CXX executable legacy $ ./build/legacy An old project, built with a new CMake
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 legacy_session.py
Configure stops with an error, and the message lists the ways out. For your own project, raise the minimum and give a
range, such as cmake_minimum_required(VERSION 3.10...4.4), after checking that the project builds. For a project
you do not maintain, -DCMAKE_POLICY_VERSION_MINIMUM=3.5 (or an environment variable of the same name) lets it
configure anyway. The recorded run shows why that is only a stopgap: CMake already warns that compatibility with
versions older than 3.10 will be removed in a future release.
Each behaviour that changed between releases is a policy (CMake): A behaviour that changed between CMake releases, named CMP and a number, such as CMP0155. cmake_minimum_required decides which releases' behaviour a project gets, so a newer CMake keeps building an older file the same way., named CMP and a number,
such as the module scan’s CMP0155, and the minimum decides which releases’ policies a project gets. Walk through the
releases this lesson’s files depend on:
- 3.5
The oldest minimum CMake 4 accepts
CMake 4.0 removed compatibility with every release before 3.5, so
cmake_minimum_requiredhas to ask for 3.5 or later. The legacy project asks for 2.8.12 and stops at its first line. - 3.12
A range in cmake_minimum_required new syntax
A minimum may be followed by
...and a second version, as in3.28...4.4. The file then needs only the first; a newer CMake applies the behaviour of every release up to the second. - 3.19
Presets arrive
CMakePresets.jsonandCMakeUserPresets.jsonappeared with version 1 of the presets format, which held configure presets only. - 3.20
Build and test presets
Version 2 of the presets format added
buildPresetsandtestPresets, socmake --build --preset releaseneeds no folder name. - 3.25
Presets version 6
The version this lesson's preset files use. It added workflow presets, which chain other presets into one command,
cmake --workflow. - 3.28
Sources scanned for modules
Policy CMP0155: sources of targets that use C++20 or later are scanned for
importwhen the compiler can scan them. It is why the GCC 16 builds took 8 steps. The lesson's files ask for at least 3.28. - 3.31
A warning below 3.10 deprecation
A minimum below 3.10 now brings a deprecation warning: compatibility with those releases will be removed in a future version. The legacy run printed it.
- 4.0
Old minimums become errors breaking change
Compatibility with releases before 3.5 was removed, and
CMAKE_POLICY_VERSION_MINIMUMwas added so that packagers and users can still try to configure projects that were not updated. - 4.4
The release on this page
Every session of this lesson ran CMake 4.4.4. It reads preset files up to version 12, and it is the newest release the lesson's files name in their range,
3.28...4.4.
The timeline has 9 events, from 3.5 to 4.4.
Checkpoint
Check: Old minimums and CMake 4
Practise: write the CMakeLists.txt
A small geometry project has three source files and a preset file, but no CMakeLists.txt:
C++ header · exercise/include/shapes/shapes.hpp
#pragma once
namespace shapes {
struct Circle {
double radius;
};
struct Rectangle {
double width;
double height;
};
double area(const Circle& c);
double area(const Rectangle& r);
} // namespace shapes C++ (shapes.cpp) · exercise/src/shapes.cpp
#include "shapes/shapes.hpp"
#include <numbers>
namespace shapes {
double area(const Circle& c) { return std::numbers::pi * c.radius * c.radius; }
double area(const Rectangle& r) { return r.width * r.height; }
} // namespace shapes C++ (main.cpp) · exercise/app/main.cpp
#include <print>
#include "shapes/shapes.hpp"
int main() {
std::println("circle of radius 2: {:.2f}", shapes::area(shapes::Circle{2.0}));
std::println("3 by 4.5 rectangle: {:.2f}", shapes::area(shapes::Rectangle{3.0, 4.5}));
} JSON · exercise/CMakePresets.json
{
"version": 6,
"configurePresets": [
{
"name": "debug",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" }
},
{
"name": "release",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"cacheVariables": { "CMAKE_BUILD_TYPE": "Release" }
}
],
"buildPresets": [
{ "name": "debug", "configurePreset": "debug" },
{ "name": "release", "configurePreset": "release" }
]
} Write CMakeLists.txt so that these commands work, and the program prints circle of radius 2: 12.57 and
3 by 4.5 rectangle: 13.50:
cmake --preset debug,cmake --build --preset debugand./build/debug/area- the same with the
releasepreset - a GCC build in a folder of its own, with
-DCMAKE_CXX_COMPILER=g++-16(or your GCC)
Make shapes a library and area the program, compile with standard C++23 rather than the GNU dialect, and turn on
warnings for the library’s code. Hint: app/main.cpp includes shapes/shapes.hpp, so think about which keyword lets
the program see the library’s include folder.
One solution
cmake_minimum_required(VERSION 3.28...4.4)
project(area LANGUAGES CXX)
set(CMAKE_CXX_EXTENSIONS OFF)
add_library(shapes src/shapes.cpp)
target_include_directories(shapes PUBLIC include)
target_compile_features(shapes PUBLIC cxx_std_23)
if(MSVC)
target_compile_options(shapes PRIVATE /W4 /permissive-)
else()
target_compile_options(shapes PRIVATE -Wall -Wextra -Wpedantic)
endif()
add_executable(area app/main.cpp)
target_link_libraries(area PRIVATE shapes) from session import run
# The solution's CMakeLists.txt with the two presets the exercise gives, then with GCC 16 in a folder of its own.
run("""
cd exercise
cmake --preset debug
cmake --build --preset debug
./build/debug/area
cmake --preset release
cmake --build --preset release
./build/release/area
cmake -S . -B build/gcc -G Ninja -DCMAKE_CXX_COMPILER=g++-16 -DCMAKE_CXX_SCAN_FOR_MODULES=OFF
cmake --build build/gcc
./build/gcc/area
""", keep=r"^(?!-- (?:Detecting|Check|Configuring|Generating|The CXX))") Output
$ cd exercise $ cmake --preset debug -- Build files have been written to: build/debug [8 lines not shown] $ cmake --build --preset debug [1/4] Building CXX object CMakeFiles/shapes.dir/src/shapes.cpp.o [2/4] Linking CXX static library libshapes.a [3/4] Building CXX object CMakeFiles/area.dir/app/main.cpp.o [4/4] Linking CXX executable area $ ./build/debug/area circle of radius 2: 12.57 3 by 4.5 rectangle: 13.50 $ cmake --preset release -- Build files have been written to: build/release [8 lines not shown] $ cmake --build --preset release [1/4] Building CXX object CMakeFiles/shapes.dir/src/shapes.cpp.o [2/4] Linking CXX static library libshapes.a [3/4] Building CXX object CMakeFiles/area.dir/app/main.cpp.o [4/4] Linking CXX executable area $ ./build/release/area circle of radius 2: 12.57 3 by 4.5 rectangle: 13.50 $ cmake -S . -B build/gcc -G Ninja -DCMAKE_CXX_COMPILER=g++-16 -DCMAKE_CXX_SCAN_FOR_MODULES=OFF -- Build files have been written to: build/gcc [12 lines not shown] $ cmake --build build/gcc [1/4] Building CXX object CMakeFiles/shapes.dir/src/shapes.cpp.o [2/4] Linking CXX static library libshapes.a [3/4] Building CXX object CMakeFiles/area.dir/app/main.cpp.o [4/4] Linking CXX executable area $ ./build/gcc/area circle of radius 2: 12.57 3 by 4.5 rectangle: 13.50
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 exercise_session.py
The include folder and C++23 are PUBLIC because the program needs them too; the warnings are PRIVATE to shapes.
The GCC build turned off the module scan, so it built in 4 steps like the others.
Check yourself
Now test yourself on the six ideas of this lesson. Each choice says why it is right or what it misses as soon as you pick it, and the ideas you miss come back before the ones you know:
Practice that adapts to you
Practise this lesson
15 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
What is the difference between PRIVATE, PUBLIC and INTERFACE in target_link_libraries? (warm-up)
They say who a dependency and its usage requirements are for. With PRIVATE, the target links the library and uses
its settings, but targets that link this target receive nothing from it. With PUBLIC, the target uses the library
and passes it on, so its users link it and get its include folders, definitions and compile features too. With
INTERFACE, the target does not use the library itself and only passes it on, which suits header-only libraries and
settings-only targets. A practical rule: if a library’s public headers include another library’s headers, link that
library PUBLIC; if only its .cpp files do, link it PRIVATE. A missing PUBLIC shows up as a “file not found”
in the users’ code, while an unneeded PUBLIC silently spreads flags and slows builds.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A CMake project builds on your laptop, but a teammate's fresh clone fails: the app cannot find mathx/stats.hpp, a header of the project's own mathx library. That library's CMakeLists.txt calls include_directories(include), and you once ran make install for mathx. Explain both results and fix the CMake code.
-
Mid to senior
You added -Wall -Werror to CMAKE_CXX_FLAGS at the top of your CMakeLists.txt. Now a third-party library pulled in with FetchContent fails to build with GCC 16, on warnings in its own code that you cannot fix. Why, and how do you keep strict warnings on your code only?
Key takeaways
- CMake generates build files and a build tool runs them: configure with
cmake -S . -B build -G Ninja, build withcmake --build build, and keep every build in a folder of its own. - A project is a set of targets:
PUBLICandINTERFACEsettings pass to the targets that link one,PRIVATEsettings stay with it, and a missingPUBLICinclude folder breaks only the users’ build. - The compiler is chosen when a folder is configured, and GCC 16 builds of C++20 and later targets add module scan
steps, which
CMAKE_CXX_SCAN_FOR_MODULESturns off in a project without modules. - Set the standard, include folders and warnings with
target_*commands, and leave whole-build settings such as a CI-only-Werrorto the command line or a preset. CMakePresets.jsonnames the shared builds andCMakeUserPresets.jsonthe local ones, which stay out of version control.- CMake 4 refuses
cmake_minimum_requiredbelow 3.5: give your own projects a range such as3.10...4.4, and useCMAKE_POLICY_VERSION_MINIMUMonly as a stopgap for other people’s.
Terms in this lesson
- build system generator
- A tool that writes the files a build tool runs, instead of compiling anything itself. CMake is one: from CMakeLists.txt it writes build.ninja for Ninja, Makefiles for Make or a solution for Visual Studio.
- compilation database
- The file compile_commands.json: the exact compiler command of every source file of a build. CMake writes it when CMAKE_EXPORT_COMPILE_COMMANDS is ON, and tools such as clang-tidy and editors read it.
- INTERFACE library
- A CMake target that builds nothing of its own and only carries settings, such as warning flags or the include folder of a header-only library, to the targets that link it.
- out-of-source build
- A build whose generated files, object files and programs all go into a folder of their own, such as build, and never next to the sources. Deleting that folder starts the build again from nothing.
- policy (CMake)
- A behaviour that changed between CMake releases, named CMP and a number, such as CMP0155. cmake_minimum_required decides which releases' behaviour a project gets, so a newer CMake keeps building an older file the same way.
- preset (CMake)
- A named set of options in CMakePresets.json or CMakeUserPresets.json, such as the generator, the build folder and cache variables, so that one command such as cmake --preset release runs that whole configure step.
- target (CMake)
- A library or program that a CMakeLists.txt declares with add_library or add_executable, together with its properties: its sources, include folders, C++ standard, flags and the targets it links.
- usage requirements
- The settings a CMake target passes to every target that links it: its PUBLIC and INTERFACE include folders, definitions, compile options and features. Its PRIVATE settings stay with the target itself.
References
- CMake Tutorial (Kitware)
- cmake(1): the cmake command line (Kitware)
- cmake-buildsystem(7): targets, properties and usage requirements (Kitware)
- target_link_libraries (Kitware)
- target_compile_features (Kitware)
- CXX_EXTENSIONS (target property) (Kitware)
- CMP0155: C++ sources are scanned for imports (Kitware)
- cmake-cxxmodules(7): compilers and generators that scan for modules (Kitware)
- cmake-presets(7) (Kitware)
- cmake_minimum_required (Kitware)
- CMake 4.0 Release Notes (Kitware)
- CMAKE_POLICY_VERSION_MINIMUM (Kitware)
- CMake releases (Kitware)
- The Ninja build system (manual) (Ninja project)
- Configure and build with CMake Presets (Microsoft)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress