Skip to content

Commit ded76aa

Browse files
committed
feat: fast-path fingerprint coherence + diagnostic + cxx_scan restat
P1: .build_cache now stores fingerprint hex (4th line). try_fast_build validates that the cached fingerprint matches the outputDir basename; if inconsistent (e.g. switched mcpp installation), the cache is invalidated immediately instead of silently falling through to a different fingerprint directory. P1.5: run_build_plan prints a warning when the outputDir fingerprint differs from the previous .build_cache entry, so users immediately see why a full rebuild is happening instead of getting a silent 26s. P2: cxx_scan rule now writes .ddi to $out.tmp first, then compares with existing $out via cmp -s. If content is identical, old $out is kept (preserving mtime). Combined with restat = 1, this prevents downstream dyndep/compile edges from being marked dirty when a source file's mtime changed but its module dependencies didn't.
1 parent 9bd1a6d commit ded76aa

2 files changed

Lines changed: 58 additions & 11 deletions

File tree

src/build/ninja_backend.cppm

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -243,23 +243,36 @@ std::string emit_ninja_string(const BuildPlan& plan) {
243243

244244
if (dyndep) {
245245
// Scan rule: produce P1689 .ddi for one TU.
246+
// P2: write to $out.tmp first, then compare with existing $out.
247+
// If content is identical, keep old $out (preserving mtime) so
248+
// downstream dyndep/compile edges are not marked dirty.
249+
// restat = 1 tells Ninja to check if $out actually changed.
246250
// GCC: built-in -fdeps-format=p1689r5 flags during preprocessing.
247251
// Clang: external clang-scan-deps tool with -format=p1689.
248252
append("rule cxx_scan\n");
249253
if (plan.scanDepsPath.empty()) {
250254
// GCC path: compiler-integrated P1689 scanning.
251255
append(" command = $toolenv $cxx $cxxflags -fmodules "
252256
"-fdeps-format=p1689r5 "
253-
"-fdeps-file=$out -fdeps-target=$compile_target "
254-
"-M -MM -MF $out.dep -E $in -o $compile_target\n");
257+
"-fdeps-file=$out.tmp -fdeps-target=$compile_target "
258+
"-M -MM -MF $out.dep -E $in -o $compile_target && "
259+
"if [ -f \"$out\" ] && cmp -s \"$out.tmp\" \"$out\"; then "
260+
"rm -f \"$out.tmp\"; "
261+
"else "
262+
"mv -f \"$out.tmp\" \"$out\"; "
263+
"fi\n");
255264
} else {
256-
// Clang path: clang-scan-deps produces P1689 JSON to stdout,
257-
// then we redirect to $out. The -- separator passes the full
258-
// compile command so clang-scan-deps knows the flags/sysroot.
265+
// Clang path: clang-scan-deps produces P1689 JSON to stdout.
259266
append(" command = $toolenv $scan_deps -format=p1689 -- "
260-
"$cxx $cxxflags -c $in -o $compile_target > $out\n");
267+
"$cxx $cxxflags -c $in -o $compile_target > $out.tmp && "
268+
"if [ -f \"$out\" ] && cmp -s \"$out.tmp\" \"$out\"; then "
269+
"rm -f \"$out.tmp\"; "
270+
"else "
271+
"mv -f \"$out.tmp\" \"$out\"; "
272+
"fi\n");
261273
}
262-
append(" description = SCAN $out\n\n");
274+
append(" description = SCAN $out\n");
275+
append(" restat = 1\n\n");
263276

264277
// Aggregate .ddi files into a Ninja dyndep file.
265278
append(std::format(

src/cli.cppm

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2106,7 +2106,8 @@ constexpr std::string_view kBuildCacheFile = "target/.build_cache";
21062106
void write_build_cache(const std::filesystem::path& projectRoot,
21072107
const std::filesystem::path& outputDir,
21082108
const std::string& ninjaProgram,
2109-
const std::string& targetTriple) {
2109+
const std::string& targetTriple,
2110+
const std::string& fingerprintHex = "") {
21102111
auto path = projectRoot / kBuildCacheFile;
21112112
std::error_code ec;
21122113
std::filesystem::create_directories(path.parent_path(), ec);
@@ -2115,6 +2116,7 @@ void write_build_cache(const std::filesystem::path& projectRoot,
21152116
f << outputDir.string() << '\n';
21162117
f << ninjaProgram << '\n';
21172118
f << targetTriple << '\n';
2119+
f << fingerprintHex << '\n';
21182120
}
21192121
}
21202122

@@ -2173,10 +2175,27 @@ int run_build_plan(BuildContext& ctx, bool verbose, bool no_cache,
21732175
}
21742176
}
21752177

2178+
// P1.5: warn if fingerprint changed from last build (explains full rebuild).
2179+
{
2180+
auto cachePath = ctx.projectRoot / kBuildCacheFile;
2181+
std::ifstream cf(cachePath);
2182+
std::string oldDir;
2183+
if (std::getline(cf, oldDir) && !oldDir.empty()) {
2184+
auto oldFp = std::filesystem::path(oldDir).filename().string();
2185+
auto newFp = ctx.outputDir.filename().string();
2186+
if (oldFp != newFp) {
2187+
mcpp::ui::warning(std::format(
2188+
"fingerprint changed ({} → {}), full rebuild",
2189+
oldFp, newFp));
2190+
}
2191+
}
2192+
}
2193+
21762194
// P0: save build cache for fast-path on next invocation.
21772195
if (!no_cache && !r->ninjaProgram.empty()) {
2196+
auto fpHex = ctx.outputDir.filename().string();
21782197
write_build_cache(ctx.projectRoot, ctx.outputDir, r->ninjaProgram,
2179-
std::string(targetOverride));
2198+
std::string(targetOverride), fpHex);
21802199
}
21812200

21822201
mcpp::ui::finished("release", r->elapsed);
@@ -2204,15 +2223,30 @@ std::optional<int> try_fast_build(const std::filesystem::path& projectRoot,
22042223
if (!std::filesystem::exists(cachePath, ec)) return std::nullopt;
22052224

22062225
std::ifstream f(cachePath);
2207-
std::string outputDirStr, ninjaProgram, cachedTarget;
2226+
std::string outputDirStr, ninjaProgram, cachedTarget, cachedFingerprint;
22082227
if (!std::getline(f, outputDirStr) || outputDirStr.empty()) return std::nullopt;
22092228
if (!std::getline(f, ninjaProgram) || ninjaProgram.empty()) return std::nullopt;
2210-
std::getline(f, cachedTarget); // may be empty for old cache files
2229+
std::getline(f, cachedTarget); // may be empty for old cache files
2230+
std::getline(f, cachedFingerprint); // may be empty for pre-0.0.15 caches
22112231

22122232
// Reject cache if target triple changed (e.g. previous build used
22132233
// --target x86_64-linux-musl but this one is a default build).
22142234
if (cachedTarget != currentTarget) return std::nullopt;
22152235

2236+
// P1: verify fingerprint matches the outputDir basename. If someone
2237+
// switched mcpp installations (different toolchain binary), the cached
2238+
// outputDir points to a stale fingerprint directory. Detect and reject.
2239+
if (!cachedFingerprint.empty()) {
2240+
std::filesystem::path outputDir(outputDirStr);
2241+
auto dirBasename = outputDir.filename().string();
2242+
if (dirBasename != cachedFingerprint) {
2243+
// Cache is inconsistent — invalidate it.
2244+
std::error_code ec2;
2245+
std::filesystem::remove(cachePath, ec2);
2246+
return std::nullopt;
2247+
}
2248+
}
2249+
22162250
std::filesystem::path outputDir(outputDirStr);
22172251

22182252
auto ninjaPath = outputDir / "build.ninja";

0 commit comments

Comments
 (0)