Build Systems: CMake & Package Management
Master CMake fundamentals, modern target-based CMake, FetchContent for dependencies, vcpkg and Conan package managers, and compiler flags for safe, optimised builds.
Why Build Systems Matter
As C++ projects grow beyond a single file, you need a build system to manage compilation, linking, dependencies, and platform differences. Manually running g++ *.cpp doesn't scale: you need incremental builds (recompile only changed files), library detection, cross-platform support, and reproducible builds. CMake has become the de facto standard build system generator for C++. It doesn't compile code itself — it generates native build files (Makefiles, Ninja files, Visual Studio projects) for your platform. In short, it is the organised friend who books the table but does not do the cooking.
CMake Basics
A CMakeLists.txt file describes your project structure. Modern CMake (3.12+) uses a target-based approach where everything is attached to targets (executables or libraries):
# CMakeLists.txt — minimum viable project
cmake_minimum_required(VERSION 3.20)
project(MyApp VERSION 1.0.0 LANGUAGES CXX)
# Set C++ standard for the entire project
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF) # use -std=c++20, not -std=gnu++20
# Create an executable target
add_executable(myapp
src/main.cpp
src/engine.cpp
src/utils.cpp
)
# Add include directories for this target
target_include_directories(myapp PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
)
# Create a library target
add_library(mathlib STATIC
src/math/vector.cpp
src/math/matrix.cpp
)
target_include_directories(mathlib PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include/math
)
# Link the library to the executable
target_link_libraries(myapp PRIVATE mathlib)
# Compiler warnings — essential for quality code
target_compile_options(myapp PRIVATE
$<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra -Wpedantic -Werror>
$<$<CXX_COMPILER_ID:MSVC>:/W4 /WX>
)Building with CMake
CMake uses a two-step process: configure (generate build files) and build (compile). Always use out-of-source builds to keep the source tree clean and tidy:
# Configure — generate build files in a 'build' directory
cmake -B build -DCMAKE_BUILD_TYPE=Release
# Build — compile the project
cmake --build build --parallel $(nproc)
# Install (optional)
cmake --install build --prefix /usr/local
# Common build types:
# Debug — -O0 -g (full debug symbols, no optimization)
# Release — -O3 -DNDEBUG (max optimization, no asserts)
# RelWithDebInfo — -O2 -g -DNDEBUG (optimized with debug info)
# MinSizeRel — -Os -DNDEBUG (optimize for size)
# Run with sanitizers (add to CMakeLists.txt or command line)
cmake -B build-asan -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer"FetchContent: Dependency Management
CMake's FetchContent module downloads and builds dependencies at configure time, making it easy to pull in libraries without manual setup:
cmake_minimum_required(VERSION 3.20)
project(MyApp LANGUAGES CXX)
include(FetchContent)
# Fetch GoogleTest
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.14.0
)
# Fetch fmt (formatting library)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 10.2.1
)
# Download and make available
FetchContent_MakeAvailable(googletest fmt)
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE fmt::fmt)
# Test executable
enable_testing()
add_executable(tests tests/test_main.cpp)
target_link_libraries(tests PRIVATE GTest::gtest_main)
add_test(NAME unit_tests COMMAND tests)vcpkg and Conan
For larger projects with many dependencies, dedicated C++ package managers provide better dependency resolution and caching:
vcpkg (Microsoft) — integrates directly with CMake via a toolchain file. Install packages globally or in "manifest mode" with a vcpkg.json file:
{
"dependencies": ["fmt", "spdlog", "nlohmann-json", "catch2"]
}Use with: cmake -B build -DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake
Conan — uses a conanfile.txt or conanfile.py to declare dependencies. Supports multiple build configurations and binary caching:
[requires]
fmt/10.2.1
spdlog/1.13.0
[generators]
CMakeDeps
CMakeToolchainBoth package managers maintain repositories of thousands of C++ libraries with tested, pre-built binaries.
Follow these rules for maintainable CMake:
1. Always use target-based commands: target_include_directories, target_compile_options, target_link_libraries — never include_directories or add_definitions (which pollute the global scope).
2. Understand PRIVATE/PUBLIC/INTERFACE: PRIVATE = only for this target, PUBLIC = for this target and its consumers, INTERFACE = only for consumers.
3. Set the C++ standard per-target or project-wide, never via raw flags like -std=c++20.
4. Enable maximum warnings: -Wall -Wextra -Wpedantic (GCC/Clang) or /W4 (MSVC). Consider -Werror / /WX in CI, however much it stings.
5. Use cmake --preset for reproducible builds (CMakePresets.json).
6. Never hardcode paths — use find_package, FetchContent, or package managers. Your colleague's home directory is not a standard location.
Using file(GLOB ...) to collect sources: file(GLOB SOURCES src/*.cpp) does NOT re-run when files are added or removed. New files won't be compiled until you manually re-configure, which is rather a nasty surprise at four o'clock on a Friday. Always list source files explicitly.
Forgetting CMAKE_CXX_EXTENSIONS OFF: Without this, GCC uses -std=gnu++20 instead of -std=c++20, enabling non-standard extensions that break portability.
Mixing add_subdirectory and FetchContent: Both bring in external projects, but they interact differently with the build cache. Choose one approach per dependency.
Not setting CMAKE_EXPORT_COMPILE_COMMANDS ON: This generates compile_commands.json, which is needed by clang-tidy, clangd (LSP), and many IDE integrations. Always enable it.
- CMake is the standard C++ build system generator — learn its target-based API
- Use
target_*commands (not global scope) with PRIVATE/PUBLIC/INTERFACE visibility - FetchContent downloads and builds dependencies at configure time — ideal for small dependency sets
- vcpkg (manifest mode) and Conan handle larger dependency graphs with binary caching
- Always enable warnings (
-Wall -Wextra -Wpedantic) and use out-of-source builds - Generate
compile_commands.jsonfor IDE and static analysis tooling
Quiz — Test Your Knowledge
(15 XP)1. What does `target_link_libraries(myapp PRIVATE mathlib)` do?
2. Why is `file(GLOB SOURCES src/*.cpp)` considered a bad practice in CMake?
3. What is the purpose of `CMAKE_CXX_EXTENSIONS OFF`?