Fix silent stale-build corruption on module interface changes #28
No reviewers
Labels
No labels
bug
claude:blocked
claude:done
claude:failed
claude:in-progress
claude:ready
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
wontfix
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Catcrafts/Crafter.Build!28
Loading…
Reference in a new issue
No description provided.
Delete branch "claude/issue-27"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Adding a data member to a class in a module interface did not rebuild every object compiled against the old layout. The build succeeded, and the resulting mixed-layout binary surfaced later as a SIGSEGV in a destructor.
Root cause
GetInterfacesAndImplementationsscans a TU'simport X;statements when the source is declared. A name matching neither a module in the Configuration nor one reachable throughdependencieswas dropped on the floor — leaving that TU with no staleness edge to the interface it consumes.dependenciesis routinely assigned after the scan.AddTestis exactly that shape: it resolvestests/<name>/main.cppand only then returns a builder whose.Dependencies()supplies the library (Crafter.Build-Test.cpp:439 vs :484). So every test that imported a dependency's module had no edge to it at all. A member added to the interface rebuilt the library, relinked the test, and kept the test's object exactly as it was — which is why the original report saw the corruption in test executables specifically, and whyShouldInteropCurlHTTP1passed: it drives the listener withcurland never constructs the mismatched object.Nothing fails to link when a member is added — every signature and mangled name is unchanged — so there was no diagnostic of any kind.
Reproduced first, in the reporter's shape
A library whose interface gains a
std::string, plus a consumer declared in theAddTestorder. Before the fix the second build printedLinking SizeCheckwith noCompiling main.cpp, and the binary reportedtest sees sizeof(Widget)=72 lib says=96. The same edit after the fix recompiles the consumer and the sizes agree.The fix
Unresolved names are kept on the partition/implementation as
pendingImports, andConfiguration::ResolvePendingImports()retries them against the dependency DAG as it stands.Build()calls it immediately before comparing mtimes, so the window closes for every caller rather than only those that declare in the right order;TestBuilder::Dependenciesalso calls it so the Configuration is coherent for anyone inspecting it beforehand.On the issue's suggested directions: this takes option 1's spirit at the point where it costs nothing — the edge is established before the answer is computed. It does not add option 2's post-hoc consistency check, because after this the tracking has the information it needs and a check would only restate it. Option 3 is included.
Also in this PR — the same theme elsewhere
Project args are now part of the variant identity. Flags
ApplyStandardArgsdoesn't itself interpret typically decide what gets compiled or bundled (--no-webgpudropping entries fromcfg.files). Both settings previously shared one bin dir and interleaved their outputs, leaving a bundle matching neither. Sorted and deduplicated so flag order doesn't split the cache; inherited by test Configurations. Verified:crafter-buildandcrafter-build -- --no-webgpunow land in separate directories for both the exe and the lib.The host PCM cache is invalidated by source content, not mtime.
<cache>/crafter.build/<target>-<march>/is shared by every crafter-build on the machine, and an mtime cannot distinguish "this PCM is newer than my source" from "this PCM was built from different sources that happen to be newer". Two checkouts of different versions silently compiled theirproject.cppagainst each other's declarations. I hit this for real while establishing the baseline for this PR — amasterworktree poisoned the cache for the main checkout, and the resulting crashes cost a while to attribute. Content stamping also covers what a per-file mtime never could: the cached PCMs import each other, so a change to:Interfaceinvalidates:Clang's PCM withCrafter.Build-Clang.cppmuntouched.crafter-build cleanremoves the project'sbin/andbuild/trees. It deliberately does not loadproject.cpp— cleaning is most often reached when something is already wrong, and a clean that first needs the project to compile is useless exactly then.Tests
crafter-build test— 18 passed, 0 failed.Three new tests, each verified to fail when its fix is reverted:
tests/IncrementalInterfaceChange— the interface member change end to end. Asserts the pending import is recorded, thatResolvePendingImportsplaces it and is idempotent, thatAddTest(...).Dependencies()wires it up, and — without callingResolvePendingImportsitself, so the guarantee under test isBuild()'s — that the consumer object is recompiled and the binary agrees on the layout. With the fix's call sites neutered it reports 4 failures includingsecond pass agrees on the layout (exit=1 output='24 48').tests/HostCacheSourceStamp— asserts the cache notices a change the mtime rule provably cannot see (different content, deliberately backdated), and that unchanged sources are still a no-op.tests/CleanProject— clean works against an uncompilable project, is idempotent, leaves unrelated files alone, and resolves relative to the project file rather than the cwd.crafter-build lintandcrafter-build format --checkare clean (41 files, 15 rules).The issue's three-step reproduction now gives identical results for the incremental and clean builds.
Two environment notes, not repo changes
Worth recording, since a human reproducing this locally may not need them:
llvm-aris absent from this container, so the LTO static-archive step fails. I shimmed it to GNUar(which here has the LLVM plugin) to run the suite. Unrelated to this PR, but it will blockcrafter-build teston any static-lib project in this image./usr/local/share/crafter-buildpredates the Lint module, sotests/ConcurrentCacheRace— which hardcodes that path — fails on unmodifiedmastertoo. PointingCRAFTER_BUILD_HOMEat this checkout'sshare/crafter-buildgives it a complete module set; with that it passes both here and onmaster.No UI surface changed, so no screenshots.
Resolves #27