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_controlcan be a library used by bothrobot_pipelineandrobot_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-sanitizepreset 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)
For a 100 Hz control loop, T = 1/100 s.
Convert: 0.01 s = 10 ms per cycle.
If work sometimes takes 13 ms, it misses the 10 ms deadline by 3 ms; measure the distribution, not only the average.
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
The build graph has targets and 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.
Create
robot_filteras a library containing the value types and filter calculation.Create
linux_serialas a separate library that links only the operating-system dependencies it needs.Create
robot_pipelineas an executable that links both libraries and owns composition and shutdown.Create
robot_filter_testsas an executable linked to the samerobot_filterproduction target.Mark each header path and compile feature PRIVATE, PUBLIC, or INTERFACE according to whether consumers need it.
Add a checked-in preset and run configure, build, and CTest from a new empty build directory.
The dependency graph states which products exist, tests reuse production code, and an unrelated target does not inherit serial-driver settings.
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.
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.
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.
Write the library header and source with no terminal input, device access, or global state.
Create a CMake library target and declare the required C++ standard with
target_compile_features.Create the console target and link it to the library target.
Create the test target, link the same library, enable testing, and register the test with CTest.
Add a project preset that selects an out-of-source build directory and a debug configuration.
Delete only the generated build directory, then configure, build, and test from scratch using the preset.
Save the tool versions and exact commands in a short build note.
A missing source, include path, requirement, or dependency becomes visible when the clean build cannot borrow state from an older build tree.
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
Adding include directories and compiler flags globally until every target builds.
Attach the smallest correct requirements to the target that owns them and propagate only what consumers truly need.
Compiling a separate copy of production source directly into tests.
Build production logic as a library target and link both the application and tests to that same target.
Claiming reproducibility because rebuilding twice on the same dirty machine succeeds.
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`?
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?
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`?
Project-wide shared configurations suitable for version control belong in CMakePresets.json; personal paths and local-only settings belong in the user file.