Building with CMake

C++ · Practical · 15 min

CMake is the de-facto build system for C++; nearly every library and IDE speaks it.

Compiling by hand stops scaling after a few files. CMake is a build-system generator: you describe your project in CMakeLists.txt, and CMake generates Ninja files, Makefiles or Visual Studio projects. IDEs (CLion, VS Code, Visual Studio) open CMake projects directly.

Modern CMake is all about targets:

  • add_executable(app main.cpp) and add_library(core STATIC a.cpp b.cpp) define targets.
  • target_link_libraries(app PRIVATE core) links them, and also pulls in core's public include paths and flags.
  • target_include_directories, target_compile_options and target_compile_definitions attach settings to one target.

The keywords matter. PRIVATE applies only to this target, PUBLIC applies to it and everything that links to it, and INTERFACE applies only to dependents. Avoid the old global commands like include_directories() and add_compile_options() in new code.

Set the standard with set(CMAKE_CXX_STANDARD 20), CMAKE_CXX_STANDARD_REQUIRED ON and CMAKE_CXX_EXTENSIONS OFF (so you get -std=c++20, not -std=gnu++20). CMAKE_EXPORT_COMPILE_COMMANDS writes compile_commands.json, which clangd and clang-tidy use.

Dependencies come in through find_package(Foo CONFIG REQUIRED) for installed packages (often via vcpkg or Conan), or FetchContent to download and build a dependency as part of your project.

Always build out of source: cmake -S . -B build configures into a separate build directory, cmake --build build compiles only what changed, and ctest runs tests registered with add_test. Choose -DCMAKE_BUILD_TYPE=Debug or Release with single-config generators. CMake presets (CMakePresets.json) let a team share these settings.

Example

cmake_minimum_required(VERSION 3.20)
project(inventory LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)        # compile_commands.json for tools

add_library(core STATIC src/inventory.cpp src/storage.cpp)
target_include_directories(core PUBLIC include)   # users of core see include/
if(MSVC)
  target_compile_options(core PRIVATE /W4)
else()
  target_compile_options(core PRIVATE -Wall -Wextra -Wpedantic)
endif()

find_package(fmt CONFIG REQUIRED)            # an installed dependency
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE core fmt::fmt)

enable_testing()
add_executable(core_tests tests/core_tests.cpp)
target_link_libraries(core_tests PRIVATE core)
add_test(NAME core_tests COMMAND core_tests)

# cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
# cmake --build build -j
# ctest --test-dir build

Remember

  • Think in targets: add_library/add_executable + target_* commands
  • PRIVATE / PUBLIC / INTERFACE control what propagates to dependents
  • Configure out of source: cmake -S . -B build, then cmake --build build

Watch out for

  • Using global include_directories() and raw CMAKE_CXX_FLAGS edits instead of per-target settings.

Check yourself

  1. Library `core` has `target_include_directories(core PUBLIC include)`. App links core with `target_link_libraries(app PRIVATE core)`. Can app's sources include headers from core's include/ directory?
  2. Why set CMAKE_CXX_EXTENSIONS OFF?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: cmake.org/cmake/help/latest/guide/tutorial/index.html