Skip to content

docs(skills): lean, targeted builds in /pr-review and /pr-land - #6215

Merged
ten9876 merged 5 commits into
mainfrom
docs/skills-targeted-builds
Oct 5, 2026
Merged

ten9876 merged 5 commits into
mainfrom
docs/skills-targeted-builds

Conversation

@ten9876

@ten9876 ten9876 commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

/pr-review and /pr-land both tell the agent to build the PR head. The obvious way to do that builds the default all target, which is the whole tree. This PR changes both skills to configure a lean build and build only the targets the review actually runs.

Why: what a full build costs

Measured while reviewing #6195 and #6198. Each head was built on Linux (GCC, Qt 6.12.0, RelWithDebInfo, static core: the default configuration):

Full tree (default all)
Build directory 84 GB per PR (two PRs plus one merge base: ~172 GB)
Of which, top-level executables 83.7 GB
Test executables ~628; 232 of them over 100 MB each
AetherSDR 832 MB, of which ~680 MB is .debug_* sections and ~137 MB code and data
Largest tests radio_setup_*_test ~397 MB each, weather_radar_loading_test 408 MB

The review itself ran about twenty of those tests.

Two causes:

  1. The static core is copied into every test. Each executable that links aethercore absorbs a full static copy. The AETHER_SHARED_CORE comment in CMakeLists.txt already records this ("~200 MB in a Debug ASan build … shared, they average ~13 MB"); the sanitizer lane switched to a shared core for exactly this reason.
  2. Full debug info per binary. -g under RelWithDebInfo makes debug info about 5× the size of the code. That ratio repeats across every binary.

What changes

/pr-review step 7, new "Build only what you run" paragraph:

cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo \
  -DAETHER_SHARED_CORE=ON \
  "-DCMAKE_CXX_FLAGS_RELWITHDEBINFO=-O2 -g1 -DNDEBUG" \
  "-DCMAKE_C_FLAGS_RELWITHDEBINFO=-O2 -g1 -DNDEBUG"
cmake --build build --parallel --target AetherSDR <test_a> <test_b>
ctest --test-dir build -R '^(test_a|test_b)$' --output-on-failure
  • Choosing tests: the ones the PR adds or modifies, the ones its body names, and the ones whose tests/tests.cmake block compiles a file the PR touches.
  • Full-tree builds are kept for claims about the tree itself (CMake or link-contract changes), and the report must say when one was done.
  • The merge-base comparison ("before blaming the PR") builds the same targets instead of a clean full tree.
  • Caveats, stated in the skill:
    • Linux and macOS only. AETHER_SHARED_CORE is a configure error on Windows, and -g1 is a GCC/Clang flag.
    • The app loads the core as a shared library, which isn't the shipped shape. Fine for driving behaviour; a finding that could depend on link mode should say so.
    • A test that calls WDSP directly can't rely on the shared core exporting it (WDSP is a private, hidden-visibility dependency). It links aether_wdsp explicitly or goes through WdspChannel.h; no static reconfigure is needed.
    • -R matches the add_test(NAME …) name, which can differ from the target name, so both recipes pass --no-tests=error. A filter that matches nothing fails instead of passing silently.
    • One binary can also be registered under several names (deepfist_cw_model_test five times; test: green the sanitizer lane — scale TIMEOUTs under sanitizers; feed rtl_capture_worker_test in lockstep. Principle VIII. #6208 adds rtl_capture_worker_slow_device_test on rtl_capture_worker_test). A filter built from the target name runs only one, and --no-tests=error can't notice, so the names come from grep -n 'COMMAND <target>' tests/tests.cmake.
    • Test selection follows an OBJECT library or …_SOURCES list to its consumers. After build: compile shared test sources once, and default RelWithDebInfo to -g1 (#6214) #6217 the logging sources appear once in tests.cmake, and files compiled into aethercore reach every test that links it.
    • An unfiltered ctest reports unbuilt tests as "Not Run". That means the -R filter is missing, not that a test failed.

/pr-land step 5: the old cmake --build build --parallel (full tree) becomes the same lean configuration and named targets. The selection also includes the tests covering the findings being fixed.

Evidence for the lean recipe

Same machine and toolchain, on main at this branch's base. Built AetherSDR plus radio_setup_label_theme_token_test, sim_backend_test and frameless_resizer_test:

Full tree (before) Lean build (this PR)
Build directory 84 GB 1.3 GB
AetherSDR 832 MB 144 MB (+ 134 MB libaethercore.so, shared by all targets)
radio_setup_label_theme_token_test 397 MB 18 MB
Cold build, all ccache misses — 219 s
  • ldd build/AetherSDR resolves libaethercore.so from the build tree, so the shared core really is the one in use.
  • ctest -R '^(radio_setup_label_theme_token_test|sim_backend_test|frameless_resizer_test)$': 100% of 3 passed.

For reference, a second worktree at the same commit, building the same targets, took 41 s with a 99.4% ccache hit rate (1094/1101). That needs a local ccache setting (base_dir set and hash_dir=false, so absolute worktree paths don't split the cache key). It is a per-machine choice, so it is not in the skill text.

Also: a test that tested nothing under -DNDEBUG

tests/digital_voice_mode_registry_test.c does all of its checking inside plain assert(), with the calls under test inside them. RelWithDebInfo and Release define NDEBUG, so there it compiled to an empty main and passed whatever the registry did. That covers CMake's default RelWithDebInfo flags, CI's build and TSan lanes, and this PR's recipe; only the Debug (ASan) lane ever ran it.

#undef NDEBUG before <assert.h> fixes it, since <assert.h> re-reads NDEBUG on each inclusion. Built at -O2 -g1 -DNDEBUG, with the first assertion inverted on purpose:

Unmodified test First assertion inverted
Before 100% passed 100% passed (the check never ran)
After 100% passed 0% passed, 1 failed

It is the only test that includes <assert.h> without handling NDEBUG. The automated code-review pass found it while checking this PR's recipe, and it is folded in here by maintainer decision.

Not in this PR

🤖 Generated with Claude Code

ten9876 and others added 2 commits October 5, 2026 13:49
…inciple VIII.

Both skills told the agent to build the PR head, and the obvious way to do
that builds the default `all` target: several hundred test executables, most
linking a large slice of the app, each with its own debug info. A two-PR
review left ~170 GB of build trees behind to run about twenty tests.

Both now build `AetherSDR` plus the tests the diff selects (added/modified,
named in the body, or compiling a touched file per tests/tests.cmake), run
ctest with a matching -R filter, and keep a full-tree build for CMake or
link-contract changes. The pr-review merge-base comparison builds the same
targets.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The review/land build recipe now configures AETHER_SHARED_CORE=ON (the
sanitizer lane's link mode) and -g1 debug info on Linux/macOS. Measured on
main with Qt 6.12: AetherSDR plus three tests is a 1.3 GB tree instead of
84 GB for the full RelWithDebInfo build; the app binary drops from 832 MB to
144 MB and radio_setup_label_theme_token_test from 397 MB to 18 MB. The three
tests pass. The skill names the caveats: the app links the core dynamically,
which is not the shipped shape, and WDSP-direct tests need a static build.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue fit

No linked issue, and none is required: GOVERNANCE.md:115 lists "Documentation additions and corrections" under What does NOT require an RFC, and this changes only skill prose. Reviewed against the PR's own stated intent — replace the implicit full-tree all build in /pr-review and /pr-land with a lean, named-target build — and it delivers exactly that. The body's measurement table is unusually good for a docs PR; most of what I checked below was checking it, not the prose.

One note on the body: the "Caveats, stated in the skill" bullet about WDSP is wrong in the same way the skill text is (finding 1), so the error is in both places and the body bullet needs the same correction.

Scope

File What it changes Claimed by title/body? Verdict
.claude/skills/pr-review/SKILL.md Adds the "Build only what you run" block (457-489); rewords the merge-base rule at 500-502 to build the same targets rather than a clean full tree Yes — body describes both In scope
.claude/skills/pr-land/SKILL.md Replaces the full-tree cmake --build build --parallel at 442-452 with the lean configure + named targets, cross-referencing /pr-review step 7 Yes — body's "/pr-land step 5" section In scope

Everything in the diff is explained by the stated intent. No source, CMake, CI, or test file is touched — I confirmed the body's "No change to CMakeLists.txt, tests/tests.cmake, CI or release builds" claim against the file list, and it holds. No new public surface, no CHANGELOG.md entry (correct), no deleted guard. The - lines remove only the two superseded build commands.

Blockers

None.

Nits

1. .claude/skills/pr-review/SKILL.md:475-476 — the WDSP caveat inverts its source and prescribes a needless full reconfigure.

A test that calls WDSP directly does not
link against the shared core by design; reconfigure static for that one.

The comment this paraphrases (CMakeLists.txt:156-160) says something different, and gives a different remedy:

A test that calls WDSP directly while linking only aethercore links against
the static core and fails here; go through core/dsp/WdspChannel.h.

Two problems. "Does not link against the shared core by design" reads as a graceful fallback; the mechanism described in CMake is a link failure — WDSP is a PRIVATE dependency (CMakeLists.txt:1616) built with hidden visibility, so its symbols stop at the shared-library boundary. And the sanctioned fix is to route through WdspChannel.h, not to reconfigure the tree static — which costs the full rebuild this PR exists to avoid.

In practice the case does not arise today: both tests that reach WDSP directly already link it explicitly — tests/tests.cmake:1570 (wdsp_allocation_scope_test PRIVATE aether_wdsp Threads::Threads) and :1575 (rtl_receive_pipeline_test PRIVATE aethercore aether_wdsp Qt6::Core). And .github/workflows/sanitizers.yml builds the whole tree (cmake --build build --parallel, line 162) with shared_core: "ON" in both matrix rows (lines 42, 75), with no exclusion for either test. So the entire tree, those two included, builds shared in CI today. Suggested replacement:

finding could depend on link mode. A test that reaches into WDSP while linking
only `aethercore` fails to link under a shared core: WDSP is a PRIVATE
dependency built with hidden visibility, so its symbols stop at the library
boundary. Both such tests in the tree already link `aether_wdsp` explicitly
(`tests/tests.cmake:1570`, `:1575`); on a new one, go through
`core/dsp/WdspChannel.h` or link `aether_wdsp` rather than reconfiguring static.

2. .claude/skills/pr-review/SKILL.md:466 — the anchored -R assumes test name == target name, which is true 568 times out of 569.

ctest --test-dir build -R '^(test_a|test_b)$' --output-on-failure

The recipe reuses one token for both --target and the anchored -R. Across tests/tests.cmake that holds for every registration but one: add_test(NAME settings_browser_dialog COMMAND settings_browser_dialog_test) — building settings_browser_dialog_test and filtering ^settings_browser_dialog_test$ matches nothing. Worth half a sentence that names usually equal targets and that ctest --test-dir build -N resolves it when -R comes back empty, since an empty filter looks like a pass.

3. BUILD-OPTIONS.md:152 and CMakeLists.txt:151 still scope AETHER_SHARED_CORE to "Sanitizer CI only." The option's help string reads "Build libaethercore as a shared library (sanitizer CI only)" and the options table says "Sanitizer CI only, never for releases." This PR makes it the standard local review configuration, which I think is fine — a review build is not a release, and the skill discloses the link-mode caveat honestly. But the option's own documentation now understates where it is used; a line in the BUILD-OPTIONS.md row mentioning local review builds would keep the two in sync. Non-blocking, and arguably a maintainer's call on whether to broaden the sanctioned use in writing.

4. .claude/skills/pr-review/SKILL.md:471-474 — a dangling "then" after the Windows aside. "Both are for Linux and macOS: on Windows, drop the shared core … and the GCC/Clang flags. The app then loads the core as a shared library, which is not how it ships." The antecedent is the shared-core configure two sentences back, but following the Windows sentence it can read as the Windows outcome — which is the opposite of true. Moving the Windows sentence after the link-mode caveat fixes it.

What I tried to break

  • "step 7" cross-reference in /pr-land:444 — held. The new block sits at pr-review/SKILL.md:457-489, inside ## 7. Code-quality audit (427) and before ## 8 (511). A stale section number was my first guess here and it is correct.
  • Invented or wrong compiler flags — held, and this is the strongest thing in the PR. The flag pair is verbatim what CI already uses: "-DCMAKE_C_FLAGS_RELWITHDEBINFO=-O2 -g1 -DNDEBUG" / CXX appear identically at ci.yml:270-271 and full-suite.yml:103-104. RelWithDebInfo is canon's own build type (AGENTS.md:189, BUILD-OPTIONS.md:20), so the added -DNDEBUG is not a new hazard — I went looking for tests whose assertions it would silently disable and the exposure is pre-existing to canon, not introduced here.
  • Multi-target --target A B C — held. That form needs CMake ≥ 3.15; CMakeLists.txt:1 pins cmake_minimum_required(VERSION 3.25), so the central command cannot fail on a floor install.
  • Windows claim and target name — held. CMakeLists.txt:153-155 is message(FATAL_ERROR "AETHER_SHARED_CORE is not supported on Windows"), so "it is a configure error" is exact; add_executable(AetherSDR exists at CMakeLists.txt:1636.
  • A stale sibling build recipe left behind — held. Grepping both skills for cmake --build, cmake -S, cmake -B and ctest returns only the lines this PR added or rewrote; no third place still tells the agent to build all.
  • Scale claims — the "several hundred test executables" figure checks out (569 add_test registrations over 535 unique executables, 651 add_executable calls in tests.cmake), and the body's own 397 MB → 18 MB measurement does support "roughly twentyfold." I could not re-run the measurements: I am headless with no build, so the GB figures, the 219 s cold build, and the ldd result are taken on the body's word and not reproduced.
  • CI — all required checks green on b2aafd9, including Static checks and Sanitizer option configures (static-checks.yml:144-153 configures AETHER_SHARED_CORE=ON and asserts a libaethercore.so rule, which is the probe most relevant to this recipe). Analyze (python) is cancelled, not failed. No socket test is added or removed by this PR.

Every finding above is reasoned from reading the checkout at /tmp/aetherclaude/pr-6215 and the CI configuration — none is a runtime observation.

Recommendation

Approve with nits. The recipe is sound, and the two claims I most expected to be wrong — the cross-reference and the flag set — both check out against CI's own configuration rather than against the author's assertion. Finding 1 is the one worth fixing before merge: it is a docs PR whose job is to be followed literally, and that sentence sends a reviewer into a full static reconfigure for a case that no longer exists in the tree. The replacement text is above; findings 2-4 can ride along or be skipped. Thanks for the measurement table — it made this reviewable.


🤖 aethersdr-agent · cost: $4.9302 · model: claude-opus-5

@ten9876 ten9876 self-assigned this Oct 5, 2026

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed b2aafd9 against its stated documentation intent; there is no closing issue. Documentation corrections need no RFC under GOVERNANCE.md. Recommendation: Approve with nits, subject to the usual CODEOWNERS/check requirements. No blockers.

Scope Change Claimed? Verdict
.claude/skills/pr-review/SKILL.md Lean shared-core configuration, named targets and matching base comparison Yes In scope
.claude/skills/pr-land/SKILL.md Uses that recipe for remediation verification Yes In scope

The two commits contain only these skill edits. No runtime, release, CI, test registration, autonomy, or CHANGELOG change is bundled.

Nits

  1. .claude/skills/pr-review/SKILL.md:475 — the WDSP caveat sends reviewers into an unnecessary static reconfigure. wdsp_allocation_scope_test already links aether_wdsp directly, and rtl_receive_pipeline_test links both aethercore and aether_wdsp. The failure described in CMake is specifically direct WDSP calls while linking only shared aethercore; existing direct-call tests already handle it. Replace the sentence with: “Direct WDSP calls cannot rely on shared aethercore exporting its private WDSP dependency. Existing direct-call tests link aether_wdsp explicitly; prefer the WdspChannel seam for a new engine test.” This confirms the earlier bot's substantive caveat finding.

  2. .claude/skills/pr-review/SKILL.md:466 and .claude/skills/pr-land/SKILL.md:446 — distinguish executable targets from registered CTest names and fail on an empty filter. settings_browser_dialog_test is the target, but settings_browser_dialog is its CTest name. I ran the recipe's anchored target-name filter: it printed “No tests were found!!!” and exited 0. Suggested commands:

cmake --build build --parallel --target settings_browser_dialog_test
ctest --test-dir build -R '^settings_browser_dialog$' --no-tests=error --output-on-failure

State that the filter uses add_test(NAME ...), which may differ from the build target, and add --no-tests=error to both recipes. This is a robustness nit rather than a workflow-policy violation: canon's mandatory flag rule applies to workflow invocations.

Verification / attacks survived

  • Configured the exact head with shared core and the stated -O2 -g1 -DNDEBUG C/C++ flags; built the three socket-free targets named in the body's measurement: radio_setup_label_theme_token_test, sim_backend_test, and frameless_resizer_test. All three passed with the desktop theme plugin disabled for this sandbox. ldd confirms all three load the same scratch libaethercore.so.
  • Checked the Windows configure refusal, CMake's multi-target support, actual core/WDSP link rules, the /pr-land step reference and the matching flag recipe in CI. No socket-owning test is introduced or modified. No app or live-radio session was needed for these documentation claims.
  • This is a focused build, not the full tree or the author's AetherSDR-plus-tests benchmark; I have not reproduced the 84 GB, twentyfold, cold-build timing, or ccache measurements. ASR and RADE were disabled in my local build; those options do not validate their own paths here.
  • Required CI build/macOS/Windows/static checks passed on this head. Their runs were created at 21:03 UTC; current main's tip 173c0d485 was committed at 21:22:46 UTC, so those greens cover an older integration. The CodeQL Python job was cancelled and its workflow reports failure; this is not an observed source defect in this docs-only diff.
  • An invocable automated code-review skill was unavailable; manual review and independent checks above were used. The relevant bot claims were checked against the actual head.

Posted as COMMENT because the authenticated reviewer is also this PR's author; GitHub does not permit self-approval.

@ten9876 ten9876 removed their assignment Oct 5, 2026

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed b2aafd9 as a pair with #6217 (head 7154f88). Both were merged onto current main 173c0d485, in both orders, and an affected subset was built. Recommendation: approve with nits. No blockers. I'm posting this as COMMENT because the reviewing account is the author.

Issue fit: there is no linked issue, and none is needed: GOVERNANCE.md exempts documentation corrections from the RFC process. The PR does what it says. Both skills now use a lean, shared-core build of named targets instead of all.

Scope

File Change Claimed? Verdict
.claude/skills/pr-review/SKILL.md "Build only what you run" block; merge-base rule builds the same targets Yes In scope
.claude/skills/pr-land/SKILL.md Step 5 uses the same lean recipe Yes In scope

Everything in the diff is explained by the body. There is no CMake, CI, test or CHANGELOG change.

Pair findings (new; neither earlier review could see these)

1. .claude/skills/pr-review/SKILL.md:479-481 (and pr-land/SKILL.md:447-448): once #6217 lands, the test-selection grep misses the logging consumers. It already misses the settings consumers.

The rule is "the ones whose tests/tests.cmake block compiles a file the PR touches (grep -n 'Touched.cpp' tests/tests.cmake)". These are non-comment hits for the source path, counted on each tree:

File main main + #6217
LogManager.cpp 63 1 (the add_library(aether_test_log_manager OBJECT …) line)
AsyncLogWriter.cpp 65 1
AppSettings.cpp 0 (behind ${AETHER_SETTINGS_SOURCES}) 0

Take a PR that touches LogManager.cpp. A reviewer who follows this text literally after #6217 builds none of its 63 consumers. Separately, build.ninja shows that 270 of 626 executables link aethercore, so no grep of tests.cmake sees any consumer of a file compiled into the core. That limit already exists without this PR, but the skill should say so. Suggested replacement for the sentence:

Pick the tests from the diff: the ones the PR adds or modifies, the ones its
body names, and the ones whose `tests/tests.cmake` block compiles a file the
PR touches (`grep -n 'Touched.cpp' tests/tests.cmake`). When the hit is an
`OBJECT` library or a `set(..._SOURCES` list rather than a test, grep again
for that target or variable name. A file compiled into `aethercore` reaches
every test that links it, so no grep finds them all; pick the ones that
exercise the touched code.

Severity is a nit on this PR alone, and the one to fix if #6217 lands.

2. Non-blocking: after #6217, the explicit -g1 flags in the recipe (SKILL.md:461-463) do nothing on GCC/Clang. I confirmed this with a fresh configure of the merged tree and no flags: the cache holds CMAKE_CXX_FLAGS_RELWITHDEBINFO=-O2 -g1 -DNDEBUG. They are harmless, and keeping them keeps the recipe correct on a checkout without #6217. Mentioned only so nobody reads them as load-bearing.

Nits from the earlier reviews, still open at this head (not re-argued)

  • SKILL.md:475-476: the WDSP sentence still prescribes a static reconfigure. Both direct-WDSP tests already link aether_wdsp; route new ones through WdspChannel.h.
  • SKILL.md:466, pr-land/SKILL.md:446: -R matches the add_test(NAME …) name, not the target name (settings_browser_dialog_test → settings_browser_dialog). Add --no-tests=error, because an empty filter exits 0.
  • SKILL.md:472-474: after the Windows sentence, "The app then loads the core as a shared library" reads as if it describes Windows.
  • CMakeLists.txt:151 and BUILD-OPTIONS.md:172 still describe AETHER_SHARED_CORE as "sanitizer CI only". This PR makes it the standard review configuration.

What I tried to break

  • Pair merge: main + #6217 + #6215 and main + #6215 + #6217 both merge cleanly and give an identical tree. The two pr-review/SKILL.md hunks (the step 5 CMake-contract line and the step 7 block) do not overlap, so landing order doesn't matter.
  • The recipe on the merged tree: I configured exactly as written (shared core, -g1, plus local mold, ccache and Qt 6.12; ASR and RADE off). I then built split_qsy_settings_test log_manager_filter_rules_test async_log_writer_test theme_manager_test transmit_model_test sim_backend_test. That took 42 s, and the build dir is 428 MB. ctest -R '^(…)$' --no-tests=error: 6/6 passed. ldd build/sim_backend_test resolves libaethercore.so from the build tree. The largest of these test binaries is 10 MB.
  • A shared core next to #6217's object libraries: from build.ninja, 77 executables link the new object libraries, and 0 of them also link libaethercore/libaetherdesktop_support. So 6215's recipe does not create the second-AppSettings-singleton case that #6217's comment warns about.
  • Not reproduced: the 84 GB full-tree figure and the 219 s / 41 s timings. I did not build all.
  • CI: the earliest run on this head was created at 21:02:58Z, before main's tip 173c0d485 (21:22:46Z). The green therefore describes an older merge. My clean merge onto current main stands in for it on this docs-only diff. Analyze (python) is cancelled, not failed.
  • Bridge: not applicable. There is nothing to drive in a skills-text change.

ten9876 and others added 2 commits October 5, 2026 15:27
- Test selection follows an OBJECT library or a _SOURCES list to its
  consumers. After #6217 the logging sources appear once in tests.cmake,
  and the settings sources were already behind a variable.
- Both ctest recipes add --no-tests=error and note that -R matches the
  add_test name, not the target name.
- The WDSP caveat matches CMakeLists.txt: link aether_wdsp or go through
  WdspChannel.h instead of reconfiguring static.
- The link-mode caveat now comes before the Windows sentence.
- -g1 is described in BUILD-OPTIONS.md's words and stays explicit, so the
  recipe does not depend on the build-type default.
- AETHER_SHARED_CORE's help string, comment and BUILD-OPTIONS.md row now
  cover local review builds, not only the sanitizer lane.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ciple VIII.

One binary can be registered under several names: deepfist_cw_model_test
five times on main, and #6208 adds rtl_capture_worker_slow_device_test on
rtl_capture_worker_test's binary. A filter built from the target name runs
only one of them, and --no-tests=error cannot notice, because something
matched. The selection step now takes the names from
`grep -n 'COMMAND <target>' tests/tests.cmake`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ten9876
ten9876 force-pushed the docs/skills-targeted-builds branch from b5d3c92 to 1fe7804 Compare October 5, 2026 22:45
ten9876 added a commit that referenced this pull request Oct 5, 2026
…o -g1 (#6214) (#6217)

Part of #6214 (settings and logging; the widgets half is left open, see
the end).

## Root cause

The light tests deliberately avoid linking `aethercore`, so they compile
the app sources they need themselves. Three of those sources were listed
in dozens of test targets:

| Source | Test targets compiling it |
|---|---:|
| `${AETHER_SETTINGS_SOURCES}` (5 files: `AppSettings`,
`SettingsDatabase`, `SettingsPaths`, `SettingsBootstrap`,
`SettingsSanitizer`) | 75 |
| `src/core/AsyncLogWriter.cpp` | 65 |
| `src/core/LogManager.cpp` | 63 |

CMake compiles a listed source once per target, and each test's own
flags and include dirs differ, so ccache could not merge the copies.
That came to about 500 compiles of the same seven files on every
full-tree build.

## Fix

- **Compile once:** each source is now an OBJECT library compiled once:
`aether_test_settings`, `aether_test_log_manager` and
`aether_test_async_log_writer` (`tests/tests.cmake`, "Shared objects" at
the top).
- **One-for-one swap:** every use became a matching
`$<TARGET_OBJECTS:...>` line, so each test links exactly the objects it
linked before. Nothing else in the 77 blocks changed.
- **OBJECT, not STATIC:** this keeps the same semantics as listing the
source. Every object is linked, so nothing that matters only through a
static initializer can be dropped. It's the same reasoning as the
existing `aether_test_wisdom_isolation`.
- **Light tests only:** a test linking
`aethercore`/`aetherdesktop_support` must not take these. Next to
`libaethercore.so` (`AETHER_SHARED_CORE`) that would be a second
`AppSettings` singleton. None of the 77 does; I checked every
`target_link_libraries` in the file.
- **Guard:** a configure check fails if a test compiles one of these
from source again. Otherwise a block copied from an older test would
quietly bring the recompiles back.
- **Docs:** `tests/README.md` and the CMake-contract line in
`.claude/skills/pr-review/SKILL.md` describe the new form. That skill
file is also touched by #6215, in a different hunk; whichever lands
second may need a trivial rebase.

## Evidence

Linux, GCC, Qt 6.12.0, static core. `main` at `03dcff47c` and this
branch were configured identically in sibling worktrees. Compile-job
counts come from `ninja -t commands`, and timings are clean builds with
`CCACHE_DISABLE=1`.

Primary measurement, at CI's `build` flags: `RelWithDebInfo` with `-O2
-g1 -DNDEBUG` (`ci.yml:263-271`):

| | `main` | this PR |
|---|---:|---:|
| Compile jobs, full tree | 3806 | **3322** (−484) |
| Compile jobs, the 77 affected targets | 1015 | **524** (−48%) |
| Clean uncached build of those 77 targets | 151 s | **89 s** (−41%) |
| Build dir after that build | 0.86 GB | 0.72 GB |

At plain `RelWithDebInfo` (full `-g`) the job counts are the same; the
build went from 192 s to 116 s (−40%), and the build dir from 3.05 to
2.34 GB.

- **Tests:** all 70 tests registered from the 77 affected targets pass
on both `main` and this branch at full `-g`, and on this branch again at
`-g1` (`ctest -R … --no-tests=error`). The other 7 targets are tools
with no registered test.
- **Guard mutation:** I swapped one
`$<TARGET_OBJECTS:aether_test_log_manager>` back to
`src/core/LogManager.cpp`. Configure failed at `tests/tests.cmake:106`
with the guard's message. After restoring the file (sha256 identical),
configure passed.
- **Checks:** `check_test_registration.py --strict`,
`check_ci_test_gate.py` and `check_build_options.py` pass.
- **Sanitizer configure:** configure passes with
`-DAETHERSDR_SANITIZER=address,undefined -DAETHER_SHARED_CORE=ON`.

**Not verified locally; CI covers it:**
- The Windows and macOS compile of the OBJECT form. AUTOMOC on
`LogManager.h` now runs once in the object library instead of per test.
- The sanitizer lane *running* these targets. I only configured that
lane.

The agent automation bridge doesn't apply here: this is a build change
with no runtime behaviour to drive.

## Also: RelWithDebInfo defaults to `-g1` on GCC/Clang
(`CMakeLists.txt`, after the build-type default)

`RelWithDebInfo` is the default build type, and CMake's `-g` gives every
binary full DWARF. Measured on `main`:
- the `AetherSDR` binary is 832 MB, about 680 MB of it debug info;
- 232 test binaries are over 100 MB each;
- the clean build of the 77 affected targets is 3.05 GB at `-g` and 0.86
GB at `-g1`.

`-g1` keeps line tables and function names, so backtraces and core dumps
still symbolize; locals and types need a `Debug` build. CI's Linux
`build` leg and `full-suite.yml` already pass `-g1` explicitly. I left
those lines alone; they now restate the default.

Scope of the change:
- **What it replaces:** only CMake's untouched default (`-O2 -g
-DNDEBUG`) for GCC/Clang (AppleClang included), and only when
`CFLAGS`/`CXXFLAGS` set no debug level. The TSan lane injects `-g3`
through `CXXFLAGS`, and `RelWithDebInfo`'s flags come after it on the
command line, so a blanket `-g1` would have overridden it.
- **What still wins:** an explicit
`-DCMAKE_<LANG>_FLAGS_RELWITHDEBINFO`.
- **Existing build trees:** a tree configured before this change cached
CMake's default `-O2 -g -DNDEBUG`. This change moves it to `-g1` on the
next reconfigure, so it rebuilds once after pulling, mostly missing
ccache. That is expected, not a regression.
- **Whitespace:** the debug-level check accepts a space or a tab before
`-g` (`(^|[ \t])-g`), so `CXXFLAGS="-Wall<TAB>-g3"` is left alone too.
- **Reconfiguring with a debug level:** the value is cached, so an
internal `_AETHER_<LANG>_RWD_G1` records that the project wrote it. If a
later reconfigure adds a `-g` to `CMAKE_<LANG>_FLAGS`, CMake's `-O2 -g
-DNDEBUG` comes back instead of the cached `-g1` overriding it; drop the
`-g` and `-g1` returns. A RelWithDebInfo value anyone else set is never
touched.
- **What it never touches:**
- MSVC flags: the Windows installer's `RelWithDebInfo` is `/Zi`, which
never matches.
  - `Release`, which the Linux AppImage and macOS DMG builds use.
- One local path does change: `packaging/macos/build-installer.sh` (the
manual `.pkg` script, which no workflow runs) configures
`RelWithDebInfo`, so it now builds at `-g1` under AppleClang.
- **`BUILD-OPTIONS.md`** gets a "Build type and debug info" section on
getting full debug info. Spell it `-g2`, because a value identical to
CMake's default can't be told apart from no choice.

Configure-only checks, each in a fresh build dir:

| Case | `CMAKE_CXX_FLAGS_RELWITHDEBINFO` |
|---|---|
| Fresh, no options | `-O2 -g1 -DNDEBUG`, and the compile line for
`AppSettings.cpp` carries `-O2 -g1` |
| No build type given | `-O2 -g1 -DNDEBUG` |
| Reconfigure of that tree | stays `-O2 -g1 -DNDEBUG` |
| Same tree reconfigured with `-DCMAKE_CXX_FLAGS=-g3` | `-O2 -g
-DNDEBUG` (CMake's default restored; compile line `-g3 -g`, as before
this PR) |
| …then with the flags cleared | `-O2 -g1 -DNDEBUG` again |
| Explicit `RelWithDebInfo` value (`-g1` or `-g2`), then `-g3` |
unchanged: the value is the user's |
| `CFLAGS=-g3 CXXFLAGS=-g3` (TSan shape) | `-O2 -g -DNDEBUG` (untouched)
|
| Explicit `-O2 -g2 -DNDEBUG` | `-O2 -g2 -DNDEBUG` (wins) |
| `CFLAGS`/`CXXFLAGS` = `-Wall<TAB>-g3` | `-O2 -g -DNDEBUG` (untouched)
|
| `main`-configured tree, reconfigured with this branch | `-O2 -g
-DNDEBUG` → `-O2 -g1 -DNDEBUG` |

`check_build_options.py --strict` and its self-test pass. FORCE cache
sets are outside its catalog, which is why the doc section is prose
rather than a table row.

## Not in this PR

#6214 also proposed a widgets library (`DragValuePopup` ×40,
`FramelessResizer` ×29, `FramelessWindowTitleBar` ×23,
`PersistentDialog` ×19). Those tests link differing mixes of
`aetherdesktop_support` and Qt modules, so they need the per-target
audit this PR did for settings and logging. I left them for a follow-up
rather than guessing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ten9876 ten9876 self-assigned this Oct 5, 2026
…rinciple VIII.

Every check in the test is an assert(), and the calls it exercises sit
inside them. RelWithDebInfo and Release define NDEBUG (CMake's default
RelWithDebInfo flags, CI's build and TSan lanes, and the lean review build),
so there the test compiled to an empty main and passed whatever the registry
did. #undef NDEBUG before <assert.h>, which re-reads it on each inclusion.

Built at -O2 -g1 -DNDEBUG, with the first assertion inverted on purpose:
  before: 100% tests passed (the inverted check never ran)
  after:  0% tests passed, 1 failed; unmodified: 100% passed

It is the only test that includes <assert.h> without handling NDEBUG.
Found by the automated code-review pass on #6215's recipe.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ten9876
ten9876 requested a review from a team as a code owner October 5, 2026 23:47
@ten9876
ten9876 merged commit b68df86 into main Oct 5, 2026
7 checks passed
@ten9876
ten9876 deleted the docs/skills-targeted-builds branch October 5, 2026 23:57
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