Learn C++ - Modern Tooling & Build Automation
Series/Learn C++/Episode 19
Episode 19 of 24

Learn C++ - Modern Tooling & Build Automation

This episode covers modern C++ tooling: CMake workflows, continuous integration with GitHub Actions and GitLab CI, static analysis and code formatting with clang-format, as well as reproducible builds and dependency management.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Great code means nothing without a process that keeps it high quality. In real projects, you don't wait for humans to check everything — tooling takes over: CMake builds correctly on all platforms, CI runs tests and analysis automatically, formatters keep consistency, and dependency management keeps builds reproducible.

Episode 19 covers modern tooling: correct CMake workflows, continuous integration pipelines with GitHub Actions and GitLab CI, clang-format and clang-tidy for code consistency, as well as reproducible builds and dependency management.

Modern CMake Workflows

The Correct CMake Structure

Modern CMake no longer uses global variables. Each target declares its own needs with target_compile_features and target_link_libraries. This target-based approach makes dependencies propagate transparently:

Modern CMake
cat > CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.20)
project(workspace LANGUAGES CXX)
 
add_library(utilitas STATIC util.cpp)
target_compile_features(utilitas PUBLIC cxx_std_20)
target_include_directories(utilitas PUBLIC .)
 
add_executable(app main.cpp)
target_link_libraries(app PRIVATE utilitas)
EOF
cmake -S . -B build -G Ninja
cmake --build build
ctest --test-dir build

cmake -S . -B build -G Ninja configures the project with the Ninja generator, which is faster than Make. target_compile_features(utilitas PUBLIC cxx_std_20) sets C++20 for the library and everything that links against it. ctest --test-dir build runs the registered tests.

Presets and Build Modes

Use CMake presets to standardize how the team builds. Presets define debug and release configurations in a single CMakePresets.json file, so everyone uses the same commands — without memorizing flags:

CMakePresets.json
{
  "version": 3,
  "configurePresets": [
    {
      "name": "debug",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/debug",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug",
        "CMAKE_CXX_FLAGS": "-fsanitize=address"
      }
    }
  ]
}

The debug preset above enables AddressSanitizer automatically. Calling cmake --preset debug then cmake --build --preset debug produces a consistent build for every team member.

Continuous Integration

The GitHub Actions Pipeline

CI runs builds, tests, and analysis on a server every time there's a change. A GitHub Actions pipeline for C++ usually uses a matrix of compilers and operating systems:

GitHub Actions for C++
name: cpp-ci
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: sudo apt install -y ninja-build
      - run: cmake -S . -B build -G Ninja
      - run: cmake --build build
      - run: ctest --test-dir build --output-on-failure

cmake --build build builds the project in CI, and ctest --test-dir build --output-on-failure runs the tests with failure reports. The pipeline runs for every push and pull request, catching regressions before code is merged.

GitLab CI

GitLab CI uses .gitlab-ci.yml with a similar stages concept. The key to success in both: builds and tests must run without manual interaction, and build artifacts should be uploaded so they can be downloaded for further analysis.

Formatting and Linting

clang-format for Consistency

clang-format automatically formats code following the rules set in .clang-format. Every team member uses an identical format, so diffs stay clean and reviews focus on logic instead of whitespace differences:

Format and lint
clang-format -i main.cpp util.cpp
clang-tidy main.cpp -- -std=c++20
cppcheck --enable=all main.cpp

clang-format -i rewrites files according to the configuration. clang-tidy and cppcheck — already covered in episode 11 — detect bugs and style issues. A common rule for formatter configuration: commit the .clang-format file so everyone uses the same one.

Checking Format in CI

Inconsistent formatting can block merges. Add a CI step that checks whether code is already formatted:

Check format in CI
clang-format --dry-run --Werror main.cpp

clang-format --dry-run --Werror only reports files that don't match the format without changing them, and returns an error code. The pipeline rejects changes that aren't formatted — this keeps consistency without relying on individual discipline.

Reproducible Builds and Dependency Management

Rebuilding Identical Outputs

A reproducible build means two builds of the same code produce identical binaries. Factors that break reproducibility: build timestamps, absolute paths, and environment variations. CMake helps through fixed CMAKE_CXX_FLAGS and sources stored together:

Reproducible build
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \
    -DCMAKE_CXX_FLAGS="-std=c++20 -O2 -ffile-prefix-map=$PWD=."
cmake --build build

-ffile-prefix-map=$PWD=. replaces absolute paths in metadata with relative ones, removing one source of inconsistency. For similar reasons, CI should use version-pinned container images.

Dependency Management

Managing C++ dependencies isn't as easy as in other languages. Vendor with git submodules or FetchContent for small dependencies, and package managers like vcpkg or Conan for large ones. FetchContent lets you put a dependency directly in a CMakeLists:

FetchContent
include(FetchContent)
FetchContent_Declare(json
  GIT_REPOSITORY https://github.com/nlohmann/json
  GIT_TAG v3.11.3)
FetchContent_MakeAvailable(json)

FetchContent_MakeAvailable(json) downloads and builds the nlohmann JSON library at the fixed version v3.11.3. Pinning versions (not branches) is a requirement for reproducible builds.

Tip

Use vcpkg manifest mode (vcpkg.json) or a Conan lockfile so dependency versions are pinned for the whole team and CI. Episode 20 will cover library portability further.

Conclusion

Here's what to take away:

  • Modern CMake is target-based: target_compile_features and target_link_libraries.
  • Presets standardize build configuration for the whole team.
  • GitHub Actions and GitLab CI run builds and tests automatically.
  • clang-format keeps consistency; check formatting in CI.
  • Reproducible builds avoid timestamps and absolute paths.
  • Vcpkg, Conan, and FetchContent manage dependencies with pinned versions.

In the next episode, episode 20, we'll discuss cross-platform development — writing portable code for Windows, Linux, and macOS, platform-specific abstractions and conditional compilation, cross-compilation for different target architectures, as well as vendor-neutral library and dependency portability.

Learn C++ - Modern Tooling & Build Automation | Learn C++