Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 20 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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:

hello/CMakeLists.txt Text · hello/CMakeLists.txt
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)
hello/main.cpp C++ · hello/main.cpp
#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:

Configure, build, run Python · hello_session.py
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:

Interactive · stage by stage The hello project through configure and build Step through the session. Each step is one stage of the two commands, with what it left in the build folder.
The hello project through configure and build, step 6 of 6. Showing CMakeLists.txt, Configure, The build folder (build.ninja, CMakeCache.txt), Build, Ninja runs the compiler (2 steps: compile, link), Run the program (C++ 202302). In focus: Run the program.CMakeLists.txtone program and its sourcesConfigurecmake -S . -B buildThe build folderbuild.ninja, CMakeCache.txtBuildcmake --build buildNinja runs the compiler2 steps: compile, linkRun the programC++ 202302hello

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The project. CMakeLists.txt names the CMake versions it was written for, the project and its language, and one program, hello, built from main.cpp with at least C++23.
  2. Configure. cmake -S . -B build -G Ninja reads the project in . and works in build; 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.
  3. The build files. Configuring wrote build.ninja, the script Ninja runs, and CMakeCache.txt, the answers it found, such as the compiler, plus compile_commands.json, which that option asked for. Nothing was written next to the sources.
  4. Build. cmake --build build runs the build tool the folder was configured for, Ninja here. The same command builds a folder configured for Make or Visual Studio.
  5. Compile and link. Ninja ran two steps: [1/2] compiled main.cpp with -std=gnu++23, the GNU dialect of C++23, and [2/2] linked the program hello.
  6. Run. The program printed Hello from a CMake build, C++ 202302, and 202302 is the value of __cplusplus in C++23. To build again from nothing, delete the folder build and 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

  1. Question 1 of 2 After cmake -S . -B build -G Ninja and before any build, what is in the folder build?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: build.ninja and CMakeCache.txt, but no program yet

    cmake -S . -B build reads the project and writes the build files into build: build.ninja for Ninja and CMakeCache.txt with the answers it found, such as the compiler. cmake --build build then runs Ninja, which compiles and links.

  2. Question 2 of 2 You switched compilers and want the hello project built from nothing, with no cached answers. What do you do?

    Choose one answer.

    A hint

    Where does CMake keep the answers of the configure step, such as the compiler?

    Show the answer: Question 2 of 2

    Answer: Delete the folder build and configure again

    The build folder holds everything CMake generated, including CMakeCache.txt with the compiler it chose. Deleting the folder and configuring again is a clean start, and it never touches the sources.

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:

wordfreq/CMakeLists.txt Text · wordfreq/CMakeLists.txt
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)
wordfreq/textstats/CMakeLists.txt Text · wordfreq/textstats/CMakeLists.txt
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)
wordfreq/app/CMakeLists.txt Text · wordfreq/app/CMakeLists.txt
add_executable(wordfreq main.cpp)
target_link_libraries(wordfreq PRIVATE textstats wordfreq_warnings)
The C++ sources

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
Three CMake targets: a warnings target and a static library, both linked by the wordfreq program, with what each link passes on.wordfreq_warningsINTERFACE library: builds nothingcarries -Wall -Wextra -Wpedantictextstats: static libraryPUBLIC: include/ and C++23built with the warningswordfreq: executablegets include/ and C++23 from textstatsbuilt with the warningsPRIVATE linkPRIVATE linkPRIVATE link

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.

  1. wordfreq_warnings is an INTERFACE library. It builds nothing; it only carries the warning flags -Wall, -Wextra and -Wpedantic to whatever links it.
  2. 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.
  3. 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:

Two targets, built with Apple clang Python · targets_session.py
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:

Interactive · step by step How each flag reached each compile command Step through the links of the wordfreq project. The right column shows the flags each compile command has so far.
How each flag reached each compile command, step 6 of 6. Showing wordfreq_warnings, textstats (PRIVATE include folder), wordfreq, textstats.cpp command (-I… -std=c++23 -Wall…), main.cpp command (-std=c++23 -Wall…, no -I). In focus: textstats, main.cpp command.PRIVATE linkPRIVATE linkPRIVATE linkwordfreq_warningsINTERFACE: builds nothingtextstatsPRIVATE include folderwordfreqthe programtextstats.cpp command-I… -std=c++23 -Wall…main.cpp command-std=c++23 -Wall…, no -Iheader not found

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Three targets. wordfreq_warnings is an INTERFACE library: it builds nothing and carries -Wall -Wextra -Wpedantic to whatever links it. textstats is the library that counts words, wordfreq the program.
  2. PUBLIC settings. textstats declares its include folder and C++23 as PUBLIC. Its own source file gets both: -Itextstats/include -std=c++23.
  3. Warnings for the library. textstats links wordfreq_warnings PRIVATE, so its own command adds the three warning flags. PRIVATE means they stop there.
  4. A link passes PUBLIC on. wordfreq links textstats. The PUBLIC settings travel along the link, so main.cpp gets -Itextstats/include -std=c++23 without the app's CMakeLists.txt naming either.
  5. The program's own warnings. The warnings of textstats did not travel: its link to them is PRIVATE. main.cpp has them because wordfreq links wordfreq_warnings itself.
  6. One keyword changed. With the folder made PRIVATE to textstats, as the next session does, the library still compiles, but the main.cpp command has no -I at all and stops at textstats/textstats.hpp file 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:

The include folder made PRIVATE Python · private_session.py
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

  1. Question 1 of 2 A library's own headers live in its include folder, and programs that link the library include them. Which keyword should target_include_directories use?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: PUBLIC

    The library's sources and its users both need the folder, so it is PUBLIC. With PRIVATE, the lesson's program failed with 'textstats/textstats.hpp' file not found, because its compile command had no -I at all. INTERFACE would give the folder to users only, which suits a header-only library.

  2. Question 2 of 2 Why was app/main.cpp compiled with -Wall -Wextra -Wpedantic?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: wordfreq links wordfreq_warnings itself

    Only PUBLIC and INTERFACE settings travel along a link. The warnings reach both targets because each one links wordfreq_warnings itself, PRIVATE.

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:

The same project with GCC 16 Python · gcc_session.py
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

  1. One question The GCC 16 build of wordfreq took 8 steps, 4 of them module scans and dependency files. How many steps does it take in a folder configured with -DCMAKE_CXX_SCAN_FOR_MODULES=OFF?

    Type a number.

    A hint

    Count what is left once nothing is scanned.

    Show the answer: One question

    Answer: 4 steps

    Without the scan, the GCC build has the same 4 steps as the Apple clang build: compile textstats.cpp, archive libtextstats.a, compile main.cpp and link wordfreq. The exercise's GCC build, with the scan off, took 4 steps too.

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

  1. One question Only CI should build with -Werror. Where does that flag belong?

    Choose one answer.

    Show the answer: One question

    Answer: On the configure command line of the CI job, or in a preset for CI

    Some settings describe a whole build: its type, its compiler, sanitizers and a CI-only -Werror. They go on the command line or in a preset; CMakeLists.txt keeps what the code itself needs.

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:

wordfreq/CMakePresets.json (committed) JSON · wordfreq/CMakePresets.json
{
  "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" }
  ]
}
wordfreq/CMakeUserPresets.json (this computer only) JSON · wordfreq/CMakeUserPresets.json
{
  "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": 6 is the file format, which CMake 3.25 and later read; CMake 4.4 reads up to version 12.
  • base is hidden, so it cannot be chosen by itself; the other configure presets inherits it and add a build type. ${sourceDir} and ${presetName} are macros, so each preset gets its own folder such as build/release.
  • buildPresets name the build step of a configure preset, so cmake --build --preset release needs no folder name.
  • CMakeUserPresets.json holds 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.
Listing, configuring and building presets Python · presets_session.py
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:

Interactive · step by step Five configure presets, and what each one inherits Step through the presets of the wordfreq project. Each step adds one preset and says what its build used.
Five configure presets, and what each one inherits, step 6 of 6. Showing base, debug, release (-O3 -DNDEBUG), asan-ubsan (debug + sanitizers), gcc-debug (user file: g++-16). In focus: debug, release, asan-ubsan, gcc-debug.inheritsinheritsinheritsinheritsbasehiddendebugbuild/debugrelease-O3 -DNDEBUGasan-ubsandebug + sanitizersgcc-debuguser file: g++-16

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The hidden base. base holds what every build shares: the Ninja generator, the folder build/${presetName} and CMAKE_EXPORT_COMPILE_COMMANDS. It is hidden, so it can only be inherited, and cmake --list-presets does not show it.
  2. debug. debug inherits base and sets CMAKE_BUILD_TYPE to Debug. The macro ${presetName} gives it a folder of its own, build/debug.
  3. release. release inherits base with the Release type. Its compile commands carried -O3 -DNDEBUG, CMake's own release flags for GCC and Clang.
  4. asan-ubsan. asan-ubsan inherits debug and adds the sanitizer flags. Its commands carried -fsanitize=address,undefined -fno-omit-frame-pointer -g, and the program loads AddressSanitizer's run-time library.
  5. gcc-debug. gcc-debug is in CMakeUserPresets.json, on this computer only. It inherits debug from the shared file and sets CMAKE_CXX_COMPILER to g++-16; its build took 8 steps, with the module scans.
  6. Build presets. Each entry of buildPresets names a configure preset, so cmake --build --preset release builds build/release with 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.

JSON Validator Paste a CMakePresets.json to find a missing comma or bracket before CMake reports a parse error.

Checkpoint

Check: Presets for every build

  1. Question 1 of 2 cmake --preset debug works, but cmake --preset base stops with an error. Why?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: base is hidden: it exists only to be inherited

    base holds what every build shares and is marked hidden, so presets inherit it but nobody chooses it. The other presets are listed by cmake --list-presets and can be chosen with cmake --preset.

  2. Question 2 of 2 Which preset file goes into version control?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: CMakePresets.json; CMakeUserPresets.json stays on your computer

    CMakePresets.json is committed, so the team and CI run the same builds. CMakeUserPresets.json adds builds for one computer and may inherit the shared ones.

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:

legacy/CMakeLists.txt Text · legacy/CMakeLists.txt
cmake_minimum_required(VERSION 2.8.12)
project(legacy CXX)

add_executable(legacy main.cpp)
A CMake 2.8 project and CMake 4.4.4 Python · legacy_session.py
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:

Timeline · 9 events in order The CMake releases behind this lesson's files Walk through the releases in order. Each one added something this lesson's files use, or changed what an old file may ask for.
  1. 3.5

    The oldest minimum CMake 4 accepts

    CMake 4.0 removed compatibility with every release before 3.5, so cmake_minimum_required has to ask for 3.5 or later. The legacy project asks for 2.8.12 and stops at its first line.

  2. 3.12

    A range in cmake_minimum_required new syntax

    A minimum may be followed by ... and a second version, as in 3.28...4.4. The file then needs only the first; a newer CMake applies the behaviour of every release up to the second.

  3. 3.19

    Presets arrive

    CMakePresets.json and CMakeUserPresets.json appeared with version 1 of the presets format, which held configure presets only.

  4. 3.20

    Build and test presets

    Version 2 of the presets format added buildPresets and testPresets, so cmake --build --preset release needs no folder name.

  5. 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.

  6. 3.28

    Sources scanned for modules

    Policy CMP0155: sources of targets that use C++20 or later are scanned for import when 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.

  7. 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.

  8. 4.0

    Old minimums become errors breaking change

    Compatibility with releases before 3.5 was removed, and CMAKE_POLICY_VERSION_MINIMUM was added so that packagers and users can still try to configure projects that were not updated.

  9. 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

  1. One question A library you do not maintain starts with cmake_minimum_required(VERSION 2.8.12), and CMake 4.4.4 refuses to configure it. What lets you configure it today?

    Choose one answer.

    Show the answer: One question

    Answer: Configure with -DCMAKE_POLICY_VERSION_MINIMUM=3.5

    CMAKE_POLICY_VERSION_MINIMUM lets packagers and users configure projects that ask for an older minimum. It is a stopgap: the run already warned that compatibility below 3.10 will go as well.

Practise: write the CMakeLists.txt

A small geometry project has three source files and a preset file, but no CMakeLists.txt:

The provided files

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 debug and ./build/debug/area
  • the same with the release preset
  • 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

exercise/CMakeLists.txt Text · exercise/CMakeLists.txt
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)
Both presets and a GCC build Python · exercise_session.py
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.

  1. Question 1 of 15 A project has been configured into the folder build. Which command compiles it, whatever the generator?

    Choose one answer.

    Show the answer: Question 1 of 15

    Answer: cmake --build build

    cmake --build runs the build tool the folder was configured for, Ninja here, Make or Visual Studio elsewhere. cmake -S . -B build configures; it writes build files but compiles nothing.

  2. Question 2 of 15 target_compile_features(app PRIVATE cxx_std_23) produced -std=gnu++23. What makes CMake use -std=c++23 instead?

    Choose one answer.

    Show the answer: Question 2 of 15

    Answer: Setting CMAKE_CXX_EXTENSIONS to OFF (or the target's CXX_EXTENSIONS property)

    Compiler extensions are on by default, so CMake asks for the GNU dialect. With CMAKE_CXX_EXTENSIONS set to OFF in the top-level file, the lesson's compile commands said -std=c++23.

  3. Question 3 of 15 What does the INTERFACE library wordfreq_warnings build?

    Choose one answer.

    Show the answer: Question 3 of 15

    Answer: Nothing: it only carries settings to the targets that link it

    An INTERFACE library builds nothing and only carries settings. Each target that links wordfreq_warnings gets its three warning flags in its compile commands.

  4. Question 4 of 15 With the include folder made PRIVATE, which step of the build failed?

    Choose one answer.

    Show the answer: Question 4 of 15

    Answer: Compiling app/main.cpp, step 3 of 4

    A PRIVATE include folder is used by the library's own sources and by nothing that links the library. The library compiled and was archived; the program's compile command had no -I and stopped at the #include.

  5. Question 5 of 15 A header-only library fastmath has headers in its include folder and no .cpp files. After add_library(fastmath INTERFACE), which keyword goes in target_include_directories(fastmath ... include)?

    Choose one answer.

    Show the answer: Question 5 of 15

    Answer: INTERFACE

    The table of keywords gives INTERFACE to header-only libraries and settings-only targets: they build nothing, so a setting is only ever for the targets that link them.

  6. Question 6 of 15 Why did the GCC 16 build of the two-target project take 8 steps when the Apple clang build took 4?

    Choose one answer.

    Show the answer: Question 6 of 15

    Answer: CMake scanned the C++23 sources for module imports, which it does with GCC but not with Apple clang

    Since CMake 3.28, C++20 and later sources are scanned for import statements when the compiler supports it. The extra steps were the scans and the dependency files; with CMAKE_CXX_SCAN_FOR_MODULES set to OFF, the exercise's GCC build took 4 steps again.

  7. Question 7 of 15 When is the compiler of a CMake build chosen?

    Choose one answer.

    Show the answer: Question 7 of 15

    Answer: When its build folder is configured

    The compiler is found, or given with CMAKE_CXX_COMPILER, when a folder is configured, and kept in that folder. The lesson built the same project with Apple clang in build and with GCC 16 in build-gcc.

  8. Question 8 of 15 Which command replaces include_directories(include) for the textstats library?

    Choose one answer.

    Show the answer: Question 8 of 15

    Answer: target_include_directories(textstats PUBLIC include)

    Each folder-wide command has a target_* counterpart: target_include_directories, target_compile_options, target_compile_definitions, target_compile_features and target_link_libraries, each with a keyword that says who the setting is for.

  9. Question 9 of 15 The top-level CMakeLists.txt sets CMAKE_CXX_FLAGS to -Wall -Werror, then adds a third-party library with add_subdirectory. Which code is built with those flags?

    Choose one answer.

    Show the answer: Question 9 of 15

    Answer: Every target in that folder and below, the third-party library included

    Global settings land on every target of the folder and the folders below it, including code added with add_subdirectory whose warnings you cannot fix. Settings on targets stay with the code that needs them.

  10. Question 10 of 15 Which statements about preset files are true? Choose every true one.

    Choose every answer that is right.

    Show the answer: Question 10 of 15

    Answer:

    • CMakeUserPresets.json holds settings for one computer and stays out of version control
    • CMakePresets.json is meant to be committed and shared
    • A build preset names the configure preset whose folder it builds

    The shared file names the builds everyone uses; the user file adds local ones, such as the lesson's GCC 16 preset. Hidden presets exist only to be inherited, so cmake --preset refuses them.

  11. Question 11 of 15 Which flags did the release preset add to every compile command?

    Choose one answer.

    Show the answer: Question 11 of 15

    Answer: -O3 -DNDEBUG

    The release preset sets CMAKE_BUILD_TYPE to Release, and CMake then adds its release flags for GCC and Clang, -O3 -DNDEBUG, as the recorded compile command shows.

  12. Question 12 of 15 Everyone on the team has GCC 16 and should be able to run a release build with it. What do you add?

    Choose one answer.

    Show the answer: Question 12 of 15

    Answer: In CMakePresets.json, a configure preset that inherits release and sets CMAKE_CXX_COMPILER to g++-16, and a build preset for it

    A preset that everyone may run belongs in the shared file. It inherits release, sets the compiler as a cache variable, gets a folder of its own from ${presetName}, and a build preset makes cmake --build --preset work.

  13. Question 13 of 15 CMake 4.4.4 stops at cmake_minimum_required(VERSION 2.8.12) in your own project. What is the lasting fix?

    Choose one answer.

    Show the answer: Question 13 of 15

    Answer: Raise the minimum and give a range, such as cmake_minimum_required(VERSION 3.10...4.4), after checking the build

    CMAKE_POLICY_VERSION_MINIMUM is meant for projects you do not maintain, and the lesson's run showed it already brings a warning that versions older than 3.10 will lose compatibility next. Your own project should state a current minimum, with a range so newer CMake applies its newer behaviour.

  14. Question 14 of 15 What does cmake_minimum_required(VERSION 3.28...4.4) say?

    Choose one answer.

    Show the answer: Question 14 of 15

    Answer: The file needs CMake 3.28 or later, and a newer CMake applies the behaviour of releases up to 4.4

    A range in cmake_minimum_required requires only its first version. When a newer CMake runs, it applies the behaviour of every release up to the second version, here 4.4.

  15. Question 15 of 15 With -DCMAKE_POLICY_VERSION_MINIMUM=3.5, the 2.8.12 project configured and built, with a warning. What should its maintainers take from that warning?

    Choose one answer.

    Show the answer: Question 15 of 15

    Answer: Raise the minimum to 3.10 or later: compatibility below 3.10 is the next to go

    CMake 4.0 removed compatibility below 3.5, and since 3.31 a minimum below 3.10 brings a deprecation warning. A project that raises its minimum to 3.10 or later, with a range, is ready for both.

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.

  1. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • CMake generates build files and a build tool runs them: configure with cmake -S . -B build -G Ninja, build with cmake --build build, and keep every build in a folder of its own.
  • A project is a set of targets: PUBLIC and INTERFACE settings pass to the targets that link one, PRIVATE settings stay with it, and a missing PUBLIC include 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_MODULES turns 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 -Werror to the command line or a preset.
  • CMakePresets.json names the shared builds and CMakeUserPresets.json the local ones, which stay out of version control.
  • CMake 4 refuses cmake_minimum_required below 3.5: give your own projects a range such as 3.10...4.4, and use CMAKE_POLICY_VERSION_MINIMUM only 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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.