Skip to content

Commit 34db876

Browse files
authored
feat: multi-fingerprint build cache LRU (P3) (#37)
build_cache upgraded to multi-entry format keyed by target triple, up to 4 entries LRU. Legacy format auto-migrated.
1 parent 35c2dc6 commit 34db876

2 files changed

Lines changed: 99 additions & 38 deletions

File tree

src/build/ninja_backend.cppm

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,11 @@ std::string emit_ninja_string(const BuildPlan& plan) {
245245
// Scan rule: produce P1689 .ddi for one TU.
246246
// GCC: built-in -fdeps-format=p1689r5 flags during preprocessing.
247247
// Clang: external clang-scan-deps tool with -format=p1689.
248+
// Note: restat is intentionally NOT used here. The downstream
249+
// cxx_dyndep and cxx_module rules already have restat = 1 and
250+
// BMI preservation logic, which is sufficient to prevent
251+
// cascading rebuilds when only implementation (not interface)
252+
// changes.
248253
append("rule cxx_scan\n");
249254
if (plan.scanDepsPath.empty()) {
250255
// GCC path: compiler-integrated P1689 scanning.

src/cli.cppm

Lines changed: 94 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2102,21 +2102,85 @@ prepare_build(bool print_fingerprint,
21022102
// ─── P0: build cache for fast-path rebuilds ─────────────────────────
21032103

21042104
constexpr std::string_view kBuildCacheFile = "target/.build_cache";
2105+
constexpr int kBuildCacheMaxEntries = 4; // P3: LRU capacity
2106+
2107+
// P3: one entry per (target, fingerprint) pair.
2108+
struct BuildCacheEntry {
2109+
std::string targetTriple; // "" for default target
2110+
std::string outputDir;
2111+
std::string ninjaProgram;
2112+
std::string fingerprint; // outputDir basename
2113+
};
2114+
2115+
std::vector<BuildCacheEntry> read_build_cache(const std::filesystem::path& projectRoot) {
2116+
auto path = projectRoot / kBuildCacheFile;
2117+
std::ifstream f(path);
2118+
if (!f) return {};
2119+
2120+
std::string firstLine;
2121+
if (!std::getline(f, firstLine) || firstLine.empty()) return {};
2122+
2123+
// Detect legacy format (first line is an absolute path, not "[target=...]").
2124+
if (firstLine[0] != '[') {
2125+
// Legacy 4-line format: outputDir, ninjaProgram, target, fingerprint.
2126+
BuildCacheEntry e;
2127+
e.outputDir = firstLine;
2128+
std::getline(f, e.ninjaProgram);
2129+
std::getline(f, e.targetTriple);
2130+
std::getline(f, e.fingerprint);
2131+
if (e.outputDir.empty() || e.ninjaProgram.empty()) return {};
2132+
return {e};
2133+
}
2134+
2135+
// P3 multi-entry format: sections of [target=<triple>] + 3 lines.
2136+
std::vector<BuildCacheEntry> entries;
2137+
std::string line = firstLine;
2138+
while (true) {
2139+
// Parse [target=<triple>]
2140+
if (line.size() < 9 || !line.starts_with("[target=") || line.back() != ']')
2141+
break;
2142+
BuildCacheEntry e;
2143+
e.targetTriple = line.substr(8, line.size() - 9);
2144+
if (!std::getline(f, e.outputDir) || e.outputDir.empty()) break;
2145+
if (!std::getline(f, e.ninjaProgram) || e.ninjaProgram.empty()) break;
2146+
std::getline(f, e.fingerprint);
2147+
entries.push_back(std::move(e));
2148+
if (!std::getline(f, line)) break;
2149+
}
2150+
return entries;
2151+
}
21052152

21062153
void write_build_cache(const std::filesystem::path& projectRoot,
21072154
const std::filesystem::path& outputDir,
21082155
const std::string& ninjaProgram,
21092156
const std::string& targetTriple,
21102157
const std::string& fingerprintHex = "") {
21112158
auto path = projectRoot / kBuildCacheFile;
2159+
auto entries = read_build_cache(projectRoot);
2160+
2161+
// Remove existing entry for this target (will be re-added at front).
2162+
std::erase_if(entries, [&](const BuildCacheEntry& e) {
2163+
return e.targetTriple == targetTriple;
2164+
});
2165+
2166+
// Insert at front (MRU).
2167+
BuildCacheEntry newEntry{targetTriple, outputDir.string(), ninjaProgram, fingerprintHex};
2168+
entries.insert(entries.begin(), std::move(newEntry));
2169+
2170+
// Trim to LRU capacity.
2171+
if ((int)entries.size() > kBuildCacheMaxEntries)
2172+
entries.resize(kBuildCacheMaxEntries);
2173+
2174+
// Write P3 format.
21122175
std::error_code ec;
21132176
std::filesystem::create_directories(path.parent_path(), ec);
21142177
std::ofstream f(path, std::ios::trunc);
2115-
if (f) {
2116-
f << outputDir.string() << '\n';
2117-
f << ninjaProgram << '\n';
2118-
f << targetTriple << '\n';
2119-
f << fingerprintHex << '\n';
2178+
if (!f) return;
2179+
for (auto& e : entries) {
2180+
f << "[target=" << e.targetTriple << "]\n";
2181+
f << e.outputDir << '\n';
2182+
f << e.ninjaProgram << '\n';
2183+
f << e.fingerprint << '\n';
21202184
}
21212185
}
21222186

@@ -2177,16 +2241,16 @@ int run_build_plan(BuildContext& ctx, bool verbose, bool no_cache,
21772241

21782242
// P1.5: warn if fingerprint changed from last build (explains full rebuild).
21792243
{
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));
2244+
auto entries = read_build_cache(ctx.projectRoot);
2245+
for (auto& e : entries) {
2246+
if (e.targetTriple == targetOverride && !e.fingerprint.empty()) {
2247+
auto newFp = ctx.outputDir.filename().string();
2248+
if (e.fingerprint != newFp) {
2249+
mcpp::ui::warning(std::format(
2250+
"fingerprint changed ({} → {}), full rebuild",
2251+
e.fingerprint, newFp));
2252+
}
2253+
break;
21902254
}
21912255
}
21922256
}
@@ -2218,35 +2282,27 @@ std::optional<int> try_fast_build(const std::filesystem::path& projectRoot,
22182282
std::string_view currentTarget = "") {
22192283
if (no_cache) return std::nullopt;
22202284

2221-
auto cachePath = projectRoot / kBuildCacheFile;
2222-
std::error_code ec;
2223-
if (!std::filesystem::exists(cachePath, ec)) return std::nullopt;
2224-
2225-
std::ifstream f(cachePath);
2226-
std::string outputDirStr, ninjaProgram, cachedTarget, cachedFingerprint;
2227-
if (!std::getline(f, outputDirStr) || outputDirStr.empty()) return std::nullopt;
2228-
if (!std::getline(f, ninjaProgram) || ninjaProgram.empty()) return std::nullopt;
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
2231-
2232-
// Reject cache if target triple changed (e.g. previous build used
2233-
// --target x86_64-linux-musl but this one is a default build).
2234-
if (cachedTarget != currentTarget) return std::nullopt;
2235-
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.
2285+
// P3: read multi-entry cache and find entry matching currentTarget.
2286+
auto entries = read_build_cache(projectRoot);
2287+
const BuildCacheEntry* match = nullptr;
2288+
for (auto& e : entries) {
2289+
if (e.targetTriple == currentTarget) { match = &e; break; }
2290+
}
2291+
if (!match) return std::nullopt;
2292+
2293+
auto outputDirStr = match->outputDir;
2294+
auto ninjaProgram = match->ninjaProgram;
2295+
auto cachedFingerprint = match->fingerprint;
2296+
2297+
// P1: verify fingerprint matches the outputDir basename.
22392298
if (!cachedFingerprint.empty()) {
2240-
std::filesystem::path outputDir(outputDirStr);
2241-
auto dirBasename = outputDir.filename().string();
2299+
auto dirBasename = std::filesystem::path(outputDirStr).filename().string();
22422300
if (dirBasename != cachedFingerprint) {
2243-
// Cache is inconsistent — invalidate it.
2244-
std::error_code ec2;
2245-
std::filesystem::remove(cachePath, ec2);
22462301
return std::nullopt;
22472302
}
22482303
}
22492304

2305+
std::error_code ec;
22502306
std::filesystem::path outputDir(outputDirStr);
22512307

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

0 commit comments

Comments
 (0)