Skip to content

build: compile shared test sources once, and default RelWithDebInfo to -g1 (#6214) - #6217

Merged
ten9876 merged 4 commits into
mainfrom
fix/6214-test-support-libs
Oct 5, 2026
Merged

ten9876 merged 4 commits into
mainfrom
fix/6214-test-support-libs

Conversation

@ten9876

@ten9876 ten9876 commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

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 docs(skills): lean, targeted builds in /pr-review and /pr-land #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

@ten9876
ten9876 marked this pull request as ready for review October 5, 2026 21:24
@ten9876
ten9876 requested review from a team as code owners October 5, 2026 21:24
ten9876 and others added 2 commits October 5, 2026 14:49
…ests. Principle VIII.

75 test targets compiled ${AETHER_SETTINGS_SOURCES} (five files) themselves,
63 compiled LogManager.cpp and 65 AsyncLogWriter.cpp: one compile per test
per file, each with that test's flags so ccache could not merge them. These
are the light tests that deliberately avoid linking aethercore.

Each is now an OBJECT library compiled once (aether_test_settings,
aether_test_log_manager, aether_test_async_log_writer), and every use is a
one-for-one $<TARGET_OBJECTS:...> line, so each test links the same objects
as before. OBJECT, not STATIC, so nothing reached only through a static
initializer can be dropped. A configure guard fails if a test compiles one
of these from source again. tests/README.md and the pr-review CMake contract
describe the new form.

Same config both sides (RelWithDebInfo, static core):
  full-tree compile jobs           3806 -> 3322
  compile jobs, 77 affected targets 1015 -> 524
  uncached build of those targets   192 s -> 116 s
Affected tests: 70/70 pass on main and on this branch.

Part of #6214

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
RelWithDebInfo is the default build type, and CMake's -g gave every test
binary full DWARF: 100-400 MB each, and about 680 MB of the 832 MB app
binary. -g1 keeps line tables and function names, so backtraces and core
dumps still symbolize. CI's Linux build already passed -g1 explicitly.

Only CMake's untouched default (-O2 -g -DNDEBUG) on GCC/Clang is replaced,
and only when CFLAGS/CXXFLAGS carry no debug level of their own: the TSan
lane injects -g3 there, and RelWithDebInfo's flags come after it on the
command line. An explicit -DCMAKE_<LANG>_FLAGS_RELWITHDEBINFO wins; MSVC and
Release are untouched. BUILD-OPTIONS.md says how to get full debug info.

Configure-only checks: a fresh tree and no build type give -g1 (compile line
shows -O2 -g1), a reconfigure keeps it, CFLAGS/CXXFLAGS=-g3 leaves -g, and
an explicit -g2 wins.

Part of #6214

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ten9876
ten9876 force-pushed the fix/6214-test-support-libs branch from fbf662f to 7154f88 Compare October 5, 2026 21:51
@ten9876 ten9876 changed the title build(tests): compile the settings store and logging once for light tests (#6214) build: compile shared test sources once, and default RelWithDebInfo to -g1 (#6214) Oct 5, 2026
@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 the updated head 7154f88 (including the new debug-default commit), compared with merge base 173c0d4. Recommendation: Approve with one nit, pending required build/platform checks. No blockers.

Issue fit and scope

#6214 asks to reduce repeated compilation without pulling the engine into light tests, changing settings isolation, or changing test/CI membership. The settings/logging portion meets that request; the widgets portion remains expressly open. The added debug-default change addresses the same binary-size problem and is now disclosed in both title and body. These are build/performance changes with no application UX or new dependency; GOVERNANCE.md does not require an RFC for them.

Scope Change Claimed? Verdict
tests/tests.cmake Three OBJECT libraries, matching source substitutions and configure guard Yes In scope
tests/README.md, .claude/skills/pr-review/SKILL.md Document the new standalone settings/logging contract Yes In scope
CMakeLists.txt, BUILD-OPTIONS.md Default untouched GNU/Clang RelWithDebInfo flags to g1; document overrides Yes, updated body In scope

No application source, CI filter, test registration, release file, or CHANGELOG change is bundled. Both commits are verified signed. The debug-default tradeoff is explicit: locals/types require Debug or an explicit g2 override.

Nit — CMakeLists.txt:285: recognize whitespace before a debug flag.

The condition promises to retain an existing debug-level choice in CFLAGS/CXXFLAGS, but (^| )-g recognizes only a literal space. In a fresh actual configure with CFLAGS and CXXFLAGS containing -Wall<TAB>-g3, CMake retains those flags but this block changes RelWithDebInfo to -O2 -g1 -DNDEBUG. The later g1 therefore overrides the requested higher level. Ordinary space-separated flags work; this is an edge-case override nit, not a blocker. Drop-in replacement:

       AND NOT CMAKE_${_lang}_FLAGS MATCHES "(^|[ \t])-g")

I exercised that replacement in an isolated CMake script using the actual block; it preserves the default in the tab-separated case. It also preserves the existing behavior for default flags, explicit g2, and ordinary g3 input. The original untouched -g is itself later than global -g3; this nit only addresses the new block's stated preservation condition, not an assertion that this PR introduced that older ordering issue.

What I tried to break / evidence

  • Configured separate head and base trees with GCC/Qt 6.12, ASR/RADE disabled. Compile databases drop from 3603 to 3112 entries (491 fewer) in this configuration. All 77 old standalone consumers remain executable targets. Inspected their generated link edges: none gains aethercore or aetherdesktop_support. Tested the potential lost-definition trap: none of their old AppSettings compile commands had HAVE_KEYCHAIN, and the remaining target-specific definitions do not control these shared source implementations.
  • Compared configured CTest names: 644 on each side, identical lists. The selected settings, async writer, logging filter and widget targets built on both sides. On each side the same 32 CTest entries produce 31 passes and one platform skip (explicit-profile-path-isolation). The initial widget startup failure was identical on base and head: the inherited GNOME/GTK plugin attempted desktop access in the sandbox. With QT_QPA_PLATFORMTHEME=generic QT_STYLE_OVERRIDE=Fusion, the widget test passes on both sides. No source modification was needed.
  • Mutation-tested the new guard by replacing one logging object reference with src/core/LogManager.cpp: configure failed at tests/tests.cmake:106 with the intended message. Restored exact bytes; configure succeeds and the scratch source diff is clean.
  • Fresh real configurations verify ordinary CFLAGS/CXXFLAGS g3 leaves RelWithDebInfo unchanged and explicit g2 wins; fresh default/reconfigure use g1. readelf --debug-dump=decodedline finds source line tables in the resulting AppSettings object. Registration, frozen CI gate, build-options strict checks and the build-options self-test pass.
  • The latest-head static checks and sanitizer configure probe pass. Linux CI build passes; macOS and Windows checks are still pending. Latest runs were created at 21:51:27 UTC, after main 173c0d485 at 21:22:46 UTC, so they are not stale by that comparison. Earlier greens belonged to the superseded head and are not approval evidence for the debug-default commit.

Test boundary and honest limits

Before building, I inspected the changed targets' test sources. Four existing default-suite synthetic-peer tests are affected by source substitution: vkamp_connection_test binds TCP and UDP; pgxl_direct_protocol_test, pgxl_panel_test, and tgxl_direct_protocol_test listen on loopback TCP. I notified the operator and did not build or run those targets, as the review skill requires PR-specific direction. This PR adds no new socket behavior or peer; those existing boundary debts remain. They run in the default main/full-suite and weekly sanitizer graphs, not the frozen per-PR test gate.

I did not build the whole tree, reproduce the author's uncached wall-clock/size benchmarks, run all affected tests, execute sanitizer binaries, or locally verify Windows/macOS. The graph/target audit and focused behavioral tests support the shared-object change; they do not replace those omitted checks. No application GUI/bridge or radio session is needed for this build change. An invocable automated code-review skill was unavailable; this was a manual adversarial review.

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

@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 7154f88 as a pair with #6215 (head b2aafd9), against merge base 173c0d485. Recommendation: approve with nits. No blockers. I'm posting this as COMMENT because the reviewing account is the author.

Issue fit: #6214 asks to stop recompiling the most-repeated test sources. This PR does that for the settings store, LogManager and AsyncLogWriter, and leaves the widgets half explicitly open, so it delivers part of the issue as stated. The -g1 default is a separate change. #6214 does not ask for it, but the title and body disclose it, and it is aimed at the same binary-size problem. Whether it belongs in this PR is a maintainer call, and the maintainer is the author.

Scope

File Change Claimed? Verdict
tests/tests.cmake 3 OBJECT libraries, 1:1 $<TARGET_OBJECTS> swaps in 77 blocks, configure guard Yes In scope (#6214)
tests/README.md, .claude/skills/pr-review/SKILL.md:324-326 Describe the new contract Yes In scope
CMakeLists.txt:274-289, BUILD-OPTIONS.md RelWithDebInfo defaults to -g1 on GCC/Clang Yes (title and body) Separate change, disclosed; maintainer call

Findings (all non-blocking)

1. Pair: this PR makes #6215's test-selection grep miss every logging consumer. On main, grep 'LogManager.cpp' tests/tests.cmake hits 63 test blocks and AsyncLogWriter.cpp hits 65. With this PR, each grep returns only the one add_library(... OBJECT ...) line. #6215 tells reviewers to choose tests with exactly that grep. The fix belongs in #6215's text; the drop-in is in my review there. I'm noting it here so whichever PR lands second carries it.

2. CMakeLists.txt:274-289: every existing RelWithDebInfo tree recompiles once after this merges. The body describes this case only as "Reconfigure of that tree stays -g1". I reproduced the main-to-PR case: I configured a fresh tree with main's CMakeLists.txt, which cached -O2 -g -DNDEBUG, then reconfigured the same tree with this PR. The cache became -O2 -g1 -DNDEBUG. Every object is rebuilt and ccache misses on all of them. That is intended and correct, but worth one line in the body so nobody mistakes the long build after pulling main for a regression.

3. packaging/macos/build-installer.sh:15 is a third RelWithDebInfo path, and it now gets -g1. The body says the change never touches "Release, which the Linux AppImage and macOS DMG builds use". That is true; macos-dmg.yml uses Release. The local .pkg script, however, configures RelWithDebInfo under AppleClang and now builds at -g1. codeql.yml:32 says no workflow runs it, so this only needs a disclosure line.

4. Still open from the earlier review: CMakeLists.txt:285, (^| )-g misses a tab-separated -g3. Use (^|[ \t])-g.

5. Optional: the guard catches source re-listing, but nothing catches a test that links both these object libraries and aethercore. That combination is the second-singleton case the comment at tests/tests.cmake:76-80 warns about. Today the count is 0 (verified below). Whether it is worth a get_target_property(LINK_LIBRARIES) sweep in the deferred-retrofit section is your call.

What I tried to break

  • -g1 default, configure-only checks run myself in fresh trees:
    • No flags gives -O2 -g1 -DNDEBUG.
    • CFLAGS=-g3 CXXFLAGS=-g3 gives -O2 -g -DNDEBUG (untouched).
    • A main-configured tree reconfigured with this PR gives -g1 (finding 2).
  • Guard mutation, run myself: I swapped one $<TARGET_OBJECTS:aether_test_log_manager> back to src/core/LogManager.cpp. Configure failed with CMake Error at tests/tests.cmake:106 and the guard's message. After git checkout -- tests/tests.cmake, the tree was clean and configure passed again.
  • Link edges, from the merged tree's build.ninja: 77 executables take the object libraries, and 0 of them also link libaethercore/libaetherdesktop_support. No aether_test_log_manager consumer is missing aether_test_settings.
  • Behaviour, with #6215's lean recipe on main + both PRs (AETHER_SHARED_CORE=ON): I built split_qsy_settings_test (settings objects plus target-specific definitions), log_manager_filter_rules_test, async_log_writer_test, theme_manager_test, transmit_model_test and sim_backend_test. 6/6 passed with --no-tests=error.
  • Merge: the pair merges cleanly in either order onto current main and gives an identical tree. The pr-review/SKILL.md hunks don't overlap.
  • Test boundary: the TARGET_OBJECTS swaps also land in four existing socket-owning tests: vkamp_connection_test, pgxl_direct_protocol_test, pgxl_panel_test and tgxl_direct_protocol_test. This PR adds no socket behaviour. I did not build or run those four, and I flagged them to the operator.
  • Not reproduced: the author's 151 s → 89 s and 3806 → 3322 job figures, the Windows/macOS compile (CI on this head is green, created after main's tip, so not stale), and sanitizer execution.
  • Bridge: not applicable. This is a build change with no runtime surface.

ten9876 added a commit that referenced this pull request Oct 5, 2026
- 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>
The -g1 default skips CFLAGS/CXXFLAGS that already set a debug level, but
it only recognised a space before -g. With "-Wall<TAB>-g3" it lowered
RelWithDebInfo to -g1, and that came after -g3 on the command line.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ten9876 ten9876 self-assigned this Oct 5, 2026

@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

Partially — and deliberately so, which the body states up front. #6214 asked for the repeated app sources in tests/tests.cmake to be factored into shared libraries, listing eight sources. This PR does the settings store, LogManager and AsyncLogWriter, and explicitly leaves the widgets half (DragValuePopup ×40, FramelessResizer ×29, FramelessWindowTitleBar ×23, PersistentDialog ×19) open because those targets link differing mixes of aetherdesktop_support. That is a reasonable split and it is disclosed.

The constraints #6214 asked to keep all hold, and I checked each of them against the head checkout rather than taking the body's word:

  • "Light tests stay light. Neither library pulls in aethercore." — aether_test_settings links Qt6::Core + aether_sqlite3, the two log libraries Qt6::Core only (tests/tests.cmake:84-94). Confirmed.
  • "The AETHER_SETTINGS_SOURCES / AETHER_SETTINGS_CONSUMERS contract unchanged." — I parsed every add_executable block and diffed per-target source counts against main at 173c0d48. Every one of the 78 affected targets links exactly the same object set it did before; zero targets changed count. The settings-objects users and the AETHER_SETTINGS_CONSUMERS membership are an exact two-way match — no user missing from the list, no new gap.
  • "No change to which tests exist, or to ci.yml filters." — confirmed, no test added, removed or renamed; no workflow touched.

The -g1 half is a second, independent change — see the scope table.

Scope

File / group What it changes Claimed by title/body? Verdict
tests/tests.cmake three OBJECT libraries + 77 one-for-one $<TARGET_OBJECTS:> swaps + a configure guard yes, and it is #6214's actual ask In scope
tests/README.md documents the new form, incl. the two log libraries yes In scope
.claude/skills/pr-review/SKILL.md rewrites the CMake-contract line to the new rule yes, with the #6215 rebase conflict disclosed In scope — the contract doc has to move with the contract
CMakeLists.txt:274-289 RelWithDebInfo now defaults to -g1 for every GCC/Clang build yes — title, a body section, three config-test tables Needs maintainer decision (below)
BUILD-OPTIONS.md new "Build type and debug info" section yes In scope, for the -g1 change

No CHANGELOG.md entry. No new public surface, protocol verb, settings key or capability field. No removed guard: the - lines are all source-list entries and two stale comment sentences, and every explanatory comment that named a link-time reason (qrz_callsign_test's "LogManager provides lcQrz…", cwx_speed_modifier_test's #3949 note, mini_pan_widget_test's seed-TU warning) survives intact next to the new lines. No socket test added, modified or removed, and no Fake* peer — I read the added/changed tests.cmake blocks and there are no new test sources in this PR at all.

On the -g1 bundling. #6214's own Context paragraph scopes this out: it records that AETHER_SHARED_CORE=ON with -g1 is already in use by the review skills, and then says "This issue covers the remaining part, the per-target recompiles, which only a CMake change fixes." So no issue asks for the default debug level to change, and this one changes what every contributor and every agent gets from the recipe printed in README.md:214, AGENTS.md:189, docs/BUILDING.md:93/115/158 and docs/first-contribution-cheatsheet.md:302. It is fully disclosed, it is well evidenced, and it lives in two cleanly separable commits (7154f88, 2cf4532) — so this is not "unrelated → unbundle" and not a nit either. It is a maintainer call on whether a build-default change for the whole contributor base lands inside a test-refactor PR. The split would be a trivial cherry-pick either way.

Blockers

None. Nothing here breaks users, violates canon, or will fail on current main.

Nits

All non-blocking.

  • tests/tests.cmake:72 — the settings count is off by one, and the body's "77 affected targets" with it. The comment reads (75 tests compile the settings store, 63 LogManager, 65 AsyncLogWriter). The file text gives 76 targets taking aether_test_settings (79 textual hits minus three in comments and the FATAL_ERROR string) and 78 taking at least one of the three (76 settings users, plus async_log_writer_test and issue_report_test, which take only aether_test_async_log_writer). 63/65 tie out exactly. The likely cause is that copy_assist_settings_dialog_test sits inside if (ENABLE_ASR), so a configured-target count on a tree without ASR sees 75/77 while a text count sees 76/78. Harmless, but the two methods are mixed in one parenthesis.

  • tests/tests.cmake:95-118 — the guard cannot distinguish "someone brought the recompiles back" from "someone legitimately needs a private compile", and offers no escape hatch. The message is good, but it is a hard FATAL_ERROR on NOT _aether_shared_source_count EQUAL 3 with no documented way out. If a future test genuinely needs AppSettings compiled with its own definitions (control_credential_vault_test and icom_credentials_singleflight_test already do that shape of thing with HAVE_KEYCHAIN), the only path is to edit the guard, and nothing tells the author that is allowed or how to justify it. This file already has the convention for exactly this — the # not registered: <reason> marker. One sentence in the FATAL_ERROR body ("if a test truly needs its own compile, raise the expected count and say why on the line") would match it.

  • tests/tests.cmake:100-103 — the guard works only because its own pattern string is double-escaped, and nothing says so. The regex literal in the file is "\\$\\{AETHER_SETTINGS_SOURCES\\}|src/core/LogManager\\.cpp|...", so the raw text it scans contains LogManager\\.cpp, not LogManager.cpp, and the pattern does not match itself. Rewrite it with single backslashes — which CMake also accepts as \$, \{ escapes — and the count becomes 4 and configure fails with a message blaming a test that is fine. It fails loud rather than silent, so this is cosmetic, but the hazard is invisible and worth a half-line comment. (Incidentally, green configure in CI proves all three alternatives match exactly once each, including the ${AETHER_SETTINGS_SOURCES} one your mutation test did not cover — if that alternative had silently never fired the count would be 2 and every configure would abort. Nice property, worth noting in the comment.)

  • tests/tests.cmake:76-83 — the invariant that actually matters is the one left in prose. The comment correctly identifies the real hazard: "a second copy of AppSettings next to libaethercore.so (AETHER_SHARED_CORE) would be a second singleton." That is a worse failure than the recompiles the guard does catch, and it is enforced only by the audit you did by hand. I re-ran it independently — I parsed every target_link_libraries for all 78 users and zero of them link aethercore or aetherdesktop_support, so the tree is clean today — but nothing keeps it clean. A configure-time loop is mechanical here, since both facts are target properties:

    # Nothing may take these objects AND link a library that already contains
    # them: under AETHER_SHARED_CORE that is a second AppSettings singleton.
    foreach(_obj_lib aether_test_settings aether_test_log_manager
                     aether_test_async_log_writer)
        foreach(_tgt ${AETHER_SETTINGS_CONSUMERS})
            if(TARGET ${_tgt})
                get_target_property(_srcs ${_tgt} SOURCES)
                get_target_property(_libs ${_tgt} LINK_LIBRARIES)
                if(_srcs MATCHES "TARGET_OBJECTS:${_obj_lib}"
                   AND _libs MATCHES "aethercore|aetherdesktop_support")
                    message(FATAL_ERROR
                        "${_tgt} takes $<TARGET_OBJECTS:${_obj_lib}> and links "
                        "aethercore/aetherdesktop_support, which already contain "
                        "it. Drop the objects and rely on the library.")
                endif()
            endif()
        endforeach()
    endforeach()

    Worth having or worth declining — AETHER_SETTINGS_CONSUMERS only covers the settings users, so the log libraries would need their own list to be fully covered, and that may not be worth the bookkeeping.

  • tests/tests.cmake:5587-5589 — the rewritten AETHER_SETTINGS_CONSUMERS header is narrower than the list. It now reads "Every standalone test/tool target that takes $<TARGET_OBJECTS:aether_test_settings> (rather than linking aethercore) needs the vendored SQLite engine." The list has 157 entries; only 76 take the objects. The other ~81 (reroute_dead_controls_test, anan_backend_test, tx_operation_integration_test, …) link aethercore and are in the list anyway. The old wording was equally narrow, so this is pre-existing imprecision the rewrite inherits rather than introduces — but since you were in the sentence, "or links a library that contains it" would make it true. Same sentence in .claude/skills/pr-review/SKILL.md:324-326.

  • CMakeLists.txt:274-289 / BUILD-OPTIONS.md:37 — the -g1 default is documented only where a contributor will not look. Every build recipe in the repo (README.md:214, AGENTS.md:189, docs/BUILDING.md:93, docs/first-contribution-cheatsheet.md:302) prints -DCMAKE_BUILD_TYPE=RelWithDebInfo with no hint that locals and types are now gone from the debugger. A one-line pointer to the new BUILD-OPTIONS.md section from docs/BUILDING.md would save someone an afternoon wondering why p m_settings prints nothing.

  • CMakeLists.txt:282 — ObjC/ObjCXX are not in the foreach. The loop covers C CXX. project() declares LANGUAGES C CXX and I found no enable_language(OBJCXX), so there is probably nothing to do; but if a .mm source ever gets its own language enabled, CMAKE_OBJCXX_FLAGS_RELWITHDEBINFO keeps full -g and the macOS half of the win quietly stops applying. Not worth changing now — worth a word in the comment so the next person knows it was considered.

What I tried to break

Everything below is reasoned from the head checkout at /tmp/aetherclaude/pr-6217 plus CI metadata. I did not build or run anything.

  • "One-for-one swap: nothing else in the 77 blocks changed." Tested, held. I parsed every add_executable block in both main (173c0d48) and the head, computed per-target counts of ${AETHER_SETTINGS_SOURCES}+TARGET_OBJECTS:aether_test_settings and the same for both log sources, and diffed: zero targets differ. Totals are identical on both sides (76 / 63 / 65). Raw source references remaining in the file: exactly one each, the three add_library lines.
  • The attack I expected to land: per-target compile definitions and include dirs the object libraries no longer see. An OBJECT library compiles with its own flags, not the consumer's, so any test that was relying on a private macro reaching AppSettings.cpp would silently change behavior. It does not happen here. Of the 78 users, five have target_compile_definitions — AETHER_SOURCE_DIR (×4) and HAVE_SERIALPORT (dstar_model_test) — and none of the seven shared sources reads either; their only non-platform conditional is #ifdef HAVE_KEYCHAIN (AppSettings.cpp:26,542), which is set only as target_compile_definitions(aethercore PUBLIC HAVE_KEYCHAIN) (CMakeLists.txt:2464) and no user links aethercore, so it was undefined before and is undefined now. Include dirs collapse to four sets (src ×55, src tests ×15, src + r8brain ×7, src src/core ×1) and all four contain src; no user uses BEFORE or tests/fakes, so there is no header-shim ordering to lose. Every Qt include across the seven sources is QtCore-only, matching the Qt6::Core-only link.
  • -g1 eating the TSan lane's -g3. Checked and it holds, for the reason the body gives. sanitizers.yml:77 injects -fsanitize=thread -g3 … -O1 through both CFLAGS and CXXFLAGS (sanitizers.yml:117-127) on a RelWithDebInfo build, and CMAKE_<LANG>_FLAGS precedes CMAKE_<LANG>_FLAGS_<CONFIG> on the command line — so an unguarded -g1 would indeed have won. The NOT CMAKE_${_lang}_FLAGS MATCHES "(^|[ \t])-g" arm catches " -g3" for both languages. (^ inside the alternation is a real anchor here — CMake's regex engine treats ^ as magic after ( and | — and \t is a genuine tab escape in a CMake quoted argument.) The ASan lane is build_type: Debug, untouched.
  • The sanitizer path the body does not mention: -DAETHERSDR_SANITIZER=. That one takes add_compile_options(-fsanitize=… -g3) at CMakeLists.txt:141, which bypasses the CMAKE_<LANG>_FLAGS guard entirely. It still comes out right: directory/target COMPILE_OPTIONS are appended after CMAKE_<LANG>_FLAGS_<CONFIG>, so -g3 wins over -g1 exactly as it won over -g. No finding, but it is a second path worth a clause in the comment.
  • The other RelWithDebInfo consumers. ci.yml:269-271 and full-suite.yml:102-104 already pass -O2 -g1 -DNDEBUG explicitly, so the STREQUAL "-O2 -g -DNDEBUG" test does not fire and nothing changes for them. windows-installer.yml:251 is MSVC (/Zi, never matches) and create-msix.ps1:512's -RequirePdb is unaffected. The AppImage and DMG legs are Release. codeql.yml:111 is Release. The only behavior change outside CI is the one the body discloses, packaging/macos/build-installer.sh:15, and I found no dsymutil, objcopy --only-keep-debug or .dSYM step anywhere in .github/workflows/, packaging/ or scripts/ that would be degraded by -g1.
  • tools/check_build_options.py skipping the new cache set. Verified rather than assumed: line 124 is elif "CACHE" in args and "FORCE" not in args, so a FORCE set is outside the catalog by construction and needs no BUILD-OPTIONS.md table row. The prose section is the right shape.
  • Stale references to the old contract. Grepped the whole tree (*.md, *.py, *.sh, *.yml, *.cmake, CMakeLists.txt) for AETHER_SETTINGS_SOURCES and for src/core/LogManager.cpp / src/core/AsyncLogWriter.cpp: the only remaining uses are the definition at CMakeLists.txt:830, the engine's own use at :921, and the three add_library lines. No doc left behind describing the retired form.
  • Ordering and CMake-version hazards. aether_sqlite3 is defined at CMakeLists.txt:1562/1569 and AETHER_SETTINGS_SOURCES at :830, both well before include(tests/tests.cmake) at :3343, so the top-of-file add_library + target_link_libraries resolve. cmake_minimum_required(VERSION 3.25) is far above the 3.12 floor for target_link_libraries on an OBJECT library. CMAKE_AUTOMOC ON is global (:14) and no user sets AUTOMOC OFF, so the moc TU moves into the object library cleanly and comes along in $<TARGET_OBJECTS:> — no target loses or doubles a moc.
  • What CI does and does not prove, against your "Not verified locally; CI covers it". All 7 checks are green on 2cf4532. The Linux build leg runs cmake --build build -j$(nproc) on the full tree (ci.yml:223), so all 78 affected targets really did compile and link under GCC — that is strong. But check-windows and check-macos build only --target AetherSDR aether-dv-waveform thumbdv_queue_test dstar_model_test digital_voice_waveform_process_test cross_needle_meter_test (ci.yml:713, :796), of which exactly two (dstar_model_test, digital_voice_waveform_process_test) take the shared objects. So MSVC and AppleClang have exercised 2 of 78 consumers of the OBJECT+AUTOMOC form, not all of them. Green here is narrower than it looks. check-windows also configures Release (ci.yml:603), so it exercises none of the -g1 change — consistent with "MSVC unaffected", but it means no CI leg currently builds the new default at all; every lane passes -g1 explicitly or uses another build type.
  • What I could not check. Whether the -g1 tree actually symbolizes backtraces and core dumps as claimed (no build, no debugger); the real wall-clock and disk numbers in the evidence tables; and whether the sanitizer lanes still run green, since only their configure is covered. The compile-job arithmetic is self-consistent with what I counted in the file, which is the part I can check.

Recommendation

Approve with nits on the tests.cmake half; needs a maintainer decision on the -g1 bundling. The refactor is the most carefully executed change of its kind I have reviewed on this repo — the swap is provably one-for-one across 78 targets, the hazard the OBJECT-vs-STATIC choice avoids is correctly identified, the no-aethercore audit reproduces exactly, and the guard against regression is self-verifying. The attack I expected to land (per-target definitions and include dirs that the shared objects no longer see) does not land, and I checked all five definition sites and all four include-dir sets to be sure.

The -g1 default is also correct and well evidenced, but it is a change #6214 explicitly scoped out, it alters what every contributor's documented build produces, and no issue asks for it. Concrete next step: a maintainer rules on whether to keep both halves here or cherry-pick 7154f88 + 2cf4532 into their own PR (trivial — the two commits touch only CMakeLists.txt and BUILD-OPTIONS.md). Either way the nits above are optional polish, not merge conditions. Thanks for the per-target audit and the guard-mutation test — both made this reviewable instead of a matter of trust.


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

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Antares Detector — candidate vulnerable file(s)

  • tools/check_build_options.py

The collect_documented function reads BUILD-OPTIONS.md and parses Option/Setting rows by splitting on '|' and '|||'. It does not validate or sanitize the input, which can lead to buffer overruns/use-after-free or unexpected parsing behavior when maliciously crafted markdown is present. This is a format-string/memory-safety vulnerability in the option-parsing code.

Localized by Cisco Foundation AI Antares-1B running locally in the AetherClaude sandbox, seeded by the Cartographer security map. Advisory only — please verify before acting.


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

…nciple VIII.

The -g1 default is a cached value. A build dir first configured plain kept
it after a later reconfigure with -DCMAKE_CXX_FLAGS=-g3, and the trailing
-g1 then overrode the -g3: less debug info than CMake's own -g gave.

_AETHER_<LANG>_RWD_G1 now records that this block wrote the value. When a
reconfigure brings a -g into CMAKE_<LANG>_FLAGS, the block restores
-O2 -g -DNDEBUG; when the -g goes again, -g1 returns. A RelWithDebInfo value
anyone else set is never touched.

Checked by reconfiguring one build dir in sequence, on the real project and
on the block lifted into a minimal C/C++ project:
  fresh                          -> -O2 -g1 -DNDEBUG (marker ON)
  reconfigure CMAKE_CXX_FLAGS=-g3 -> -O2 -g -DNDEBUG (was -g1 before)
  flags cleared again            -> -O2 -g1 -DNDEBUG
  explicit RWD -g1, then -g3     -> stays -g1 (theirs)
  user RWD -g2, then -g3/cleared -> stays -g2
  fresh CXXFLAGS=-g3             -> -O2 -g -DNDEBUG

Found by the automated code-review pass during /pr-land.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ten9876
ten9876 merged commit e1ef9b1 into main Oct 5, 2026
7 checks passed
@ten9876
ten9876 deleted the fix/6214-test-support-libs branch October 5, 2026 23:04
ten9876 added a commit that referenced this pull request Oct 5, 2026
`/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:

```sh
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; #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 #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

- **Per-test recompiles:** #6217 compiles the settings store and logging
sources once; the widget sources (`DragValuePopup.cpp` ×40 and others)
remain open in #6214.
- **No build-logic change.** `CMakeLists.txt` and `BUILD-OPTIONS.md`
change only `AETHER_SHARED_CORE`'s help string, comment and table row:
it now names local review builds as well as the sanitizer lane.
`tests/tests.cmake`, CI and release builds are untouched, and shipping
builds keep the static core.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
on8st added a commit to on8st/AetherSDR that referenced this pull request Oct 11, 2026
Conflicted file: BUILD-OPTIONS.md (hand-written; tools/check_build_options.py
checks it, nothing generates it). One hunk, two adjacent table rows:

- ENABLE_RTL: main's row, which gained "unless REQUIRE_RTL=ON" with
  aethersdr#6083 (46943eb). This branch never changed that row.
- AETHER_HL2_TX_TXA: this branch's row (SSB, AM, DSB and FM; OFF is
  SSB only). Main never changed that row; it only sat next to the other.

Everything else main added to the file (REQUIRE_RTL, the sanitizer and
RelWithDebInfo rows of aethersdr#6208, aethersdr#6214/aethersdr#6217) merged as text and is kept.
check_build_options.py --strict passes.

No other file conflicted. Hl2TxDsp.cpp/.h merged as text over aethersdr#6245:
the level chain is main's Hl2TxAlc (gain ramped inside the block), and
AM, DSB and FM audio now passes through it like SSB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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