Build System
Updated 2025-01-01T00:00:00.000ZCMake
The de facto C++ build system. Project structure, targets, dependencies, install rules, and modern CMake best practices.
TL;DR
CMake generates build files (Makefiles, Ninja, VS solutions) from a platform-neutral description. The modern CMake idiom revolves around targets and properties — not global variables and directory-level flags.
cmake
cmake_minimum_required(VERSION 3.25)
project(myapp VERSION 1.0 LANGUAGES CXX)
add_executable(myapp main.cpp)
target_compile_features(myapp PRIVATE cxx_std_20)Hello World project
cpp
myapp/
├── CMakeLists.txt
├── src/
│ └── main.cpp
└── include/
└── myapp/
└── utils.hcmake
# CMakeLists.txt
cmake_minimum_required(VERSION 3.25)
project(myapp VERSION 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_executable(myapp src/main.cpp)
target_include_directories(myapp PRIVATE include)Build it:
bash
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j$(nproc)
./build/myappTargets — the core concept
Everything in modern CMake is a target. Properties on targets propagate through the dependency graph via PRIVATE, PUBLIC, and INTERFACE keywords.
cmake
add_library(mylib STATIC
src/foo.cpp
src/bar.cpp
)
target_include_directories(mylib
PUBLIC include/ # callers also get this include path
PRIVATE src/ # only mylib itself sees this
)
target_compile_options(mylib
PRIVATE -Wall -Wextra -Wpedantic
)
target_compile_features(mylib PUBLIC cxx_std_20)
# Executable links against the library — inherits its PUBLIC properties
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE mylib)| Keyword | mylib itself | Callers of mylib |
|---|---|---|
PRIVATE | ✅ | ❌ |
PUBLIC | ✅ | ✅ |
INTERFACE | ❌ | ✅ |
Finding & using external packages
cmake
# Find system-installed packages (uses Find*.cmake modules)
find_package(OpenSSL REQUIRED)
target_link_libraries(myapp PRIVATE OpenSSL::SSL OpenSSL::Crypto)
# Find packages installed via vcpkg or Conan
find_package(fmt CONFIG REQUIRED)
target_link_libraries(myapp PRIVATE fmt::fmt)
find_package(nlohmann_json REQUIRED)
target_link_libraries(myapp PRIVATE nlohmann_json::nlohmann_json)FetchContent — download dependencies at configure time
cmake
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.14.0
)
FetchContent_MakeAvailable(googletest)
add_executable(tests test_main.cpp)
target_link_libraries(tests PRIVATE GTest::gtest_main mylib)Build types
bash
# Debug — no optimization, debug symbols
cmake -B build -DCMAKE_BUILD_TYPE=Debug
# Release — optimized, no debug symbols
cmake -B build -DCMAKE_BUILD_TYPE=Release
# RelWithDebInfo — optimized + debug symbols (for profiling)
cmake -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
# Multi-config generators (VS, Xcode, Ninja Multi-Config)
cmake -B build -G "Ninja Multi-Config"
cmake --build build --config ReleaseTesting with CTest
cmake
enable_testing()
add_executable(tests test_suite.cpp)
target_link_libraries(tests PRIVATE GTest::gtest_main mylib)
include(GoogleTest)
gtest_discover_tests(tests)bash
cmake --build build
ctest --test-dir build -j$(nproc) --output-on-failureInstallation rules
cmake
install(TARGETS myapp mylib
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)
install(DIRECTORY include/ DESTINATION include)
# Generate a config file for find_package(myapp)
include(CMakePackageConfigHelpers)
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/myappConfigVersion.cmake"
COMPATIBILITY SameMajorVersion
)
install(FILES
cmake/myappConfig.cmake
"${CMAKE_CURRENT_BINARY_DIR}/myappConfigVersion.cmake"
DESTINATION lib/cmake/myapp
)Compiler flags — platform-aware
cmake
# Add warnings without hardcoding compiler names
target_compile_options(myapp PRIVATE
$<$<CXX_COMPILER_ID:MSVC>:/W4 /WX>
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-Wall -Wextra -Wconversion -Wshadow>
)
# Enable sanitizers in debug builds
target_compile_options(myapp PRIVATE
$<$<AND:$<CONFIG:Debug>,$<NOT:$<CXX_COMPILER_ID:MSVC>>>:-fsanitize=address,undefined>
)
target_link_options(myapp PRIVATE
$<$<AND:$<CONFIG:Debug>,$<NOT:$<CXX_COMPILER_ID:MSVC>>>:-fsanitize=address,undefined>
)Presets (CMakePresets.json)
Replace shell scripts with versioned, shareable presets:
json
{
"version": 6,
"configurePresets": [
{
"name": "default",
"binaryDir": "${sourceDir}/build/${presetName}",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
}
},
{
"name": "release",
"inherits": "default",
"cacheVariables": { "CMAKE_BUILD_TYPE": "Release" }
}
],
"buildPresets": [
{ "name": "default", "configurePreset": "default" },
{ "name": "release", "configurePreset": "release" }
]
}bash
cmake --preset default
cmake --build --preset defaultCommon pitfalls
Don't set CMAKE_CXX_FLAGS globally
cmake
# BAD — affects every target, no PRIVATE/PUBLIC control
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
# GOOD — scoped to the target
target_compile_options(myapp PRIVATE -Wall -Wextra)Don't use include_directories or link_libraries (non-target forms)
cmake
# BAD — directory-level, hard to reason about
include_directories(include)
link_libraries(mylib)
# GOOD — target-scoped
target_include_directories(myapp PRIVATE include)
target_link_libraries(myapp PRIVATE mylib)Always set CXX_EXTENSIONS OFF
Without this, -std=gnu++20 is used instead of -std=c++20, pulling in GCC extensions and breaking portability.
Version requirements
| Feature | Minimum CMake |
|---|---|
cmake_minimum_required + project() | 3.0 |
target_compile_features | 3.1 |
FetchContent | 3.11 |
FetchContent_MakeAvailable | 3.14 |
cmake --build --target | 3.15 |
cmake --install | 3.15 |
| CMakePresets.json | 3.19 |
--preset flag | 3.20 |
Edit on GitHubUpdated 2025-01-01T00:00:00.000Z