feat(incremental): rebuild when an #included header changes

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.
This commit is contained in:
catbot 2026-07-31 11:11:38 +00:00
commit 1d0d8e1e28
6 changed files with 111 additions and 8 deletions

View file

@ -780,12 +780,15 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
files += std::format(" {}_source.o ", (buildDir / cFile.filename()).string());
const std::string objPath = std::format("{}_source.o", (buildDir / cFile.filename()).string());
const std::string srcPath = std::format("{}.c", cFile.string());
if (!fs::exists(objPath) || (fs::exists(srcPath) && fs::last_write_time(srcPath) > fs::last_write_time(objPath))) {
// C sources carry their dependencies entirely in #includes, so the
// depfile the compile below writes is the whole record of what this
// object was built from beyond the .c itself.
if (!fs::exists(objPath) || (fs::exists(srcPath) && fs::last_write_time(srcPath) > fs::last_write_time(objPath)) || NewestPrerequisite(std::format("{}.d", objPath)) > fs::last_write_time(objPath)) {
threads.emplace_back([&cFile, &buildDir, &buildError, &buildCancelled, &config, &includeFlags, &defineFlags, &userFlags, &cArchFlags, &ltoCompileFlags]() {
Progress::Task task(std::format("Compiling {}.c", cFile.filename().string()));
if (buildCancelled.load(std::memory_order_relaxed)) return;
std::string result = RunCommand(std::format("clang {}.c --target={}{} -O3{} -c{}{}{} -o {}_source.o", cFile.string(), config.target, cArchFlags, ltoCompileFlags, includeFlags, defineFlags, userFlags, (buildDir / cFile.filename()).string()));
std::string result = RunCommand(std::format("clang {0}.c --target={1}{2} -O3{3} -c{4}{5}{6} -MD -MF {7}_source.o.d -o {7}_source.o", cFile.string(), config.target, cArchFlags, ltoCompileFlags, includeFlags, defineFlags, userFlags, (buildDir / cFile.filename()).string()));
if (result.empty()) return;
bool expected = false;
@ -800,6 +803,10 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
files += std::format(" {}_source.o ", (buildDir / cFile.filename()).string());
const std::string objPath = std::format("{}_source.o", (buildDir / cFile.filename()).string());
const std::string srcPath = std::format("{}.cu", cFile.string());
// Still .cu-mtime only: the header tracking the C and C++ paths get
// comes from asking the compiler for a depfile, and nvcc's -MD/-MF are
// untested here (no CUDA toolkit in CI), so a .cu that only sees a
// changed header does not rebuild yet.
if (!fs::exists(objPath) || (fs::exists(srcPath) && fs::last_write_time(srcPath) > fs::last_write_time(objPath))) {
threads.emplace_back([&cFile, &buildDir, &buildError, &buildCancelled]() {
Progress::Task task(std::format("Compiling {}.cu", cFile.filename().string()));