What
Nothing in this repository catches a Q_OBJECT header that has been separated
from the translation unit AUTOMOC pairs it with. The first signal is a linker
error, and it arrives in six CI legs at once, none faster than four minutes.
This has now happened twice.
Verification status
Reproduced, on laneCI3-batch-651-652-655 at the pre-fix revision
d380895c (PR #657), rebased onto f24e225a. Local clang 22.1.8 + Ninja,
build directory configured from empty:
cmake -S . -B <empty> -G Ninja \
-DCMAKE_TOOLCHAIN_FILE=cmake/llvm-toolchain.cmake -DCMAKE_BUILD_TYPE=Debug \
-DMORPH_BUILD_QT=ON -DMORPH_BUILD_LADDER=ON -DMORPH_LADDER_RUNGS=all \
-DMORPH_BUILD_TESTS=ON -DMORPH_BUILD_NET=ON -DMORPH_BUILD_OFFLINE_SQLITE=ON
morph_ladder_testkit compiles and archives with no diagnostic at all:
[57/62] Automatic MOC and UIC for target morph_ladder_testkit
[58/62] Building CXX object .../morph_ladder_testkit_autogen/mocs_compilation.cpp.o
[59/62] Building CXX object .../testkit_src/qml_surface.cpp.o
[60/62] Building CXX object .../testkit_src/fault_proxy.cpp.o
[61/62] Linking CXX static library examples/common/libmorph_ladder_testkit.a
while its AUTOMOC output is empty:
$ cat examples/common/morph_ladder_testkit_autogen/mocs_compilation.cpp
// This file is autogenerated. Changes will be overwritten.
// No files found that require moc or the moc files are included
enum some_compilers { need_more_than_nothing };
$ nm -C examples/common/libmorph_ladder_testkit.a | grep staticMetaObject
U morph::ladder::testkit::FaultProxy::staticMetaObject
The defect only becomes visible one target later:
$ ninja ladder_common_tests
/usr/bin/ld: examples/common/libmorph_ladder_testkit.a(fault_proxy.cpp.o): in function `morph::ladder::testkit::FaultProxy::FaultProxy(QUrl, QObject*)':
examples/common/testkit_src/fault_proxy.cpp:10: undefined reference to `vtable for morph::ladder::testkit::FaultProxy'
/usr/bin/ld: ... undefined reference to `morph::ladder::testkit::FaultProxy::staticMetaObject'
/usr/bin/ld: ... undefined reference to `typeinfo for morph::ladder::testkit::FaultProxy'
clang++: error: linker command failed with exit code 1
Not verified: the proposed gate below. It is not implemented and nothing
about its cost or false-positive rate has been measured.
Why a gate, given the link already fails
Because the link is the only thing that fails, and it fails late and wide.
On PR #657 this single cause turned six legs red — Application ladder,
Application ladder / ASan+UBSan, Kanban / ThreadSanitizer,
Linux / all optional features (clang), Linux / all optional features
(gcc), Linux / clang-coverage — with the fastest of them at 4m03s and the
sanitizer legs far longer. A source-level or configure-time check costs
seconds.
More to the point, the repository already knows about this failure mode and
wrote it down. cmake/morph_add_rung.cmake, at the _lib_headers glob:
The rung's public headers are listed as target sources purely so AUTOMOC
sees them. AUTOMOC looks for a Q_OBJECT header next to the .cpp of the same
basename, and a rung's layout deliberately splits those apart
(include/<rung>/app/app.hpp vs src/app/app.cpp), so a QObject declared
in include/ gets no moc output at all otherwise — which a static library
happily builds and only fails at the first link that actually needs the
vtable (pastebin::app::App, hit the moment ladder_pastebin_tests linked it).
That is the same defect, described in advance, and #652 still walked into it —
documentation in one CMake file did not stop a change in another. Per AGENTS.md
("a control that reports success while measuring nothing"), the inverse also
holds: a known failure mode with no control is one that recurs on schedule.
Sketch of a gate
The cheapest shape reuses machinery that already exists.
scripts/check_automoc_includes.sh already walks a configured build tree
looking for moc_*.cpp and *.moc. A sibling check could assert the
complement: every first-party header that contains a macro from
AUTOMOC_MACRO_NAMES (Q_OBJECT, Q_GADGET, Q_NAMESPACE, …) and is
reachable from a configured AUTOMOC target has corresponding moc output in
that tree. On the revision above, examples/common/testkit/fault_proxy.hpp
contains Q_OBJECT and no moc_fault_proxy.cpp exists anywhere under the
build directory — so the check would have fired, before the link, in whichever
leg configured first.
The design question this needs answering before it is worth writing:
- Scoping. A build tree only covers the targets that configure enabled. A
naive "every tracked header with Q_OBJECT must have moc output" would
false-positive on everything behind an off-by-default option (the WASM
shells, bank's GUI, the rungs when MORPH_BUILD_LADDER=OFF). The check has
to derive the header set from the configured targets — plausibly from the
CMake file API's codemodel, which also yields each target's AUTOMOC
property and source list — rather than from git ls-files.
- Where it runs.
check_automoc_includes.sh is already invoked against a
Qt-enabled build; the same leg is the obvious host. That means it does not
pre-empt the very fastest configure, only the slow ones.
An alternative that needs no build at all — parse add_library/add_executable
source lists and check the pairing statically — is cheaper to run and much more
brittle to write, since the repository generates most of its Qt targets through
morph_add_rung().
What would change the verdict
- Close when a gate lands that fails on a mutated tree where
testkit/fault_proxy.hpp is removed from morph_ladder_testkit's source
list in examples/common/CMakeLists.txt — that mutation is the exact
reproduction above, so it is a ready-made anti-vacuity test.
- Close as wontfix if the judgement is that a linker error is adequate
signal for a defect that costs six CI legs and has recurred once. That is a
defensible call; it should be recorded as one rather than left implicit.
- Re-open if a third occurrence appears.
Context
Found while fixing PR #657, whose #652 commit moved
examples/common/testkit/{fault_proxy,qml_surface}.cpp into
examples/common/testkit_src/ and left the headers behind. Fixed there by
listing both headers as sources of morph_ladder_testkit. This issue is about
the absent control, not that fix.
What
Nothing in this repository catches a
Q_OBJECTheader that has been separatedfrom the translation unit AUTOMOC pairs it with. The first signal is a linker
error, and it arrives in six CI legs at once, none faster than four minutes.
This has now happened twice.
Verification status
Reproduced, on
laneCI3-batch-651-652-655at the pre-fix revisiond380895c(PR #657), rebased ontof24e225a. Local clang 22.1.8 + Ninja,build directory configured from empty:
morph_ladder_testkitcompiles and archives with no diagnostic at all:while its AUTOMOC output is empty:
The defect only becomes visible one target later:
Not verified: the proposed gate below. It is not implemented and nothing
about its cost or false-positive rate has been measured.
Why a gate, given the link already fails
Because the link is the only thing that fails, and it fails late and wide.
On PR #657 this single cause turned six legs red —
Application ladder,Application ladder / ASan+UBSan,Kanban / ThreadSanitizer,Linux / all optional features(clang),Linux / all optional features(gcc),
Linux / clang-coverage— with the fastest of them at 4m03s and thesanitizer legs far longer. A source-level or configure-time check costs
seconds.
More to the point, the repository already knows about this failure mode and
wrote it down.
cmake/morph_add_rung.cmake, at the_lib_headersglob:That is the same defect, described in advance, and #652 still walked into it —
documentation in one CMake file did not stop a change in another. Per AGENTS.md
("a control that reports success while measuring nothing"), the inverse also
holds: a known failure mode with no control is one that recurs on schedule.
Sketch of a gate
The cheapest shape reuses machinery that already exists.
scripts/check_automoc_includes.shalready walks a configured build treelooking for
moc_*.cppand*.moc. A sibling check could assert thecomplement: every first-party header that contains a macro from
AUTOMOC_MACRO_NAMES(Q_OBJECT,Q_GADGET,Q_NAMESPACE, …) and isreachable from a configured AUTOMOC target has corresponding moc output in
that tree. On the revision above,
examples/common/testkit/fault_proxy.hppcontains
Q_OBJECTand nomoc_fault_proxy.cppexists anywhere under thebuild directory — so the check would have fired, before the link, in whichever
leg configured first.
The design question this needs answering before it is worth writing:
naive "every tracked header with
Q_OBJECTmust have moc output" wouldfalse-positive on everything behind an off-by-default option (the WASM
shells, bank's GUI, the rungs when
MORPH_BUILD_LADDER=OFF). The check hasto derive the header set from the configured targets — plausibly from the
CMake file API's codemodel, which also yields each target's
AUTOMOCproperty and source list — rather than from
git ls-files.check_automoc_includes.shis already invoked against aQt-enabled build; the same leg is the obvious host. That means it does not
pre-empt the very fastest configure, only the slow ones.
An alternative that needs no build at all — parse
add_library/add_executablesource lists and check the pairing statically — is cheaper to run and much more
brittle to write, since the repository generates most of its Qt targets through
morph_add_rung().What would change the verdict
testkit/fault_proxy.hppis removed frommorph_ladder_testkit's sourcelist in
examples/common/CMakeLists.txt— that mutation is the exactreproduction above, so it is a ready-made anti-vacuity test.
signal for a defect that costs six CI legs and has recurred once. That is a
defensible call; it should be recorded as one rather than left implicit.
Context
Found while fixing PR #657, whose #652 commit moved
examples/common/testkit/{fault_proxy,qml_surface}.cppintoexamples/common/testkit_src/and left the headers behind. Fixed there bylisting both headers as sources of
morph_ladder_testkit. This issue is aboutthe absent control, not that fix.