CMake
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_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
myapp/
├── CMakeLists.txt
├── src/
│ └── main.cpp
└── include/
└── myapp/
└── utils.h# 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:
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.
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
# 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
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
# 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
enable_testing()
add_executable(tests test_suite.cpp)
target_link_libraries(tests PRIVATE GTest::gtest_main mylib)
include(GoogleTest)
gtest_discover_tests(tests)cmake --build build
ctest --test-dir build -j$(nproc) --output-on-failureInstallation rules
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
# 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:
{
"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" }
]
}cmake --preset default
cmake --build --preset defaultCommon pitfalls
Don't set CMAKE_CXX_FLAGS globally
# 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)
# 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 |
Generator Expressions — Deep Dive
Generator expressions are evaluated at build-system generation time, not at configure time — which makes them the right tool for values that differ per config, per compiler, or between the build tree and the install tree.
The building blocks are logical operators and query expressions:
# Boolean primitives
$<BOOL:string> # 1 if string is non-empty/non-zero/non-false
$<AND:a,b> # 1 if both a and b are 1
$<OR:a,b> # 1 if either a or b is 1
$<NOT:a> # inverts a
# Common queries
$<CXX_COMPILER_ID:Clang> # 1 when the C++ compiler is Clang
$<CONFIG:Release> # 1 in Release builds
$<COMPILE_FEATURES:cxx_std_20> # 1 if cxx_std_20 is available
$<TARGET_FILE:target> # full path to the output file of a targetPattern 1 — compiler-specific warnings without a chain of if() blocks:
target_compile_options(mylib PRIVATE
$<$<CXX_COMPILER_ID:MSVC>:/W4 /wd4251>
$<$<CXX_COMPILER_ID:GNU>:-Wall -Wextra -Wshadow -Wconversion>
$<$<CXX_COMPILER_ID:Clang,AppleClang>:-Wall -Wextra -Wshadow -Weverything -Wno-c++98-compat>
)Pattern 2 — config-specific preprocessor defines:
target_compile_definitions(myapp PRIVATE
$<$<CONFIG:Debug>:MY_APP_DEBUG=1>
$<$<CONFIG:Release>:MY_APP_NDEBUG=1 MY_APP_INLINE=inline>
)Pattern 3 — build-time vs install-time include paths ($<BUILD_INTERFACE:...> and $<INSTALL_INTERFACE:...> are the key pair for installable libraries):
target_include_directories(mylib PUBLIC
# When building: headers are relative to the source tree
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
# When installed: headers land under the install prefix
$<INSTALL_INTERFACE:include>
)Without this split, consumers who call find_package(mylib) after installing it would inherit absolute paths that only exist on the build machine.
Pattern 4 — conditional link flags using $<TARGET_FILE:...> to copy a DLL next to an executable on Windows:
add_custom_command(TARGET myapp POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_FILE:mylib_shared>
$<TARGET_FILE_DIR:myapp>
)Toolchain Files — Cross-Compilation
A toolchain file tells CMake which compilers, sysroot, and search paths to use for a target platform instead of the host. Specify it once at configure time; every find_package, find_library, and compiler invocation in the build respects it.
Complete toolchain file for ARM64 Linux (e.g., targeting a Raspberry Pi 4 from an x86-64 host):
# toolchains/aarch64-linux-gnu.cmake
# Identify the target system
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
# Cross compilers — adjust the prefix to match your toolchain
set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++)
# Sysroot containing target headers and libraries
set(CMAKE_SYSROOT /opt/sysroots/aarch64-linux-gnu)
# Tell CMake where to look for target libraries/headers
set(CMAKE_FIND_ROOT_PATH
/opt/sysroots/aarch64-linux-gnu
/opt/aarch64-libs
)
# Only search the sysroot for libraries and headers;
# use the host for programs (cmake, python, etc.)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)Invoke it at configure time:
cmake -B build-arm64 \
-DCMAKE_TOOLCHAIN_FILE=toolchains/aarch64-linux-gnu.cmake \
-DCMAKE_BUILD_TYPE=Release
cmake --build build-arm64 -j$(nproc)vcpkg and Conan integrate via their own toolchain files. They each emit a toolchain file during install/install-setup, and you pass that file to CMake the same way — you can chain them with include() inside your own toolchain file:
# toolchains/aarch64-linux-gnu.cmake (extended for vcpkg)
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++)
# Pull in vcpkg's triplet + library paths after the target is set
include($ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake)Custom Commands and Targets
add_custom_command teaches CMake how to produce derived files — protobuf stubs, shader headers, version stamps — so they participate in the dependency graph and rebuild only when inputs change.
The canonical pattern: generate version.h from git describe so every build embeds the exact commit:
# Find git once
find_package(Git REQUIRED)
# Step 1 — a helper script that writes version.h (avoids re-running cmake)
# Alternatively, inline the command directly:
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/version.h
COMMAND ${CMAKE_COMMAND}
-DGIT_EXECUTABLE=${GIT_EXECUTABLE}
-DOUTPUT_FILE=${CMAKE_CURRENT_BINARY_DIR}/version.h
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/GenerateVersion.cmake
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/.git/HEAD # re-run on new commits
COMMENT "Generating version.h from git describe"
VERBATIM # don't let CMake mangle the arguments
)
# Step 2 — a target that depends on the generated file
add_custom_target(generate_version
DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/version.h
)
# Step 3 — sequence: myapp can't compile until generate_version finishes
add_executable(myapp src/main.cpp)
add_dependencies(myapp generate_version)
target_include_directories(myapp PRIVATE ${CMAKE_CURRENT_BINARY_DIR})cmake/GenerateVersion.cmake (the -P script, shown in the next section):
execute_process(
COMMAND ${GIT_EXECUTABLE} describe --tags --always --dirty
OUTPUT_VARIABLE GIT_DESCRIBE
OUTPUT_STRIP_TRAILING_WHITESPACE
)
file(WRITE ${OUTPUT_FILE}
"#pragma once\n#define PROJECT_VERSION \"${GIT_DESCRIBE}\"\n"
)For protobuf generation the pattern is the same — list .proto files as DEPENDS, list .pb.cc / .pb.h as OUTPUT, and use BYPRODUCTS to tell Ninja about side-effect files so it doesn't mark the build stale:
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/foo.pb.cc
${CMAKE_CURRENT_BINARY_DIR}/foo.pb.h
BYPRODUCTS ${CMAKE_CURRENT_BINARY_DIR}/foo.grpc.pb.cc # Ninja needs this
COMMAND protobuf::protoc
--cpp_out=${CMAKE_CURRENT_BINARY_DIR}
-I${CMAKE_CURRENT_SOURCE_DIR}/proto
${CMAKE_CURRENT_SOURCE_DIR}/proto/foo.proto
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/proto/foo.proto
VERBATIM
)Always use VERBATIM — without it, CMake applies platform-specific escaping that silently corrupts paths with spaces.
CPack — Packaging
include(CPack) turns your install rules into distributable packages with a single command. CPack reads CPACK_* variables you set before the include, so configure everything first.
Minimal working setup targeting a Linux .deb:
# Near the top of CMakeLists.txt, after project()
set(CPACK_PACKAGE_NAME "myapp")
set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION}) # e.g. 1.2.0
set(CPACK_PACKAGE_VENDOR "Acme Corp")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "My application")
set(CPACK_PACKAGE_CONTACT "pkg@example.com")
# Generators to enable (can specify several at once)
set(CPACK_GENERATOR "DEB;TGZ")
# Debian-specific
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libssl3 (>= 3.0), libstdc++6 (>= 12)")
set(CPACK_DEBIAN_PACKAGE_SECTION "utils")
set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "amd64") # or use CMAKE_SYSTEM_PROCESSOR
# Install rules must be defined before this line
include(CPack)Build the package:
# Option A — via cmake --build (calls cpack internally via the "package" target)
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
cmake --build build --target package
# Option B — run cpack directly from the build directory
cd build && cpack -G DEB
cd build && cpack -G TGZ
# Option C — build multiple generators in one shot
cd build && cpackThe result is myapp-1.2.0-Linux.deb (and .tar.gz) in the build directory. Other common CPACK_GENERATOR values:
| Value | Output | Platform |
|---|---|---|
TGZ | .tar.gz tarball | All |
ZIP | .zip archive | All |
DEB | .deb package | Debian/Ubuntu |
RPM | .rpm package | Fedora/RHEL |
NSIS | Windows installer (NSIS) | Windows |
WIX | Windows installer (WiX) | Windows |
For multi-component installs, set CPACK_COMPONENTS_ALL and tag each install() call with a COMPONENT argument; CPack will split them into separate packages when CPACK_DEB_COMPONENT_INSTALL ON is set.
CMake Scripting Mode
cmake -P script.cmake runs a CMake script without configuring a build tree, making it ideal for portable CI glue that avoids a bash/PowerShell dependency. Scripts have access to all CMake commands except target-related ones.
# scripts/prepare_artifacts.cmake
cmake_minimum_required(VERSION 3.20)
message(STATUS "Preparing release artifacts...")
# Collect built binaries
file(GLOB artifacts "${CMAKE_BINARY_DIR}/bin/*")
# Create output directory
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/dist")
# Copy each artifact
foreach(artifact IN LISTS artifacts)
get_filename_component(name "${artifact}" NAME)
message(STATUS " Copying ${name}")
file(COPY "${artifact}" DESTINATION "${CMAKE_BINARY_DIR}/dist")
endforeach()
message(STATUS "Done.")Run it from a CI step:
cmake -P scripts/prepare_artifacts.cmakePass variables in with -D — they arrive as CMake cache variables:
cmake -DCMAKE_BINARY_DIR=./build -P scripts/prepare_artifacts.cmakeThe cmake -E sub-commands are portable single operations you can call inline from any shell or from another CMake script:
cmake -E copy_if_different src.txt dst.txt # copy only if changed
cmake -E make_directory path/to/dir # mkdir -p equivalent
cmake -E remove_directory old/ # rm -rf equivalent
cmake -E tar czf out.tar.gz file1 dir/ # create archive
cmake -E sha256sum file.bin # compute checksumUse execute_process inside a -P script when you need to shell out and capture output:
execute_process(
COMMAND git describe --tags --always
OUTPUT_VARIABLE version
OUTPUT_STRIP_TRAILING_WHITESPACE
RESULT_VARIABLE git_exit_code
)
if(NOT git_exit_code EQUAL 0)
message(FATAL_ERROR "git describe failed (exit ${git_exit_code})")
endif()
message(STATUS "Version: ${version}")