Skip to content

Fix CMake/CTest configuration issues - #29

Merged
nehalkpatel merged 10 commits into
mainfrom
chore/cmake-user-presets-example
Aug 27, 2026
Merged

Fix CMake/CTest configuration issues#29
nehalkpatel merged 10 commits into
mainfrom
chore/cmake-user-presets-example

Conversation

@nehalkpatel

Copy link
Copy Markdown
Owner

Summary

An audit of the CMake/CTest setup found six issues, each verified empirically against the build rather than inferred. The most serious: Release builds were not actually -O3. Also adds the missing RelWithDebInfo presets, pins the last unpinned dependency, and collapses the three-places-per-test declaration pattern into a single helper.

Every change is verified from a wiped build/ directory. All three workflows pass 29/29.

Release was silently size-optimized

COMMON_COMPILE_OPTIONS appended -Os -g after the per-config flags, and the last -O on the command line wins:

Config Flags before Flags after
Debug -g -Os -g -g
Release -O3 -DNDEBUG -Os -g -O3 -DNDEBUG
RelWithDebInfo (unreachable) -O2 -g -DNDEBUG

Proven with a vectorizable loop: -O3 → 243 bytes of .text, -Os → 107, -O3 -Os107. So Release was size-optimized and Debug was optimized rather than debuggable.

Both flags were redundant anyway — cmake/toolchain/armgcc.cmake already sets CMAKE_C_FLAGS_DEBUG_INIT "-O0" and CMAKE_C_FLAGS_RELEASE_INIT "-Os -flto" plus -g3, so the ARM path is unaffected.

Every CTest property on the integration test was inert

All three set_tests_properties calls did nothing:

  • The first set DEPENDS "blinky;uart_echo;i2c_demo", the second set DEPENDS host_emulator_venv. set_tests_properties replaces rather than appends, so the first was silently discarded — the generated CTestTestfile.cmake showed only the second.
  • DEPENDS names tests, not targets. blinky et al. are executables, and host_emulator_venv is created at configure time by execute_process, so it isn't in the test registry at all.
  • FIXTURES_SETUP declares the test is a fixture for others; nothing declared a matching FIXTURES_REQUIRED.

The intent was presumably "build the apps first", but ctest does not build — verified by deleting build/host/bin and running the ctest preset alone: 28 tests "Not Run", integration test failed. The workflow presets exist for this and are what the docs, CI and docker-compose already use.

Replaced with TIMEOUT 300 (previously a hang would block until CTest's 1500s default) and LABELS "integration".

Presets

  • default was user-visible but unbuildablecmake --preset=default failed with add_subdirectory called with incorrect number of arguments (EMBEDDED_CPP_BOARD/_MCU unset). Now hidden, matching arm/arm-cm4/arm-cm7.
  • RelWithDebInfo had zero presets despite being in CMAKE_CONFIGURATION_TYPES and having build-RelWithDebInfo.ninja generated. It was reachable only via a bare --config. Now has build/test/workflow presets — which matters more since Release no longer carries -g.
  • host build and test presets are now hidden inheritance bases. The test preset was identical to host-debug; the build preset carried no configuration. Both relied on a trailing -C/--config to mean anything. The hidden default test preset already held the output/execution settings, but the host presets duplicated them verbatim — chaining default → host → per-config removes that.

Also fixed

-R test_zmq_transport matched no tests. gtest_discover_tests registers ZmqTransportTest.SendReceive — named for the fixture, not the source file — and noTestsAction: error made the documented command fail outright. Now -R ZmqTransportTest.

uv run --extra dev was broken. fe03b14 moved dev deps to a PEP 735 [dependency-groups] table but left CLAUDE.md documenting the old flag, which now errors: Extra 'dev' is not defined in the project's optional-dependencies table. uv syncs groups by default, so plain uv run is correct.

cmake-scripts tracked main. It supplies clang_tidy(), reset_clang_tidy(), add_code_coverage_all_targets() and target_code_coverage(), so upstream drift could change lint enforcement or coverage silently. Pinned to 25.08, verified from a clean build directory.

Test declaration collapsed into one helper

Each unit test was declared in three separate places — executable+options+libs, gtest_discover_tests, and target_code_coverage. Nothing tied them together, so a test added to one list but not the others failed silently. add_host_unit_test(name source [libs...]) makes it one line.

source is a parameter rather than derived as ${name}.cpp because test_host_transport is built from test_zmq_transport.cpp; a convention-based helper would break that target.

Behaviour verified identical by comparison, not just a passing build: 30 instrumented TUs and 136 ccov targets before and after.

Test plan

From a wiped build/:

  • host-debug, host-release, host-relwithdebinfo — 29/29 each
  • Per-config flags match the config name (table above)
  • LABELS "integration" and TIMEOUT "300" present in generated CTestTestfile.cmake
  • ctest -L unit → 28, -L integration → 1, total 29
  • cmake-scripts fetches 25.08; clang-tidy found; ccov-all generates its HTML report
  • All six documented commands run verbatim

Notes

  • Breaking for muscle memory: ctest --preset=host and cmake --build --preset=host no longer resolve. All documented commands are updated; docker-compose.yml and CI are unaffected (both use --workflow --preset host-debug).
  • ARM presets remain non-functional — src/libs/mcu/arm_cm4/ doesn't exist yet. Expected, and the docs say so.
  • ccov-all emits warning: 70 functions have mismatched data. Pre-existing, reproduces from a wiped tree with a single Debug build and no changes. Cause is header-defined inline/template functions (the mcu/board/error INTERFACE libraries) instantiated into all five test binaries, then merged. Benign — the report still generates.
  • The branch name predates most of this work; it's now mostly CMake/CTest fixes rather than the presets example.

nehalkpatel and others added 10 commits August 26, 2026 21:59
Salvaged from the abandoned claude/review-cmake-config branch, which is
otherwise fully superseded by main. CMakeUserPresets.json is gitignored, so
there was no template for developers building natively instead of in the dev
container -- mainly macOS, where Homebrew LLVM and the ARM GNU Toolchain live
at paths CMake will not find on its own.

Only the example file is taken. That branch also rewrote host-clang.cmake to
drop the -18 version suffixes and call bare `clang`, which would undo the
toolchain version pinning main relies on.

The presets as written on that branch did not actually work against main. The
`default` preset sets CMAKE_PRESET to ${presetName}, which resolves to the
derived preset's own name, so `if(CMAKE_PRESET STREQUAL "host")` in
CMakeLists.txt was false and googletest/cppzmq/nlohmann_json were never
fetched -- configure failed on missing targets. Each host-derived preset now
pins CMAKE_PRESET explicitly. The ARM presets are unaffected: nothing is gated
on the cm4 path they inherit.

Verified end to end: `cmake --preset=host-linux` configures, builds, and passes
29/29 tests. Also refreshed the stale /Applications ARM toolchain version and
moved the setup caveats into preset descriptions, since preset JSON permits no
comments.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
README and CLAUDE.md listed `cmake --workflow --preset=stm32f3_discovery-release`
under "ARM targets" as though it runs today. It cannot: src/libs/mcu/CMakeLists.txt
does add_subdirectory(${EMBEDDED_CPP_MCU}) and only the `host` implementation
exists, so configure fails on the missing arm_cm4/ directory. Verified against the
stock preset with no user presets present.

This is planned sequencing rather than a defect -- host build and emulation first,
hardware platforms after -- and docs/PROJECT_PLAN.md already lists STM32F3/F7 as in
progress. The build instructions were the only place that read as working, and they
contradicted the README's own Implementation Status table.

Note the ARM toolchain files and board directories are deliberately in place ahead
of the MCU layer; nothing here changes that scaffolding.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds nine skills under .claude/skills/ covering the development lifecycle:

  idea-refine                  raw idea -> sharp concept
  spec-driven-development      spec before code
  planning-and-task-breakdown  spec -> ordered, verifiable tasks
  test-driven-development      failing test first; repro test before bug fix
  incremental-implementation   thin vertical slices that stay green
  debugging-and-error-recovery scientific-method root-cause analysis
  code-review-and-quality      multi-axis review before merge
  git-workflow-and-versioning  atomic commits, short-lived branches
  context-engineering          agent context setup and recovery

These are committed rather than left local so the workflow is shared rather
than per-machine, consistent with .claude/settings.json. Personal overrides
still belong in .claude/settings.local.json, which stays gitignored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
COMMON_COMPILE_OPTIONS appended -Os and -g after the per-config flags. The last
-O on the command line wins, so Release compiled as -O3 -DNDEBUG -Os -g and was
actually size-optimized, while Debug compiled as -g -Os -g and was optimized
rather than debuggable. Confirmed against a vectorizable loop: -O3 gives 243
bytes of .text, -Os gives 107, and -O3 -Os gives 107.

Both flags are redundant with the per-config settings. cmake/toolchain/armgcc.cmake
already sets CMAKE_C_FLAGS_DEBUG_INIT "-O0" and CMAKE_C_FLAGS_RELEASE_INIT
"-Os -flto" plus -g3, so the ARM path is unaffected. The host build now uses
CMake's defaults: Debug -g, Release -O3 -DNDEBUG, RelWithDebInfo -O2 -g -DNDEBUG.

Note Release no longer carries -g; use RelWithDebInfo for an optimized build
with symbols.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
All three set_tests_properties calls on host_emulator_test were no-ops:

- The first set DEPENDS "blinky;uart_echo;i2c_demo", then the second set DEPENDS
  host_emulator_venv. set_tests_properties replaces rather than appends, so the
  first was silently discarded -- the generated CTestTestfile.cmake showed only
  the second.
- DEPENDS names tests, not targets. blinky/uart_echo/i2c_demo are executables,
  and host_emulator_venv is created at configure time by execute_process in
  cmake/python_venv.cmake, so it is not in the test registry at all. Neither
  form ever ordered anything.
- FIXTURES_SETUP declares that this test *is* a setup fixture for other tests.
  Nothing declared a matching FIXTURES_REQUIRED, so it did nothing.

The likely intent was to ensure the apps are built before the integration test
runs, but ctest does not build. Verified by deleting build/host/bin and running
the ctest preset alone: all 28 unit tests report "Not Run" and host_emulator_test
fails. No test property can fix that -- the workflow presets exist for it, and
they are what the docs, CI and docker-compose already use.

Replaced with TIMEOUT 300 (the test takes ~6s; previously it could hang until
CTest's 1500s default) and LABELS "integration", enabling `ctest -L integration`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Hide `default` (configure and build): it is a base for host/arm and cannot
configure standalone -- EMBEDDED_CPP_BOARD/_MCU are unset, so `cmake --preset=default`
failed with "add_subdirectory called with incorrect number of arguments".

Hide the `host` build and test presets and make them inheritance bases. The test
preset was identical to host-debug, and the build preset carried no configuration,
so both relied on a trailing -C/--config to mean anything. Now there is one
explicit preset per configuration. The hidden `default` test preset already held
the output/execution settings but host/host-debug/host-release duplicated them
verbatim; chaining default -> host -> per-config removes that duplication.

Add host-relwithdebinfo build, test and workflow presets. RelWithDebInfo is in
CMAKE_CONFIGURATION_TYPES and the generator emits build-RelWithDebInfo.ninja, but
CMakePresets.json did not mention it once, so it was only reachable via a bare
--config. It matters now that Release no longer carries -g: RelWithDebInfo is the
supported optimized-with-symbols build.

Update the documented commands accordingly. Also fix `-R test_zmq_transport`,
which matched no tests -- gtest_discover_tests registers ZmqTransportTest.SendReceive,
named for the fixture rather than the source file, and noTestsAction:error made it
fail. Now -R ZmqTransportTest.

docker-compose.yml and CI are unaffected; both use --workflow --preset host-debug.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fe03b14 moved the dev dependencies from [project.optional-dependencies] to a PEP
735 [dependency-groups] table but left CLAUDE.md documenting `uv run --extra dev`.
That now fails outright:

  error: Extra `dev` is not defined in the project's `optional-dependencies` table

uv syncs dependency groups by default, which was the point of the move, so the
flag is not just wrong but unnecessary -- plain `uv run pytest`, `uv run ruff` and
`uv run mypy` all work. Verified by running each documented command verbatim.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every other FetchContent_Declare pins a tag; cmake-scripts alone tracked `main`.
It supplies clang_tidy(), reset_clang_tidy(), add_code_coverage_all_targets() and
target_code_coverage(), so upstream changes could alter lint enforcement or
coverage instrumentation with no commit in this repo -- and silently, since a
FetchContent GIT_TAG branch is only re-resolved on a fresh fetch.

25.08 is the newest tag; the tree in use was 12 commits past it. Verified against
a clean build directory: the tag fetches, clang-tidy is found, coverage still
instruments 30 translation units, all 136 ccov targets generate, ccov-all
produces its HTML report, and the suite passes 29/29. No fallback to a SHA pin
was needed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add LABELS "unit" to the five gtest_discover_tests calls. With the "integration"
label added in 4b74acc, the suite now partitions cleanly: `ctest -L unit` selects
28, `ctest -L integration` selects 1, 29 total. Previously there was no way to run
the fast C++ tests without also spinning up the Python emulator.

Remove cmake_minimum_required from every subdirectory CMakeLists.txt. Only the
top-level call has effect -- the rest were copy-paste noise, and one used a
FATAL_ERROR argument the others did not, implying a distinction that never existed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each unit test was declared in three separate locations: the executable plus its
compile options and link libraries, a gtest_discover_tests call, and a
target_code_coverage call inside the CODE_COVERAGE block. Nothing tied them
together, so a test added to one list but not the others failed silently -- no
error, just a test that never gets labelled or never gets instrumented.

Collapse all three into add_host_unit_test(name source [libs...]). Adding a test
is now a single line that cannot omit either.

`source` is a parameter rather than being derived as ${name}.cpp because
test_host_transport is built from test_zmq_transport.cpp; a convention-based
helper would silently break that target.

Behaviour is unchanged, verified by comparison rather than a passing build alone:
29/29 tests, 28 unit and 1 integration by label, 30 instrumented translation
units, and 136 ccov targets -- all identical to before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nehalkpatel
nehalkpatel force-pushed the chore/cmake-user-presets-example branch from a85ef24 to 0007564 Compare August 27, 2026 04:59
@nehalkpatel
nehalkpatel merged commit 65240be into main Aug 27, 2026
1 check passed
@nehalkpatel
nehalkpatel deleted the chore/cmake-user-presets-example branch August 27, 2026 05:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant