From 675943ab602a2d7315f7f4301994306fb1d57674 Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Sun, 20 Sep 2026 07:10:20 +0200 Subject: [PATCH 1/2] examples/bank: give bank_gui_qml_tests the include path its main needs (fixes #604) bank_gui_qml_tests compiles examples/common/testkit/testkit_main.cpp, whose `#include ` 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 26bfdb8f 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 | ^~~~~~~~~~~~~~~~~~~~~~~ 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) Claude-Session: https://claude.ai/code/session_01GS5K2vqZtC4xbRiGJHT7jH --- CHANGELOG.md | 16 ++++++++++++++++ examples/bank/CMakeLists.txt | 8 ++++++++ 2 files changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e55f0485..10287b6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -241,6 +241,22 @@ API surface). ### Fixed +- **`bank_gui_qml_tests` did not compile.** The target compiles + `examples/common/testkit/testkit_main.cpp`, whose `#include + ` resolves only against the repository's `tests/` + directory, and it reached that directory through nothing: it links + `Catch2::Catch2` directly, where its sibling `bank_gui_tests` links + `morph_test_main` (which carries the include path publicly, via + `morph_test_log_level`). Reproduced on `26bfdb8f` with the `linux-everything` + preset, the only configuration that switches `MORPH_BUILD_BANK_GUI` on: + `fatal error: 'testkit/log_level.hpp' file not found`. The target now also + links `morph_test_log_level` — the same split, for the same reason, that + `morph_qt_tests` already uses for a suite that owns its own `main`. It + builds, its 32 assertions pass offscreen, and `--log-level` (the helper that + header declares) is live on it. No CI job configures + `MORPH_BUILD_BANK_GUI`, so this fix is not yet gated by anything; morph#605 + covers that separately. morph#604. + - **`equation()` no longer walks a shared derivation once per path.** `EquationRenderer::assignLabels` was the one traversal without a visited set, so a node reachable by *k* displayed paths was walked *k* times. Since the diff --git a/examples/bank/CMakeLists.txt b/examples/bank/CMakeLists.txt index 4a884d73..a4f84b94 100644 --- a/examples/bank/CMakeLists.txt +++ b/examples/bank/CMakeLists.txt @@ -275,6 +275,14 @@ if(MORPH_BUILD_TESTS) ${PROJECT_SOURCE_DIR}/examples/common) target_link_libraries(bank_gui_qml_tests PRIVATE bank_gui_lib Catch2::Catch2 + # Our own main() owns the QGuiApplication, so this cannot link + # morph_test_main the way bank_gui_tests above does -- that + # library carries a `main` of its own. morph_test_log_level is + # the half of it that is wanted here: the --log-level helper + # testkit_main.cpp calls, and the tests/ include directory that + # puts on the path. Same split, for the + # same reason, as morph_qt_tests (tests/qt/CMakeLists.txt). + morph_test_log_level Qt6::Gui Qt6::Qml Qt6::Quick Qt6::QuickControls2) target_compile_features(bank_gui_qml_tests PRIVATE cxx_std_23) target_compile_definitions(bank_gui_qml_tests PRIVATE From 88a10481ea87404e02f9bb6855ed062aaf4f5133 Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Sun, 20 Sep 2026 07:10:32 +0200 Subject: [PATCH 2/2] cmake: name the cheap way out of a pinned compiler launcher first (fixes #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 -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) Claude-Session: https://claude.ai/code/session_01GS5K2vqZtC4xbRiGJHT7jH --- CHANGELOG.md | 18 ++++++++++++++++++ cmake/CompileCache.cmake | 29 ++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10287b6b..5ad9e392 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -257,6 +257,24 @@ API surface). `MORPH_BUILD_BANK_GUI`, so this fix is not yet gated by anything; morph#605 covers that separately. morph#604. +- **The compiler-cache module told you to throw away your build directory, and + for a preset it did not even work.** When a launcher is already pinned in the + CMake cache, `cmake/CompileCache.cmake` advised "reconfigure with `--fresh` + to let this module choose instead". Reconfiguring the *same* directory with + `-UCMAKE_C_COMPILER_LAUNCHER -UCMAKE_CXX_COMPILER_LAUNCHER` removes exactly + the two entries the module's guard reads, reaches the same selection, and + leaves the build directory standing; `--fresh` deletes `CMakeFiles/` and, + unless the generator is re-passed, drops `CMAKE_GENERATOR` back to the + platform default (measured: a `-G Ninja` tree came back `Unix Makefiles` + with a stale `build.ninja` beside the new `Makefile`). And where the pin + comes from a **preset** — one of the three sources the message itself + names — `--fresh` clears nothing at all, because the preset re-applies its + `cacheVariables` on the very reconfigure `--fresh` triggers: the tree is + gone and the launcher is still pinned. The message now names `-U` first and + keeps `--fresh` with its actual cost and its actual limits. Behaviour is + unchanged; only the advice is. Measured on an isolated harness that does + nothing but `include()` the module. morph#592. + - **`equation()` no longer walks a shared derivation once per path.** `EquationRenderer::assignLabels` was the one traversal without a visited set, so a node reachable by *k* displayed paths was walked *k* times. Since the diff --git a/cmake/CompileCache.cmake b/cmake/CompileCache.cmake index 4f6fd256..181a4772 100644 --- a/cmake/CompileCache.cmake +++ b/cmake/CompileCache.cmake @@ -58,9 +58,36 @@ if(DEFINED CMAKE_CXX_COMPILER_LAUNCHER OR DEFINED CMAKE_C_COMPILER_LAUNCHER) # A build tree configured before this module existed carries the launcher of # the day in its cache, and would keep it forever without a word about why # the selection below never runs. + # + # -U before --fresh, because the guard above reads exactly two cache entries + # and -U deletes exactly those two: the reconfigure then falls through to + # the selection below without discarding the build directory. --fresh + # reaches the same place for a -D or an older configure, but it deletes + # CMakeFiles/ and, unless the generator is re-passed, drops CMAKE_GENERATOR + # back to the platform default on the way -- a `cmake -S . -B build --fresh` + # on a tree configured `-G Ninja` comes back a Makefile tree with a stale + # build.ninja still sitting in it. + # + # What -U saves is the configure state -- the populated _deps tree, the + # generator, generated sources, cached find_ results -- and not, note, the + # compiling: a launcher is part of every C/C++ compile command, so the next + # build recompiles those TUs whichever route got here. That is the honest + # size of it. + # + # The case that makes the ordering matter rather than merely cheaper is the + # "a preset" one this very message names, where --fresh does not work at + # all: the preset re-applies its cacheVariables on the reconfigure that + # --fresh triggers, so the launcher comes back pinned and the tree is gone + # for nothing. -U clears it there too, until the preset is next run. + # All of the above measured on an isolated harness, morph#592. if(DEFINED CACHE{CMAKE_CXX_COMPILER_LAUNCHER} OR DEFINED CACHE{CMAKE_C_COMPILER_LAUNCHER}) message(STATUS "[cache] That value comes from the CMake cache (a -D, a preset, or an older configure); " - "reconfigure with --fresh to let this module choose instead.") + "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.)") endif() return() endif()