Skip to content

No gate catches a Q_OBJECT header split from its TU; the first signal is a link error in six CI legs #659

Description

@Yaraslaut

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: ciSubsystem: ciarea: qtSubsystem: qtenhancementNew feature or requesttriage: validWell-framed; implement as written

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions