Rebuild when an #included header changes #31

Merged
catbot merged 3 commits from claude/issue-30 into master 2026-07-31 11:13:32 +00:00
Member

The incremental check compared each artifact against its own source and its module imports. Headers were in neither set — the scanner reads import lines, and a .cppm/.cpp keeps its mtime when a header it includes is edited — so a header-only edit reported nothing to do and left objects compiled against the previous contents. Silent, and the same mixed-layout failure shape as #27, reached through #include instead of import.

What changed

  • Depfiles. Every C++ interface, partition, implementation and C compile now passes -MD -MF <artifact>.d, so the compiler reports what it actually opened. NewestPrerequisite (Platform) reads that file back and returns the newest prerequisite mtime; each Check folds it into the comparison against the artifact. A missing depfile (an object from a crafter-build that wrote none) or a prerequisite that no longer exists returns file_time_type::max() — rebuild once, since neither state is evidence of freshness.
  • The parser unescapes make syntax rather than splitting on whitespace: clang wraps depfiles onto continuation lines, escapes spaces in filenames, and a token ending in : is a rule target (which also keeps Windows C:\... paths intact).
  • Relink fix. The C compile loop sets no repack flag and the "any object newer than the archive" sweep only walked interfaces and implementations, so editing a .c recompiled its object and then linked nothing. Found by the new test, where a rebuilt counter.c produced a binary still printing the old value.
  • CUDA is still .cu-mtime only — nvcc's -MD/-MF are untested here, noted in place.

Tests

New tests/IncrementalHeaderChange: one header per pass, asserting both directions (the objects that include it recompile, the ones that don't are untouched), then running the binary — "was recompiled" only matters if the program agrees with itself. Covers the module interface (a macro in its global module fragment decides an exported class's layout), the implementation unit, a C source, an idle rebuild that must recompile nothing, and an object whose depfile is gone. The implementation unit's header has a space in its name so the depfile spells it escaped.

With the depfile lookup stubbed out, 9 of its assertions fail; with it, all pass. Full suite: 20 passed, 0 failed.

Also verified by hand on this repo: touch lib/DirStackFileIncluder.h now recompiles exactly Crafter.Build-Shader.cpp and relinks (previously: nothing), and a no-change rebuild still compiles nothing.

Resolves #30

🤖 Generated with Claude Code

The incremental check compared each artifact against its own source and its module imports. Headers were in neither set — the scanner reads `import` lines, and a `.cppm`/`.cpp` keeps its mtime when a header it includes is edited — so a header-only edit reported nothing to do and left objects compiled against the previous contents. Silent, and the same mixed-layout failure shape as #27, reached through `#include` instead of `import`. ## What changed - **Depfiles.** Every C++ interface, partition, implementation and C compile now passes `-MD -MF <artifact>.d`, so the compiler reports what it actually opened. `NewestPrerequisite` (Platform) reads that file back and returns the newest prerequisite mtime; each `Check` folds it into the comparison against the artifact. A missing depfile (an object from a crafter-build that wrote none) or a prerequisite that no longer exists returns `file_time_type::max()` — rebuild once, since neither state is evidence of freshness. - The parser unescapes make syntax rather than splitting on whitespace: clang wraps depfiles onto continuation lines, escapes spaces in filenames, and a token ending in `:` is a rule target (which also keeps Windows `C:\...` paths intact). - **Relink fix.** The C compile loop sets no repack flag and the "any object newer than the archive" sweep only walked interfaces and implementations, so editing a `.c` recompiled its object and then linked nothing. Found by the new test, where a rebuilt `counter.c` produced a binary still printing the old value. - CUDA is still `.cu`-mtime only — nvcc's `-MD`/`-MF` are untested here, noted in place. ## Tests New `tests/IncrementalHeaderChange`: one header per pass, asserting both directions (the objects that include it recompile, the ones that don't are untouched), then running the binary — "was recompiled" only matters if the program agrees with itself. Covers the module interface (a macro in its global module fragment decides an exported class's layout), the implementation unit, a C source, an idle rebuild that must recompile nothing, and an object whose depfile is gone. The implementation unit's header has a space in its name so the depfile spells it escaped. With the depfile lookup stubbed out, 9 of its assertions fail; with it, all pass. Full suite: 20 passed, 0 failed. Also verified by hand on this repo: `touch lib/DirStackFileIncluder.h` now recompiles exactly `Crafter.Build-Shader.cpp` and relinks (previously: nothing), and a no-change rebuild still compiles nothing. Resolves #30 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The staleness check compared an artifact against its own source and its
module imports. Headers were in neither set: the scanner reads `import`
lines, and a .cppm or .cpp keeps its mtime when a header it includes is
edited — so the build reported nothing to do and left objects compiled
against the previous contents. Same silent mixed-layout binary as issue
27, reached through #include instead of import.

Ask the compiler what it actually opened. Every C++ and C compile now
passes -MD -MF <artifact>.d, and Check reads that depfile back through
NewestPrerequisite, comparing every prerequisite's mtime against the
artifact. A missing depfile (an object from a crafter-build that wrote
none) or a prerequisite that no longer exists reads as "rebuild": neither
is evidence of freshness.

The parser unescapes make syntax rather than splitting on whitespace,
since clang wraps depfiles onto continuation lines and escapes spaces in
filenames.
The C compile loop sets no repack flag, and the "is any object newer than
the archive" sweep only walked interfaces and implementations. So editing
a .c recompiled its object and then linked nothing: the archive and the
executable kept the previous one. Surfaced by the header-change test,
where a rebuilt counter.c produced a binary still printing the old value.
test: header-change incrementality across every compile path
Some checks failed
CI / build-test-release (pull_request) Failing after 12m4s
7975cb1df8
One header per pass, asserting both directions — the objects that include
it recompile, the ones that don't are untouched — then running the binary,
since "was recompiled" only matters if the program agrees with itself.

Covers the module interface (a macro in its global module fragment decides
an exported class's layout), the implementation unit, a C source, an idle
rebuild that must recompile nothing, and an object whose depfile is gone.
The implementation unit's header has a space in its name so the depfile
spells it escaped.
catbot merged commit 8d921fd7bd into master 2026-07-31 11:13:32 +00:00
catbot deleted branch claude/issue-30 2026-07-31 11:13:32 +00:00
Sign in to join this conversation.
No description provided.