Skip to content
C++
Build System
Updated 2025-01-01T00:00:00.000Z

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
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.h
cmake
# 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/myapp

Targets — 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)
Keywordmylib itselfCallers 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 Release

Testing 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-failure

Installation 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 default

Common 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)
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

FeatureMinimum CMake
cmake_minimum_required + project()3.0
target_compile_features3.1
FetchContent3.11
FetchContent_MakeAvailable3.14
cmake --build --target3.15
cmake --install3.15
CMakePresets.json3.19
--preset flag3.20
Edit on GitHubUpdated 2025-01-01T00:00:00.000Z