Repository navigation
build: compile shared test sources once, and default RelWithDebInfo to -g1 (#6214) - #6217
Conversation
…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>
fbf662f to
7154f88
Compare
ten9876
left a comment
There was a problem hiding this comment.
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
aethercoreoraetherdesktop_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. WithQT_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 attests/tests.cmake:106with 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=decodedlinefinds 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
173c0d485at 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
left a comment
There was a problem hiding this comment.
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
-g1default, configure-only checks run myself in fresh trees:- No flags gives
-O2 -g1 -DNDEBUG. CFLAGS=-g3 CXXFLAGS=-g3gives-O2 -g -DNDEBUG(untouched).- A main-configured tree reconfigured with this PR gives
-g1(finding 2).
- No flags gives
- Guard mutation, run myself: I swapped one
$<TARGET_OBJECTS:aether_test_log_manager>back tosrc/core/LogManager.cpp. Configure failed withCMake Error at tests/tests.cmake:106and the guard's message. Aftergit 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 linklibaethercore/libaetherdesktop_support. Noaether_test_log_managerconsumer is missingaether_test_settings. - Behaviour, with #6215's lean recipe on main + both PRs (
AETHER_SHARED_CORE=ON): I builtsplit_qsy_settings_test(settings objects plus target-specific definitions),log_manager_filter_rules_test,async_log_writer_test,theme_manager_test,transmit_model_testandsim_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.mdhunks don't overlap. - Test boundary: the
TARGET_OBJECTSswaps also land in four existing socket-owning tests:vkamp_connection_test,pgxl_direct_protocol_test,pgxl_panel_testandtgxl_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.
- 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>
There was a problem hiding this comment.
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_settingslinksQt6::Core+aether_sqlite3, the two log librariesQt6::Coreonly (tests/tests.cmake:84-94). Confirmed. - "The
AETHER_SETTINGS_SOURCES/AETHER_SETTINGS_CONSUMERScontract unchanged." — I parsed everyadd_executableblock and diffed per-target source counts againstmainat173c0d48. 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 theAETHER_SETTINGS_CONSUMERSmembership are an exact two-way match — no user missing from the list, no new gap. - "No change to which tests exist, or to
ci.ymlfilters." — 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 takingaether_test_settings(79 textual hits minus three in comments and theFATAL_ERRORstring) and 78 taking at least one of the three (76 settings users, plusasync_log_writer_testandissue_report_test, which take onlyaether_test_async_log_writer).63/65tie out exactly. The likely cause is thatcopy_assist_settings_dialog_testsits insideif (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 hardFATAL_ERRORonNOT _aether_shared_source_count EQUAL 3with no documented way out. If a future test genuinely needsAppSettingscompiled with its own definitions (control_credential_vault_testandicom_credentials_singleflight_testalready do that shape of thing withHAVE_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 theFATAL_ERRORbody ("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 containsLogManager\\.cpp, notLogManager.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 tolibaethercore.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 everytarget_link_librariesfor all 78 users and zero of them linkaethercoreoraetherdesktop_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_CONSUMERSonly 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 rewrittenAETHER_SETTINGS_CONSUMERSheader is narrower than the list. It now reads "Every standalone test/tool target that takes$<TARGET_OBJECTS:aether_test_settings>(rather than linkingaethercore) 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, …) linkaethercoreand 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-g1default 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=RelWithDebInfowith no hint that locals and types are now gone from the debugger. A one-line pointer to the newBUILD-OPTIONS.mdsection fromdocs/BUILDING.mdwould save someone an afternoon wondering whyp m_settingsprints nothing. -
CMakeLists.txt:282—ObjC/ObjCXXare not in theforeach. The loop coversC CXX.project()declaresLANGUAGES C CXXand I found noenable_language(OBJCXX), so there is probably nothing to do; but if a.mmsource ever gets its own language enabled,CMAKE_OBJCXX_FLAGS_RELWITHDEBINFOkeeps full-gand 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_executableblock in bothmain(173c0d48) and the head, computed per-target counts of${AETHER_SETTINGS_SOURCES}+TARGET_OBJECTS:aether_test_settingsand 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 threeadd_librarylines. - 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.cppwould silently change behavior. It does not happen here. Of the 78 users, five havetarget_compile_definitions—AETHER_SOURCE_DIR(×4) andHAVE_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 astarget_compile_definitions(aethercore PUBLIC HAVE_KEYCHAIN)(CMakeLists.txt:2464) and no user linksaethercore, 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 containsrc; no user usesBEFOREortests/fakes, so there is no header-shim ordering to lose. Every Qt include across the seven sources isQtCore-only, matching theQt6::Core-only link. -g1eating the TSan lane's-g3. Checked and it holds, for the reason the body gives.sanitizers.yml:77injects-fsanitize=thread -g3 … -O1through bothCFLAGSandCXXFLAGS(sanitizers.yml:117-127) on aRelWithDebInfobuild, andCMAKE_<LANG>_FLAGSprecedesCMAKE_<LANG>_FLAGS_<CONFIG>on the command line — so an unguarded-g1would indeed have won. TheNOT 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\tis a genuine tab escape in a CMake quoted argument.) The ASan lane isbuild_type: Debug, untouched.- The sanitizer path the body does not mention:
-DAETHERSDR_SANITIZER=. That one takesadd_compile_options(-fsanitize=… -g3)atCMakeLists.txt:141, which bypasses theCMAKE_<LANG>_FLAGSguard entirely. It still comes out right: directory/targetCOMPILE_OPTIONSare appended afterCMAKE_<LANG>_FLAGS_<CONFIG>, so-g3wins over-g1exactly as it won over-g. No finding, but it is a second path worth a clause in the comment. - The other
RelWithDebInfoconsumers.ci.yml:269-271andfull-suite.yml:102-104already pass-O2 -g1 -DNDEBUGexplicitly, so theSTREQUAL "-O2 -g -DNDEBUG"test does not fire and nothing changes for them.windows-installer.yml:251is MSVC (/Zi, never matches) andcreate-msix.ps1:512's-RequirePdbis unaffected. The AppImage and DMG legs areRelease.codeql.yml:111isRelease. The only behavior change outside CI is the one the body discloses,packaging/macos/build-installer.sh:15, and I found nodsymutil,objcopy --only-keep-debugor.dSYMstep anywhere in.github/workflows/,packaging/orscripts/that would be degraded by-g1. tools/check_build_options.pyskipping the new cache set. Verified rather than assumed: line 124 iselif "CACHE" in args and "FORCE" not in args, so aFORCEset is outside the catalog by construction and needs noBUILD-OPTIONS.mdtable 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) forAETHER_SETTINGS_SOURCESand forsrc/core/LogManager.cpp/src/core/AsyncLogWriter.cpp: the only remaining uses are the definition atCMakeLists.txt:830, the engine's own use at:921, and the threeadd_librarylines. No doc left behind describing the retired form. - Ordering and CMake-version hazards.
aether_sqlite3is defined atCMakeLists.txt:1562/1569andAETHER_SETTINGS_SOURCESat:830, both well beforeinclude(tests/tests.cmake)at:3343, so the top-of-fileadd_library+target_link_librariesresolve.cmake_minimum_required(VERSION 3.25)is far above the 3.12 floor fortarget_link_librarieson an OBJECT library.CMAKE_AUTOMOC ONis global (:14) and no user setsAUTOMOC 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 Linuxbuildleg runscmake --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. Butcheck-windowsandcheck-macosbuild 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-windowsalso configuresRelease(ci.yml:603), so it exercises none of the-g1change — consistent with "MSVC unaffected", but it means no CI leg currently builds the new default at all; every lane passes-g1explicitly or uses another build type. - What I could not check. Whether the
-g1tree 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
|
Antares Detector — candidate vulnerable file(s)
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>
`/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>
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>
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:${AETHER_SETTINGS_SOURCES}(5 files:AppSettings,SettingsDatabase,SettingsPaths,SettingsBootstrap,SettingsSanitizer)src/core/AsyncLogWriter.cppsrc/core/LogManager.cppCMake 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
aether_test_settings,aether_test_log_managerandaether_test_async_log_writer(tests/tests.cmake, "Shared objects" at the top).$<TARGET_OBJECTS:...>line, so each test links exactly the objects it linked before. Nothing else in the 77 blocks changed.aether_test_wisdom_isolation.aethercore/aetherdesktop_supportmust not take these. Next tolibaethercore.so(AETHER_SHARED_CORE) that would be a secondAppSettingssingleton. None of the 77 does; I checked everytarget_link_librariesin the file.tests/README.mdand the CMake-contract line in.claude/skills/pr-review/SKILL.mddescribe 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.
mainat03dcff47cand this branch were configured identically in sibling worktrees. Compile-job counts come fromninja -t commands, and timings are clean builds withCCACHE_DISABLE=1.Primary measurement, at CI's
buildflags:RelWithDebInfowith-O2 -g1 -DNDEBUG(ci.yml:263-271):mainAt 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.mainand 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.$<TARGET_OBJECTS:aether_test_log_manager>back tosrc/core/LogManager.cpp. Configure failed attests/tests.cmake:106with the guard's message. After restoring the file (sha256 identical), configure passed.check_test_registration.py --strict,check_ci_test_gate.pyandcheck_build_options.pypass.-DAETHERSDR_SANITIZER=address,undefined -DAETHER_SHARED_CORE=ON.Not verified locally; CI covers it:
LogManager.hnow runs once in the object library instead of per test.The agent automation bridge doesn't apply here: this is a build change with no runtime behaviour to drive.
Also: RelWithDebInfo defaults to
-g1on GCC/Clang (CMakeLists.txt, after the build-type default)RelWithDebInfois the default build type, and CMake's-ggives every binary full DWARF. Measured onmain:AetherSDRbinary is 832 MB, about 680 MB of it debug info;-gand 0.86 GB at-g1.-g1keeps line tables and function names, so backtraces and core dumps still symbolize; locals and types need aDebugbuild. CI's Linuxbuildleg andfull-suite.ymlalready pass-g1explicitly. I left those lines alone; they now restate the default.Scope of the change:
-O2 -g -DNDEBUG) for GCC/Clang (AppleClang included), and only whenCFLAGS/CXXFLAGSset no debug level. The TSan lane injects-g3throughCXXFLAGS, andRelWithDebInfo's flags come after it on the command line, so a blanket-g1would have overridden it.-DCMAKE_<LANG>_FLAGS_RELWITHDEBINFO.-O2 -g -DNDEBUG. This change moves it to-g1on the next reconfigure, so it rebuilds once after pulling, mostly missing ccache. That is expected, not a regression.-g((^|[ \t])-g), soCXXFLAGS="-Wall<TAB>-g3"is left alone too._AETHER_<LANG>_RWD_G1records that the project wrote it. If a later reconfigure adds a-gtoCMAKE_<LANG>_FLAGS, CMake's-O2 -g -DNDEBUGcomes back instead of the cached-g1overriding it; drop the-gand-g1returns. A RelWithDebInfo value anyone else set is never touched.RelWithDebInfois/Zi, which never matches.Release, which the Linux AppImage and macOS DMG builds use.packaging/macos/build-installer.sh(the manual.pkgscript, which no workflow runs) configuresRelWithDebInfo, so it now builds at-g1under AppleClang.BUILD-OPTIONS.mdgets 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:
CMAKE_CXX_FLAGS_RELWITHDEBINFO-O2 -g1 -DNDEBUG, and the compile line forAppSettings.cppcarries-O2 -g1-O2 -g1 -DNDEBUG-O2 -g1 -DNDEBUG-DCMAKE_CXX_FLAGS=-g3-O2 -g -DNDEBUG(CMake's default restored; compile line-g3 -g, as before this PR)-O2 -g1 -DNDEBUGagainRelWithDebInfovalue (-g1or-g2), then-g3CFLAGS=-g3 CXXFLAGS=-g3(TSan shape)-O2 -g -DNDEBUG(untouched)-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 -DNDEBUGcheck_build_options.py --strictand 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 ofaetherdesktop_supportand 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