Phase 01 · Week 4 · 90 minutes

Day 24: CMake targets, libraries, and reproducible builds

Modern C++, Linux, and real-time habits · Deterministic, observable software at the hardware boundary.

Chapter 04 · Production C++, Linux, and deterministic timing

Today in the field story

One problem, then the next

The gateway’s components now have boundaries, but the old build depends on global flags and a dirty directory. Model the Night-Shift Sensor Gateway as library, executable, and test targets with explicit usage requirements and a checked-in preset. Rebuild from a clean tree so missing dependencies and accidental generated state become visible rather than being repaired by repeated deletion.

Why now

Reproducible artifacts need a target graph before Linux deployment and CI can be trusted.

Ignore today

Ignore packaging for multiple operating systems; qualify one course path first.

Unlocks next

A one-command clean build used by sanitizer, replay, timing, and CI gates.

Understand

Build the physical picture first

A build system is a labelled assembly plan: targets are the finished parts, dependencies are the required subassemblies, and each requirement travels only to the parts that actually need it.

Compiling turns one source file into an object file; linking combines object files and libraries into an executable or shared library. A robot repository normally has several such products: a reusable control library, a hardware adapter, an executable, and tests. CMake describes these products as targets. add_library and add_executable create named targets, while target_link_libraries connects them. Think in targets rather than global compiler flags, because a target is the unit that owns its source files, requirements, and dependencies.

Usage requirements say what a target and its consumers need. A PRIVATE include directory or definition is used only while building that target. A PUBLIC requirement is needed by the target and by consumers that link it. An INTERFACE requirement is for consumers only, often because it appears in public headers. These words do not control symbol visibility and they are not decoration. Choosing the wrong scope can make a build pass accidentally on one machine because an unrelated global setting leaked across the project.

A reproducible build begins with a declared toolchain and configuration, not with deleting a build folder until an error disappears. State the minimum CMake version, compiler family and supported range, C++ language standard, dependency versions, build type, and important options. A checked-in CMakePresets.json can give developers and CI the same named configure and build settings, while personal paths or secrets stay in CMakeUserPresets.json and out of version control. Pinning everything does not guarantee byte-identical binaries across all operating systems, but it makes the input configuration reviewable and repeatable.

Tests should be first-class targets. Build production code once as a library, link the executable and test executable to that same library, and register tests with CTest. Enable strict warnings for code you own, but avoid forcing those flags blindly onto third-party dependencies. A clean build must start from an empty build directory and succeed using documented commands. If another engineer needs an undeclared environment variable, an include path from your laptop, or a hand-edited generated file, the build recipe is incomplete.

Words you need

Name each idea precisely

Compilation

Translation of a source file into an object file for a particular compiler and configuration.

Physical example:

Each control or driver source file becomes one intermediate part before final assembly.

Linking

Combining object files and libraries while resolving referenced symbols into a final program or library.

Physical example:

The pipeline executable links the controller library and operating-system threading library.

CMake target

A named build product, such as a library, executable, or test, with its own sources and usage requirements.

Physical example:

robot_control can be a library used by both robot_pipeline and robot_control_tests.

Usage requirement

A compile, include, feature, definition, or link requirement attached to a target and propagated according to its scope.

Physical example:

Consumers need the public header directory of the controller library but not its private implementation directory.

Build preset

A named, shareable set of CMake configure, build, or test settings.

Physical example:

Developers and CI both use a checked-in dev-sanitize preset instead of retyping different flags.

Math, one line at a time

Work through today’s relationship

Prerequisite rescue · optionalRates, deadlines, jitter, and memory budgets

Hardware-facing software must finish work predictably before its deadline.

f
loop frequencyUnit: hertz (Hz)
T = 1/f
time available for one loopUnit: seconds (s)
jitter
variation around the expected timingUnit: milliseconds (ms)
  1. For a 100 Hz control loop, T = 1/100 s.

  2. Convert: 0.01 s = 10 ms per cycle.

  3. If work sometimes takes 13 ms, it misses the 10 ms deadline by 3 ms; measure the distribution, not only the average.

Programmer analogy

This is a strict rendering or audio-processing budget, except a missed robot deadline can destabilize an actuator.

How much time does a 50 Hz loop have per cycle?

1/50 s = 0.02 s = 20 ms.

The test invariant is

Brt=(50 s1)(0.1 s)=5 items.B\ge r t=(50\ \mathrm{s^{-1}})(0.1\ \mathrm{s})=5\ \mathrm{items}.

The build graph has 33 targets and 22 directed dependency edges; it must remain acyclic.

Design the target graph before writing CMake

The project contains a pure filter, a Linux serial adapter, one command-line pipeline, and filter tests.

  1. Create robot_filter as a library containing the value types and filter calculation.

  2. Create linux_serial as a separate library that links only the operating-system dependencies it needs.

  3. Create robot_pipeline as an executable that links both libraries and owns composition and shutdown.

  4. Create robot_filter_tests as an executable linked to the same robot_filter production target.

  5. Mark each header path and compile feature PRIVATE, PUBLIC, or INTERFACE according to whether consumers need it.

  6. Add a checked-in preset and run configure, build, and CTest from a new empty build directory.

Result

The dependency graph states which products exist, tests reuse production code, and an unrelated target does not inherit serial-driver settings.

What this proves

A modern CMake file models products and their contracts; it is not a shell script made of global flags.

Physical examples

Where this appears in real life

Subassemblies in a robot kit

A wheel module contains its private gears but exposes mounting holes and a shaft contract to the chassis that consumes it.

Look for:

Private implementation needs stay inside the module; public connection requirements travel to its consumer like target usage requirements.

Recipe with a mystery ingredient

A second cook follows a recipe but fails because the first cook silently used a spice already sitting on one kitchen shelf.

Look for:

A build that relies on an undeclared local package or path is not reproducible even if it succeeds repeatedly for its author.

Hands-on exercise

Make the idea observable

Make a three-file harmless project: one small moving-average library, one console executable, and one test executable.

  1. Write the library header and source with no terminal input, device access, or global state.

  2. Create a CMake library target and declare the required C++ standard with target_compile_features.

  3. Create the console target and link it to the library target.

  4. Create the test target, link the same library, enable testing, and register the test with CTest.

  5. Add a project preset that selects an out-of-source build directory and a debug configuration.

  6. Delete only the generated build directory, then configure, build, and test from scratch using the preset.

  7. Save the tool versions and exact commands in a short build note.

Observe

A missing source, include path, requirement, or dependency becomes visible when the clean build cannot borrow state from an older build tree.

Done when

A fresh clone-equivalent folder can build the executable and pass the registered test using the documented preset and no hand-edited generated files.

Build today

Create a Linux C++ sensor→filter→controller pipeline with device permissions, bounded timing, tests, CI, structured logs, and deterministic replay.

Evidence to save

DONE when a deterministic “CMake targets, libraries, and reproducible builds” failure test reports expected versus actual behavior and passes after the documented fix.

Common mistakes

Catch the wrong mental model

Wrong

Adding include directories and compiler flags globally until every target builds.

Better

Attach the smallest correct requirements to the target that owns them and propagate only what consumers truly need.

Wrong

Compiling a separate copy of production source directly into tests.

Better

Build production logic as a library target and link both the application and tests to that same target.

Wrong

Claiming reproducibility because rebuilding twice on the same dirty machine succeeds.

Better

Verify from a clean build tree with declared tools, dependencies, options, presets, and commands; distinguish repeatable configuration from byte-identical output.

Job connection

How this becomes employable evidence

Package control logic, hardware adapters, executables, and tests as reviewable CMake targets that build in both developer and CI environments.

Relevant target roles

  • Robotics Software Engineer — ROS 2 / AMR
  • Robotics Application / ROS 2 Integration Engineer
  • Robotics Deployment, Integration & Validation Engineer

Chapter 04 interview drill

Interview questions: CMake targets, libraries, and reproducible builds

Practise a 60–90 second answer: define the idea, connect it to a physical robot, state assumptions, frames, and units when relevant, then finish with the failure signal or evidence you would inspect.

Primary interview scenario

A CMake project builds on one laptop but not in CI. Explain how you would inspect target requirements, undeclared dependencies, presets, compiler versions, and clean-build evidence before adding flags.

Answer shape: clarify the situation → trace the physical and software path → test the most likely boundaries → name the evidence that would confirm the result.

Technical follow-up questions

Q1When should a library include directory be `PUBLIC`?
Model interview answer

When the library needs it while building and consumers also need it to compile against the library's public headers.

Q2Why should a test link the production library target?
Model interview answer

It exercises the same compiled production code and declared requirements instead of a second, potentially different test-only build of the sources.

Q3What belongs in `CMakePresets.json` rather than `CMakeUserPresets.json`?
Model interview answer

Project-wide shared configurations suitable for version control belong in CMakePresets.json; personal paths and local-only settings belong in the user file.