C++ CMake Targets and Reproducible Builds

Build C++ libraries and apps with CMake targets, propagate usage requirements, distinguish build configurations, share presets, and run CTest checks.

Lesson 44 of 44Advanced10 min read

Compiling two files manually teaches the compiler and linker boundary, but a growing project needs a declared dependency graph. CMake describes libraries, executables and their requirements, then generates a native build system such as Ninja or a Visual Studio solution. A target-based design lets a consumer inherit the requirements of a library instead of repeating guessed flags in every command.

This guide uses C++17 and CMake 3.21 or later. Ninja is required for the shown single-configuration preset. You will build a small library and app, add an actual CTest check, and identify the inputs needed for repeatable builds. Nothing in CMake alone promises byte-identical output across arbitrary machines.

Configure, generate and build are distinct stages

During configuration, CMake reads project declarations, checks the selected toolchain and stores relevant settings in its build directory. Generation produces the chosen native build files. The build stage then invokes that native system to compile and link targets.

Keep the build directory separate from source. This avoids mixing generated files with authored code and permits independent Debug and Release directories. The cache belongs to a particular configured environment; changing compilers or important toolchain settings can require a fresh build directory. Do not copy a developer's cache as though it were the portable project specification. Source files and declared configuration should be sufficient to configure a clean checkout.

Targets carry their own requirements

Add_library and add_executable create named targets. Target_link_libraries declares relationships that CMake can use for build order and required linking. Target_compile_features can request a language standard capability, while target_include_directories supplies header search requirements.

PUBLIC requirements apply to the target and its consumers. PRIVATE requirements apply to the target's own build. INTERFACE requirements are for consumers. A library's public header directory should normally be a usage requirement, while implementation-only warning flags need not become a consumer contract. Avoid global include directories and flags when a target-specific declaration makes responsibility clear. Imported dependency targets can carry their own requirements, reducing the need to guess raw library paths.

Single and multiple configurations differ

Ninja's ordinary generator is single-configuration: CMAKE_BUILD_TYPE selects Debug or Release at configuration time. Keep separate directories when both are needed. Multi-configuration generators, such as Visual Studio or Ninja Multi-Config, choose a configuration at build time using a command such as cmake --build build --config Release.

Do not assume setting CMAKE_BUILD_TYPE changes a multi-configuration generator's selected output. Executable locations can also include a configuration subdirectory. The project's documented commands should name its generator and configuration model. Debug and Release commonly change symbols, optimization and assertion settings, but inspect actual compiler options rather than assuming every platform uses identical flags.

Presets make shared commands reviewable

CMakePresets.json can record a named configuration with generator, build directory and cache settings. Version three of the preset schema is available from CMake 3.21. Keep machine-specific secrets and local paths out of a committed shared preset. CMakeUserPresets.json is appropriate for local overrides under the team's ignore policy.

The following complete preset describes the same Debug configuration. A compatible compiler and Ninja must still be available through the environment or an explicit toolchain configuration.

JSON
{
  "version": 3,
  "configurePresets": [
    {
      "name": "debug",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/debug",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug"
      }
    }
  ],
  "buildPresets": [
    { "name": "debug", "configurePreset": "debug" }
  ]
}
SHELL
cmake --preset debug
cmake --build --preset debug

A preset reduces command drift, but it does not install or pin the compiler by itself. State those prerequisites alongside the repository's setup instructions.

Reproducibility requires controlled inputs

Record the source revision, compiler and standard library versions, CMake and generator versions, dependency identities and build configuration. Pin downloaded dependencies to reviewed versions or commits and verify appropriate hashes. Avoid a moving branch name when the requirement is a repeatable dependency graph.

Environment paths, timestamps, generated files and absolute source paths can affect artifacts. A clean target graph and preset improve repeatability but do not automatically produce byte-identical binaries. Define whether the team needs repeatable behavior or exact artifact identity, then use toolchain-specific controls and verification for that goal. Never claim reproducibility merely because two incremental builds on one machine appeared successful.

Diagnose the failing stage

An unknown header usually points to a compile or usage-requirement problem. An unresolved symbol can mean a target relationship or implementation is missing. A configure failure can identify an unavailable compiler, generator or dependency before any source is compiled.

Read the first useful diagnostic and verify the relevant target's declared sources and requirements. Avoid adding global paths until the build happens to work. Use a fresh separate directory when testing a changed toolchain, and retain the commands that reproduced the failure. Run the actual test target after building; a successful link is not evidence that its contract checks passed.

Official references

C++ CMake Targets and Reproducible Builds Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Build a pricing library and application

Create the following files under a new project directory. The public header belongs in include, and the implementation and application source belong in src.

Run build/debug/pricing_app on Unix-like systems or the corresponding pricing_app.exe on Windows. The output fence describes the application, while configure and build diagnostic lines depend on the generator and toolchain. The app inherits the public include directory and C++17 requirement through its library relationship.

C++
// include/pricing.hpp
#ifndef PRICING_HPP
#define PRICING_HPP
namespace pricing {
int total_cents(int unit_cents, int quantity);
}
#endif
C++
// src/pricing.cpp
#include "pricing.hpp"
#include <limits>
#include <stdexcept>

namespace pricing {
int total_cents(int unit_cents, int quantity) {
    if (unit_cents < 0 || quantity < 0)
        throw std::invalid_argument("negative input");
    if (quantity != 0 &&
        unit_cents > std::numeric_limits<int>::max() / quantity)
        throw std::overflow_error("total overflow");
    return unit_cents * quantity;
}
}
C++
// src/main.cpp
#include "pricing.hpp"
#include <iostream>

int main() {
    std::cout << "Total cents: "
              << pricing::total_cents(250, 3) << '\n';
}
CMAKE
cmake_minimum_required(VERSION 3.21)
project(PricingDemo LANGUAGES CXX)

add_library(pricing STATIC src/pricing.cpp)
target_include_directories(pricing PUBLIC
    "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_compile_features(pricing PUBLIC cxx_std_17)

add_executable(pricing_app src/main.cpp)
target_link_libraries(pricing_app PRIVATE pricing)
set_target_properties(pricing pricing_app PROPERTIES
    CXX_EXTENSIONS OFF)
SHELL
cmake -S . -B build/debug -G Ninja -DCMAKE_BUILD_TYPE=Debug
cmake --build build/debug
Output
Total cents: 750

C++ CMake Targets and Reproducible Builds in Real Projects

Worked exercise: add a real CTest check

The task is to verify a valid total and rejection of negative input through the existing library. Add tests/pricing_checks.cpp and append the shown CMake declarations to the existing CMakeLists.txt. This is a complete test executable; failures return a nonzero status that CTest can observe.

C++
// tests/pricing_checks.cpp
#include "pricing.hpp"
#include <iostream>
#include <stdexcept>

int main() {
    try {
        if (pricing::total_cents(250, 3) != 750)
            throw std::runtime_error("wrong total");
        bool rejected = false;
        try {
            (void)pricing::total_cents(-1, 3);
        } catch (const std::invalid_argument&) {
            rejected = true;
        }
        if (!rejected)
            throw std::runtime_error("negative input accepted");
        std::cout << "Pricing checks passed\n";
        return 0;
    } catch (const std::exception& error) {
        std::cerr << error.what() << '\n';
        return 1;
    }
}
CMAKE
enable_testing()
add_executable(pricing_checks tests/pricing_checks.cpp)
target_link_libraries(pricing_checks PRIVATE pricing)
set_target_properties(pricing_checks PROPERTIES CXX_EXTENSIONS OFF)
add_test(NAME pricing_contract COMMAND pricing_checks)
SHELL
cmake --preset debug
cmake --build --preset debug
ctest --test-dir build/debug --output-on-failure
Output
Pricing checks passed

Running the test executable directly prints the shown line. CTest normally summarizes the passing test and shows its output with verbose mode; --output-on-failure reveals captured output when a test fails. Reconfiguration registers the new target and test before rebuilding.

C++ CMake Targets and Reproducible Builds FAQ

Does CMake compile the sources itself?

It configures and generates a native build system, which the build stage invokes to compile and link the targets. The selected compiler still determines language support and diagnostics. Distinguish configure, generation and build failures when investigating a problem. A portable CMake declaration cannot compensate for an unavailable or incompatible compiler, generator or standard library.

Why make the include directory PUBLIC?

The library's own source needs the public header, and consumers of that library also need its declared header path. PUBLIC expresses both responsibilities. An implementation-only directory would normally be PRIVATE. Usage requirements propagate through target relationships, so consumers should link the library target rather than independently guess its directories and flags. Keep private implementation details out of the public interface.

Why does --config Release not replace every CMAKE_BUILD_TYPE setting?

The configuration model depends on the generator. Ordinary Ninja uses a single build type chosen during configuration, while multi-configuration generators select among configurations during the build. Name the generator in instructions and use commands that fit its model. Separate build directories make single-configuration Debug and Release inputs clear and avoid confusing cache state.

Do presets guarantee byte-identical binaries?

No. Presets record useful configuration but do not by themselves pin all tools, dependencies, environment values or artifact metadata. Reproducibility needs a defined goal and controlled inputs. Exact binary identity can require compiler-specific path and timestamp controls. Verify that requirement directly under the intended environment instead of inferring it from one successful local build.

C++ CMake Targets and Reproducible Builds Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choicemedium

A new CTest executable links successfully. What establishes that its checks pass?

C++ CMake Targets and Reproducible Builds Quiz

2 questions to check what you learned.

  1. Which command relationship declares that pricing_app uses pricing?

    • A global include_directories call alone.
    • Listing pricing.cpp in the library without linking the library target to the app.
    • target_link_libraries(pricing_app PRIVATE pricing).
    • Adding the library include directory while omitting its link dependency.
  2. For ordinary single-configuration Ninja, where is Debug selected?

    • Through CXX_EXTENSIONS OFF, which selects optimization and debug-symbol settings.
    • At build time solely with --config Debug after configuring a Release directory.
    • Automatically from every --config option regardless of generator.
    • At configuration through CMAKE_BUILD_TYPE or an equivalent preset setting.