10. Build Systems: CMake¶
What & why¶
Typing g++ commands stops scaling at about three files. You need to rebuild only what changed, manage include paths and libraries, pull in dependencies, and build the same project with GCC, Clang, and MSVC on three operating systems. CMake is the de facto standard for this. It doesn't build anything itself — it generates build files for Make, Ninja, Visual Studio, or Xcode.
Install¶
- macOS:
brew install cmake ninja - Linux:
sudo apt install cmake ninja-build - Windows: bundled with Visual Studio, or
winget install Kitware.CMake Ninja-build.Ninja
A minimal project¶
cmake_minimum_required(VERSION 3.24)
project(hello LANGUAGES CXX)
add_executable(hello main.cpp)
target_compile_features(hello PRIVATE cxx_std_20)
Build it — out of source, in a separate build/ directory:
cmake -S . -B build # configure: generate build files into build/
cmake --build build # compile and link
./build/hello
Re-run only cmake --build build after editing sources; CMake re-configures itself when CMakeLists.txt changes.
Useful configure options:
cmake -S . -B build -G Ninja # use Ninja
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug # Debug | Release | RelWithDebInfo
cmake -S . -B build -DCMAKE_CXX_COMPILER=clang++
cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON # for clangd / IDEs
Targets: the core idea¶
Modern CMake is about targets (executables and libraries) and their properties (sources, include dirs, compile options, dependencies). Every property is set on a target with a visibility keyword:
| Keyword | Applies to this target | Propagates to targets that link it |
|---|---|---|
PRIVATE |
yes | no |
PUBLIC |
yes | yes |
INTERFACE |
no | yes |
A realistic layout¶
stats/
├── CMakeLists.txt
├── src/
│ ├── CMakeLists.txt
│ ├── stats/
│ │ ├── stats.hpp
│ │ └── stats.cpp
│ └── main.cpp
└── tests/
├── CMakeLists.txt
└── test_stats.cpp
Top-level CMakeLists.txt:
cmake_minimum_required(VERSION 3.24)
project(stats VERSION 0.1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF) # -std=c++20, not -std=gnu++20
add_subdirectory(src)
include(CTest) # defines BUILD_TESTING (ON by default) and enable_testing()
if(BUILD_TESTING)
add_subdirectory(tests)
endif()
src/CMakeLists.txt:
# A library with the reusable logic
add_library(stats_lib stats/stats.cpp)
target_include_directories(stats_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_options(stats_lib PRIVATE
$<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-Wall -Wextra -Wpedantic>
$<$<CXX_COMPILER_ID:MSVC>:/W4>
)
# A thin executable on top
add_executable(stats main.cpp)
target_link_libraries(stats PRIVATE stats_lib)
Because stats_lib's include directory is PUBLIC, anything that links stats_lib — the executable, the tests — automatically gets it. #include "stats/stats.hpp" just works.
The $<...> expressions are generator expressions, evaluated at build time — here they pick warning flags per compiler.
Put your logic in a library and keep main.cpp thin. Tests can then link the library directly.
Dependencies¶
find_package: libraries already installed¶
find_package(Threads REQUIRED)
target_link_libraries(stats PRIVATE Threads::Threads)
find_package(fmt CONFIG REQUIRED) # if installed via a package manager
target_link_libraries(stats PRIVATE fmt::fmt)
FetchContent: download at configure time¶
include(FetchContent)
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.7.1
)
FetchContent_MakeAvailable(Catch2)
target_link_libraries(tests PRIVATE Catch2::Catch2WithMain)
Always pin a tag or commit, never a branch.
Package managers¶
For many dependencies, use vcpkg or Conan. With vcpkg in manifest mode you list dependencies in vcpkg.json and pass -DCMAKE_TOOLCHAIN_FILE=<vcpkg>/scripts/buildsystems/vcpkg.cmake; then find_package finds them.
Presets¶
Remembering long configure commands is error-prone. CMakePresets.json checks them into the repo:
{
"version": 6,
"configurePresets": [
{
"name": "debug",
"generator": "Ninja",
"binaryDir": "build/debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
}
},
{
"name": "asan",
"inherits": "debug",
"binaryDir": "build/asan",
"cacheVariables": {
"CMAKE_CXX_FLAGS": "-fsanitize=address,undefined -fno-omit-frame-pointer"
}
},
{
"name": "release",
"generator": "Ninja",
"binaryDir": "build/release",
"cacheVariables": { "CMAKE_BUILD_TYPE": "Release" }
}
],
"buildPresets": [
{ "name": "debug", "configurePreset": "debug" },
{ "name": "asan", "configurePreset": "asan" },
{ "name": "release", "configurePreset": "release" }
],
"testPresets": [
{ "name": "debug", "configurePreset": "debug", "output": { "outputOnFailure": true } }
]
}
Install rules (briefly)¶
The advanced track covers exporting libraries for other CMake projects.
Old-style CMake to avoid¶
You'll see these in tutorials and old projects. Don't write them:
include_directories(...) # global — use target_include_directories
add_definitions(...) # global — use target_compile_definitions
link_libraries(...) # global — use target_link_libraries
set(CMAKE_CXX_FLAGS "... -Wall") # global string munging — use target_compile_options
file(GLOB SOURCES src/*.cpp) # new files aren't picked up until re-configure; list sources explicitly
Common mistakes¶
- Building in the source directory (
cmake .) — litters the tree. Always-B build. - Forgetting to re-run the build after changes, or deleting
build/to "fix" things instead of reading the error. - Directory-wide commands instead of target-based ones.
- Wrong visibility — a
PRIVATEinclude dir that users of the library also need should bePUBLIC. - Unpinned
FetchContentdependencies. - Setting
CMAKE_BUILD_TYPEwith multi-config generators (Visual Studio, Xcode) — usecmake --build build --config Releaseinstead.
Exercises¶
- Convert your beginner capstone (
todo) to CMake with atodo_liblibrary and atodoexecutable. - Add warning flags via a generator expression, and add a
-DTODO_WERROR=ONoption (option(...)) that adds-Werror. - Create
CMakePresets.jsonwithdebug,asan, andreleasepresets. Build all three. - Fetch
fmtwithFetchContentand usefmt::printin your program. - Enable
CMAKE_EXPORT_COMPILE_COMMANDSand point your editor's clangd atbuild/compile_commands.json. Notice how code navigation improves.
Recap¶
- CMake generates build files; configure with
cmake -S . -B build, build withcmake --build build. - Think in targets:
add_library,add_executable,target_link_libraries,target_include_directories,target_compile_options. PRIVATE/PUBLIC/INTERFACEcontrol what propagates to dependents.- Keep logic in a library and
mainthin so tests can link it. - Dependencies via
find_package,FetchContent(pinned), or vcpkg/Conan. CMakePresets.jsoncaptures your build configurations.
Next: Testing with Catch2 →