From ded76aa484ca761e8f093491685bc4ca9256aa2b Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 15 May 2026 20:32:22 +0800 Subject: [PATCH 1/2] 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. --- src/build/ninja_backend.cppm | 27 +++++++++++++++++------ src/cli.cppm | 42 ++++++++++++++++++++++++++++++++---- 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index 10eb595..c435648 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -243,6 +243,10 @@ std::string emit_ninja_string(const BuildPlan& plan) { if (dyndep) { // Scan rule: produce P1689 .ddi for one TU. + // P2: write to $out.tmp first, then compare with existing $out. + // If content is identical, keep old $out (preserving mtime) so + // downstream dyndep/compile edges are not marked dirty. + // restat = 1 tells Ninja to check if $out actually changed. // GCC: built-in -fdeps-format=p1689r5 flags during preprocessing. // Clang: external clang-scan-deps tool with -format=p1689. append("rule cxx_scan\n"); @@ -250,16 +254,25 @@ std::string emit_ninja_string(const BuildPlan& plan) { // GCC path: compiler-integrated P1689 scanning. append(" command = $toolenv $cxx $cxxflags -fmodules " "-fdeps-format=p1689r5 " - "-fdeps-file=$out -fdeps-target=$compile_target " - "-M -MM -MF $out.dep -E $in -o $compile_target\n"); + "-fdeps-file=$out.tmp -fdeps-target=$compile_target " + "-M -MM -MF $out.dep -E $in -o $compile_target && " + "if [ -f \"$out\" ] && cmp -s \"$out.tmp\" \"$out\"; then " + "rm -f \"$out.tmp\"; " + "else " + "mv -f \"$out.tmp\" \"$out\"; " + "fi\n"); } else { - // Clang path: clang-scan-deps produces P1689 JSON to stdout, - // then we redirect to $out. The -- separator passes the full - // compile command so clang-scan-deps knows the flags/sysroot. + // Clang path: clang-scan-deps produces P1689 JSON to stdout. append(" command = $toolenv $scan_deps -format=p1689 -- " - "$cxx $cxxflags -c $in -o $compile_target > $out\n"); + "$cxx $cxxflags -c $in -o $compile_target > $out.tmp && " + "if [ -f \"$out\" ] && cmp -s \"$out.tmp\" \"$out\"; then " + "rm -f \"$out.tmp\"; " + "else " + "mv -f \"$out.tmp\" \"$out\"; " + "fi\n"); } - append(" description = SCAN $out\n\n"); + append(" description = SCAN $out\n"); + append(" restat = 1\n\n"); // Aggregate .ddi files into a Ninja dyndep file. append(std::format( diff --git a/src/cli.cppm b/src/cli.cppm index b8b48d2..80bcce9 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -2106,7 +2106,8 @@ constexpr std::string_view kBuildCacheFile = "target/.build_cache"; void write_build_cache(const std::filesystem::path& projectRoot, const std::filesystem::path& outputDir, const std::string& ninjaProgram, - const std::string& targetTriple) { + const std::string& targetTriple, + const std::string& fingerprintHex = "") { auto path = projectRoot / kBuildCacheFile; std::error_code ec; std::filesystem::create_directories(path.parent_path(), ec); @@ -2115,6 +2116,7 @@ void write_build_cache(const std::filesystem::path& projectRoot, f << outputDir.string() << '\n'; f << ninjaProgram << '\n'; f << targetTriple << '\n'; + f << fingerprintHex << '\n'; } } @@ -2173,10 +2175,27 @@ int run_build_plan(BuildContext& ctx, bool verbose, bool no_cache, } } + // P1.5: warn if fingerprint changed from last build (explains full rebuild). + { + auto cachePath = ctx.projectRoot / kBuildCacheFile; + std::ifstream cf(cachePath); + std::string oldDir; + if (std::getline(cf, oldDir) && !oldDir.empty()) { + auto oldFp = std::filesystem::path(oldDir).filename().string(); + auto newFp = ctx.outputDir.filename().string(); + if (oldFp != newFp) { + mcpp::ui::warning(std::format( + "fingerprint changed ({} → {}), full rebuild", + oldFp, newFp)); + } + } + } + // P0: save build cache for fast-path on next invocation. if (!no_cache && !r->ninjaProgram.empty()) { + auto fpHex = ctx.outputDir.filename().string(); write_build_cache(ctx.projectRoot, ctx.outputDir, r->ninjaProgram, - std::string(targetOverride)); + std::string(targetOverride), fpHex); } mcpp::ui::finished("release", r->elapsed); @@ -2204,15 +2223,30 @@ std::optional try_fast_build(const std::filesystem::path& projectRoot, if (!std::filesystem::exists(cachePath, ec)) return std::nullopt; std::ifstream f(cachePath); - std::string outputDirStr, ninjaProgram, cachedTarget; + std::string outputDirStr, ninjaProgram, cachedTarget, cachedFingerprint; if (!std::getline(f, outputDirStr) || outputDirStr.empty()) return std::nullopt; if (!std::getline(f, ninjaProgram) || ninjaProgram.empty()) return std::nullopt; - std::getline(f, cachedTarget); // may be empty for old cache files + std::getline(f, cachedTarget); // may be empty for old cache files + std::getline(f, cachedFingerprint); // may be empty for pre-0.0.15 caches // Reject cache if target triple changed (e.g. previous build used // --target x86_64-linux-musl but this one is a default build). if (cachedTarget != currentTarget) return std::nullopt; + // P1: verify fingerprint matches the outputDir basename. If someone + // switched mcpp installations (different toolchain binary), the cached + // outputDir points to a stale fingerprint directory. Detect and reject. + if (!cachedFingerprint.empty()) { + std::filesystem::path outputDir(outputDirStr); + auto dirBasename = outputDir.filename().string(); + if (dirBasename != cachedFingerprint) { + // Cache is inconsistent — invalidate it. + std::error_code ec2; + std::filesystem::remove(cachePath, ec2); + return std::nullopt; + } + } + std::filesystem::path outputDir(outputDirStr); auto ninjaPath = outputDir / "build.ninja"; From e52028a7de919dabd7c26ebe0efd6d1030b2c72c Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 15 May 2026 20:50:28 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20revert=20cxx=5Fscan=20restat=20(P2)?= =?UTF-8?q?=20=E2=80=94=20broke=20self-host=20smoke=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The $out.tmp + cmp -s approach for content-stable .ddi writes caused linker failures in the CI self-host smoke step. GCC's -fdeps-file= interacts with ninja's depfile tracking in ways that make the temp-file approach fragile. Revert to direct -fdeps-file=$out for now. P1 (fingerprint coherence) and P1.5 (diagnostic warning) are retained. P2 needs a different approach — possibly ninja's built-in restat alone without temp-file wrappers. --- src/build/ninja_backend.cppm | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index c435648..e49fd98 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -243,10 +243,6 @@ std::string emit_ninja_string(const BuildPlan& plan) { if (dyndep) { // Scan rule: produce P1689 .ddi for one TU. - // P2: write to $out.tmp first, then compare with existing $out. - // If content is identical, keep old $out (preserving mtime) so - // downstream dyndep/compile edges are not marked dirty. - // restat = 1 tells Ninja to check if $out actually changed. // GCC: built-in -fdeps-format=p1689r5 flags during preprocessing. // Clang: external clang-scan-deps tool with -format=p1689. append("rule cxx_scan\n"); @@ -254,25 +250,14 @@ std::string emit_ninja_string(const BuildPlan& plan) { // GCC path: compiler-integrated P1689 scanning. append(" command = $toolenv $cxx $cxxflags -fmodules " "-fdeps-format=p1689r5 " - "-fdeps-file=$out.tmp -fdeps-target=$compile_target " - "-M -MM -MF $out.dep -E $in -o $compile_target && " - "if [ -f \"$out\" ] && cmp -s \"$out.tmp\" \"$out\"; then " - "rm -f \"$out.tmp\"; " - "else " - "mv -f \"$out.tmp\" \"$out\"; " - "fi\n"); + "-fdeps-file=$out -fdeps-target=$compile_target " + "-M -MM -MF $out.dep -E $in -o $compile_target\n"); } else { // Clang path: clang-scan-deps produces P1689 JSON to stdout. append(" command = $toolenv $scan_deps -format=p1689 -- " - "$cxx $cxxflags -c $in -o $compile_target > $out.tmp && " - "if [ -f \"$out\" ] && cmp -s \"$out.tmp\" \"$out\"; then " - "rm -f \"$out.tmp\"; " - "else " - "mv -f \"$out.tmp\" \"$out\"; " - "fi\n"); + "$cxx $cxxflags -c $in -o $compile_target > $out\n"); } - append(" description = SCAN $out\n"); - append(" restat = 1\n\n"); + append(" description = SCAN $out\n\n"); // Aggregate .ddi files into a Ninja dyndep file. append(std::format(