Skip to content

build: fix a target that does not compile, and advice that discards a build tree (fixes #604, fixes #592) - #606

Merged
Yaraslaut merged 2 commits into
masterfrom
fix/604-bank-qml-tests-include-592-fresh-advice
Sep 20, 2026
Merged

Yaraslaut merged 2 commits into
masterfrom
fix/604-bank-qml-tests-include-592-fresh-advice

Conversation

@Yaraslaut

@Yaraslaut Yaraslaut commented Sep 20, 2026

Copy link
Copy Markdown
Member

Two build-configuration tickets, neither touching library source, batched onto one branch so they share a CI cycle. Separate commits; they can be read independently.


#604bank_gui_qml_tests did not compile

Reproduced, on 26bfdb8f, with the linux-everything preset (the only configuration in the repository that sets MORPH_BUILD_BANK_GUI=ON; the option defaults to OFF, which is why an ordinary build never sees this):

FAILED: examples/bank/CMakeFiles/bank_gui_qml_tests.dir/__/common/testkit/testkit_main.cpp.o
.../examples/common/testkit/testkit_main.cpp:21:10: fatal error: 'testkit/log_level.hpp' file not found
   21 | #include <testkit/log_level.hpp>
      |          ^~~~~~~~~~~~~~~~~~~~~~~
1 error generated.

<testkit/log_level.hpp> exists at exactly one path in the tree — tests/testkit/log_level.hpp, so it resolves only against the repository's tests/ directory — and the target's two include directories (examples/bank/tests, examples/common) reach neither it nor anything that carries it.

The fix, and why this one

The sibling bank_gui_tests, one target above, links morph_test_main, which PUBLIC-links morph_test_log_level, whose INTERFACE include directory is the repository's tests/. bank_gui_qml_tests links neither. The divergence between the two siblings is what produced the defect, so the fix closes the divergence rather than papering over it with a bare target_include_directories(... ${PROJECT_SOURCE_DIR}/tests).

It cannot simply link morph_test_main the way its sibling does — that library's only symbol is a main, and this target has its own (testkit_main.cpp, which must own the QGuiApplication, because Qt Quick cannot instantiate an item without a platform integration and BankClient's QtExecutor captures QCoreApplication::instance() at construction). So it links the half of it that is wanted:

target_link_libraries(bank_gui_qml_tests PRIVATE
    bank_gui_lib Catch2::Catch2
    morph_test_log_level
    Qt6::Gui Qt6::Qml Qt6::Quick Qt6::QuickControls2)

That is precisely the split morph_qt_tests already uses (tests/qt/CMakeLists.txt:49-52), for precisely the same reason, and its comment there already says so. After this, the two bank GUI test targets get the same thing from the testkit and differ only where they must — in who owns main.

Evidence

Same tree, after the change:

[5/10] Building CXX object examples/bank/CMakeFiles/bank_gui_qml_tests.dir/__/common/testkit/testkit_main.cpp.o
[6/10] Building CXX object examples/bank/CMakeFiles/bank_gui_qml_tests.dir/tests/gui/test_bank_gui_qml_behaviour.cpp.o
[7/10] Linking CXX executable examples/bank/bank_gui_qml_tests

Registered with ctest and passing:

$ ctest -I 46,48 --output-on-failure
1/3 Test #46: Every bank controller exposes exactly the surface gui/qml binds, and nothing more ...   Passed    0.01 sec
2/3 Test #47: Main.qml confirms a posted transaction and a paid bill in the toast .................   Passed    0.61 sec
3/3 Test #48: MoveMoneyPage's picker keeps naming the account the next deposit will land in .......   Passed    0.46 sec
100% tests passed out of 3

Would the check still pass if the linked library did nothing? No — and that is worth showing rather than asserting, because "the include resolves" is a weak claim about a library whose actual job is a command-line option. log_level.hpp declares the --log-level helper testkit_main.cpp calls, and it is live on the binary:

$ QT_QPA_PLATFORM=offscreen ./examples/bank/bank_gui_qml_tests --log-level nonsense
error: --log-level: unknown log level 'nonsense'; expected one of debug, info, warn, error, off

A bare include-directory fix would have satisfied the compiler and left the binary's main calling a helper it had no library relationship with; this one links the thing that owns it.

The whole linux-everything tree

Built to completion with -k 0, so one failure would not have hidden others (three bounded foreground passes; ninja resumes). Zero FAILED lines across all three build logs, final pass exiting 0. bank_gui_qml_tests was the only broken target behind the gate, not the first of several. Measured on this branch, clang 22.1.8 / Ninja / Linux; I did not run the full ctest suite for this tree, only the three bank GUI tests above.

This fix alone leaves the hole open

Nothing in CI configures MORPH_BUILD_BANK_GUI, so nothing would catch this target breaking again tomorrow — including this fix. The job named "all optional features" enables 5 of the repository's 14 MORPH_BUILD_* options, and this is not among them. That is #605, deliberately not addressed here: it is a gate-coverage decision with real CI-cost implications, and it should be decided on its own terms rather than smuggled in behind a one-line link fix. No CI job is added or changed by this PR.


#592--fresh advice, where -U does the job

cmake/CompileCache.cmake returns early when a launcher is already pinned in the CMake cache, and told the reader to "reconfigure with --fresh to let this module choose instead". Documentation change only — no behaviour changes, --fresh is not removed, and it is still the right answer when a full reconfigure is genuinely wanted.

All three findings below were measured, on an isolated harness that does nothing but include() the module, with a trivial C target so there is an object to lose.

1. -U reaches the same selection and leaves the directory standing. The guard reads exactly two cache entries, and -U deletes exactly those two:

$ cmake -S src -B b -G Ninja -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
-- [cache] Compiler launcher already set externally (C='ccache', CXX='ccache'); leaving it untouched.
$ cmake --build b                       # object inode 39761163
$ cmake -S src -B b -UCMAKE_C_COMPILER_LAUNCHER -UCMAKE_CXX_COMPILER_LAUNCHER
-- [cache] Enabling fastcache-cc at 127.0.0.1:6674 (...) for C/C++ compilation
$ stat -c '%i' b/CMakeFiles/harness.dir/main.c.o
39761163
$ grep CMAKE_GENERATOR: b/CMakeCache.txt
CMAKE_GENERATOR:INTERNAL=Ninja

2. --fresh deletes CMakeFiles/ and takes the generator with it unless it is re-passed:

$ cmake -S src -B b --fresh
$ stat -c '%i' b/CMakeFiles/harness.dir/main.c.o
stat: cannot statx '.../b/CMakeFiles/harness.dir/main.c.o': No such file or directory
$ grep CMAKE_GENERATOR: b/CMakeCache.txt
CMAKE_GENERATOR:INTERNAL=Unix Makefiles

— on a directory configured -G Ninja, now a Makefile tree with a stale build.ninja still sitting in it. This is the same "generator/flag drift" already recorded in docs/superpowers/progress/2026-08-19-ledger-rung5-progress.md:65, where an implementer's own cmake --fresh desynced a build tree badly enough to block a task. morph's presets pin "generator": "Ninja", so cmake --preset X --fresh is safe; a plain cmake -S . -B build --fresh is not.

3. For a preset-pinned launcher, --fresh does not work at all. "A preset" is one of the three sources the message itself names, and it is the one case where the advice loops — the preset re-applies its cacheVariables on the very reconfigure --fresh triggers:

$ cmake -S src --preset pinned --fresh
-- [cache] Compiler launcher already set externally (C='ccache', CXX='ccache'); leaving it untouched.
-- [cache] That value comes from the CMake cache (a -D, a preset, or an older configure); ...

The tree is gone and the launcher is still pinned. -U on a plain reconfigure of that same directory does clear it (until the preset is next run, which the new text says). This one is not a cost argument at all — it is the advice being wrong — and it is the reason the ordering matters rather than merely being cheaper.

A finding that cuts against the ticket's framing

The ticket is titled "advice that costs a whole build tree", and the headline cost is partly overstated for this particular scenario. A launcher is part of every C/C++ compile command, so changing it invalidates every C/C++ object by either route — measured: the harness object was recompiled on the first build after the -U reconfigure. -U does not save the compiling.

What it does save is the configure state: the populated _deps tree, the generator, generated sources, cached find_ results — which, per finding 2, is where --fresh has actually hurt this repository before. The new comment says this in those words rather than claiming the objects are preserved, and the rendered message says "leaves the build directory standing", not "keeps your objects". Stated as measured; I have not measured the wall-clock difference on a real morph tree, and do not claim one.

The rendered message

-- [cache] That value comes from the CMake cache (a -D, a preset, or an older configure); to let this
   module choose instead, reconfigure the same build directory with -UCMAKE_C_COMPILER_LAUNCHER
   -UCMAKE_CXX_COMPILER_LAUNCHER, which clears exactly those two entries and leaves the build directory
   standing. (--fresh clears them too when they came from a -D or an older configure, at the price of the
   whole build directory; when they come from a preset it clears nothing, because the preset re-applies
   them on the same reconfigure.)

Verified by running it, and the advice verified by following it verbatim (finding 1), not by reasoning about it.

Sweep, wider than the one line

grep -rn -- "--fresh" over the tree, excluding build/ and .git/, finds two occurrences:

  • cmake/CompileCache.cmake:63 — the advisory line, fixed here.
  • docs/superpowers/progress/2026-08-19-ledger-rung5-progress.md:65 — a historical incident report that records a --fresh going wrong rather than recommending one. Left alone; progress docs are a dated record, not live guidance.

docs/, CONTRIBUTING.md and README.md contain no reconfigure or launcher advice at all (grep -niE "reconfigur|stuck|launcher" over them: no matches). No test or CI job asserts on the message text (grep -rln "already set externally" over scripts/ and .github/: no matches), so nothing downstream breaks on the rewording.


Review notes

Copilot review is unavailable on this org, so the review reasoning is here.

  • morph_test_log_level is available at that point in the configure. It is defined in the root CMakeLists.txt under if(MORPH_BUILD_TESTS), after add_subdirectory(examples/bank). That is fine and deliberate — target_link_libraries() resolution is deferred to generate time, and the root CMakeLists.txt comment at the definition site says so explicitly, naming examples/{concepts,bank,vetted_hmac} as the directories relying on it. The sibling bank_gui_tests already links morph_test_main from the same block, three lines away, so this adds no new ordering assumption. Confirmed by the configure succeeding.
  • Gating matches. bank_gui_qml_tests lives inside if(MORPH_BUILD_TESTS) (examples/bank/CMakeLists.txt:164), the same guard that creates morph_test_log_level. There is no configuration where the target exists and the library does not.
  • No over-linking. morph_test_log_level is an INTERFACE library carrying an include directory plus morph::morph and Catch2::Catch2 — both of which this target already had transitively via bank_gui_lib and directly. It adds an include path and no objects.
  • Catch2::Catch2 is kept explicitly rather than left to arrive through morph_test_log_level. A target that names its own Catch2 dependency does not silently break if the testkit's own link line is ever reorganised, and it matches morph_qt_tests, which names both.
  • The --fresh text is preserved, not deleted, as the ticket asked — with its cost and its one failure case attached. A reader who wants a full reconfigure still finds it in the message.
  • Nothing in scripts/branch_partial_allowlist.json was touched; no entry references either file.
  • Neither commit touches any file owned by qt: move QtWebSocketBackend onto the structural registration surface #585 or core: LocalBackend::trackPending rescans every pending completion on every dispatch #528. CHANGELOG.md entries are additive.

Fixes #604. Fixes #592. Related: #605 (the gate that would have caught #604 — deliberately out of scope).

🤖 Generated with Claude Code

https://claude.ai/code/session_01GS5K2vqZtC4xbRiGJHT7jH

Yaraslaut and others added 2 commits September 20, 2026 07:10
fixes #604)

bank_gui_qml_tests compiles examples/common/testkit/testkit_main.cpp, whose
`#include <testkit/log_level.hpp>` resolves against exactly one directory in
the tree -- the repository's tests/ -- and the target reached it through
nothing. It links Catch2::Catch2 directly, where its sibling bank_gui_tests,
one target above, links morph_test_main and gets the path publicly from
morph_test_log_level. That divergence is the defect; the two targets diverge
for a real reason (this one owns its own QGuiApplication-holding main, so it
cannot link a library that carries a `main` of its own), but the reason
covers only the `main`, not the include path.

So link morph_test_log_level alongside Catch2::Catch2 -- the same split, for
the same reason, that morph_qt_tests already uses in tests/qt/CMakeLists.txt.
That keeps the siblings consistent on what they get from the testkit while
leaving each one's `main` where it belongs.

Reproduced on 26bfdb8 with the linux-everything preset, which is the only
configuration in the repository that sets MORPH_BUILD_BANK_GUI=ON:

    FAILED: examples/bank/CMakeFiles/bank_gui_qml_tests.dir/__/common/testkit/testkit_main.cpp.o
    .../examples/common/testkit/testkit_main.cpp:21:10: fatal error: 'testkit/log_level.hpp' file not found
       21 | #include <testkit/log_level.hpp>
          |          ^~~~~~~~~~~~~~~~~~~~~~~
    1 error generated.

and after the change, on the same tree:

    [5/10] Building CXX object examples/bank/CMakeFiles/bank_gui_qml_tests.dir/__/common/testkit/testkit_main.cpp.o
    [6/10] Building CXX object examples/bank/CMakeFiles/bank_gui_qml_tests.dir/tests/gui/test_bank_gui_qml_behaviour.cpp.o
    [7/10] Linking CXX executable examples/bank/bank_gui_qml_tests

    $ QT_QPA_PLATFORM=offscreen ./examples/bank/bank_gui_qml_tests
    All tests passed (32 assertions in 2 test cases)

The linked library is doing more than satisfying an include: the --log-level
option that log_level.hpp declares is live on the binary, which is what tells
the two apart --

    $ ./examples/bank/bank_gui_qml_tests --log-level nonsense
    error: --log-level: unknown log level 'nonsense'; expected one of debug, info, warn, error, off

No CI job configures MORPH_BUILD_BANK_GUI, which is why master carried a
target that does not compile. That gap is #605 and is deliberately not
addressed here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GS5K2vqZtC4xbRiGJHT7jH
 #592)

When a launcher is already in the CMake cache, CompileCache.cmake returns
early and told the reader to "reconfigure with --fresh to let this module
choose instead". Reconfiguring the same build directory with

    cmake -B <dir> -UCMAKE_C_COMPILER_LAUNCHER -UCMAKE_CXX_COMPILER_LAUNCHER

removes exactly the two entries the guard reads, falls through to the
selection below, and leaves the directory standing. This is a wording change
only: --fresh still works, is still the right answer when a full reconfigure
is genuinely wanted, and is kept -- with what it actually costs, and with the
one case where it does not work at all.

Measured on an isolated harness that does nothing but include() this module,
with a trivial C target so there is an object to lose.

  1. -U reaches the selection and keeps the tree:

     $ cmake -S src -B b -G Ninja -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
     -- [cache] Compiler launcher already set externally (C='ccache', CXX='ccache'); leaving it untouched.
     $ cmake --build b   # object inode 39761163
     $ cmake -S src -B b -UCMAKE_C_COMPILER_LAUNCHER -UCMAKE_CXX_COMPILER_LAUNCHER
     -- [cache] Enabling fastcache-cc at 127.0.0.1:6674 (...) for C/C++ compilation
     $ stat -c '%i' b/CMakeFiles/harness.dir/main.c.o
     39761163
     $ grep CMAKE_GENERATOR: b/CMakeCache.txt
     CMAKE_GENERATOR:INTERNAL=Ninja

  2. --fresh deletes CMakeFiles/ and takes the generator with it unless it is
     re-passed -- the same "generator/flag drift" recorded in
     docs/superpowers/progress/2026-08-19-ledger-rung5-progress.md:

     $ cmake -S src -B b --fresh
     $ stat -c '%i' b/CMakeFiles/harness.dir/main.c.o
     stat: cannot statx '.../b/CMakeFiles/harness.dir/main.c.o': No such file or directory
     $ grep CMAKE_GENERATOR: b/CMakeCache.txt
     CMAKE_GENERATOR:INTERNAL=Unix Makefiles     # a stale build.ninja is still in the directory

  3. And for the "a preset" source the message itself names, --fresh does not
     work at all -- the preset re-applies its cacheVariables on the very
     reconfigure --fresh triggers, so the tree is gone and the pin remains:

     $ cmake -S src --preset pinned --fresh
     -- [cache] Compiler launcher already set externally (C='ccache', CXX='ccache'); leaving it untouched.
     -- [cache] That value comes from the CMake cache (a -D, a preset, or an older configure); ...

     while -U on a plain reconfigure of that same directory does clear it,
     until the preset is next run.

One thing the new text does not claim, because it is not true: -U does not
save the compiling. A launcher is part of every C/C++ compile command, so the
next build recompiles those TUs by either route (measured -- the harness
object was rebuilt after the -U reconfigure). What -U saves is the configure
state: the populated _deps tree, the generator, generated sources, cached
find_ results. The comment says so in those words.

Swept the rest of the tree for the same advice: `grep -rn -- --fresh`
outside build/ and .git/ finds only this line and one historical incident
report in docs/superpowers/progress/, which records a --fresh going wrong
rather than recommending one and is left alone. docs/, CONTRIBUTING.md and
README.md contain no reconfigure or launcher advice at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GS5K2vqZtC4xbRiGJHT7jH
@Yaraslaut
Yaraslaut force-pushed the fix/604-bank-qml-tests-include-592-fresh-advice branch from 00b07c6 to 88a1048 Compare September 20, 2026 05:12
@codecov

codecov Bot commented Sep 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@Yaraslaut
Yaraslaut merged commit 91515ae into master Sep 20, 2026
50 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant