// mcpp.bmi_cache — cross-project persistent BMI cache (M3.2). // // Layout (per docs/26-bmi-cache.md): // $MCPP_HOME/bmi//deps//@/ // {gcm,pcm}.cache/.{gcm,pcm} // obj/.m.o + .o // manifest.txt (sentinel + file list) // // fingerprint already covers compiler / flags / stdlib / mcpp version / // dep-lock hash etc. (docs/06), so different fingerprints get independent // cache directories — no risk of stale-BMI cross-contamination. // // M4 #9: populate is wrapped in an advisory exclusive flock(2) on // /.lock so two concurrent mcpp builds can race to the same // dep cache without trashing manifest.txt (docs/26 §5.4 V2). module; export module mcpp.bmi_cache; import std; import mcpp.platform; export namespace mcpp::bmi_cache { struct CacheKey { std::filesystem::path mcppHome; std::string fingerprint; std::string indexName; // "mcpplibs" / "xim" / ... std::string packageName; // "mcpplibs.cmdline" std::string version; // "0.0.1" std::string bmiDirName = "gcm.cache"; // "gcm.cache" | "pcm.cache" std::string manifestTag = "gcm"; // "gcm" | "pcm" std::filesystem::path dir() const { return mcppHome / "bmi" / fingerprint / "deps" / indexName / std::format("{}@{}", packageName, version); } std::filesystem::path manifestFile() const { return dir() / "manifest.txt"; } std::filesystem::path bmiDir() const { return dir() / bmiDirName; } std::filesystem::path objDir() const { return dir() / "obj"; } }; // File names (basename only) that belong to one dep package's cache entry. struct DepArtifacts { std::vector bmiFiles; // basenames in bmiDir/ std::vector objFiles; // basenames in obj/ }; // Cache hit if manifest.txt exists AND every listed file is present. bool is_cached(const CacheKey& key); // Read manifest.txt → DepArtifacts. std::expected read_manifest(const CacheKey& key); // Copy missing cached files into projectTarget/{bmiDirName,obj}. Existing // project outputs are left untouched: BMIs may differ byte-for-byte between // equivalent builds, and overwriting them would dirty downstream modules. std::expected stage_into(const CacheKey& key, const std::filesystem::path& projectTargetDir); // Copy fresh build outputs from projectTarget/{bmiDirName,obj} → cache dir // and write manifest.txt last (atomic-ish sentinel). std::expected populate_from(const CacheKey& key, const std::filesystem::path& projectTargetDir, const DepArtifacts& artifacts); } // namespace mcpp::bmi_cache namespace mcpp::bmi_cache { namespace { void touch_now(const std::filesystem::path& p) { std::error_code ec; auto t = std::chrono::file_clock::now() + std::chrono::seconds(1); std::filesystem::last_write_time(p, t, ec); } bool copy_one(const std::filesystem::path& from, const std::filesystem::path& to, std::error_code& ec) { std::filesystem::create_directories(to.parent_path(), ec); std::filesystem::copy_file(from, to, std::filesystem::copy_options::overwrite_existing, ec); return !ec; } std::string serialize_manifest(std::string_view tag, const DepArtifacts& a) { std::string out = "# Auto-generated by mcpp bmi_cache. Do not edit.\n"; for (auto& g : a.bmiFiles) out += std::format("{}: {}\n", tag, g); for (auto& o : a.objFiles) out += std::format("obj: {}\n", o); return out; } std::expected parse_manifest(const std::filesystem::path& p) { std::ifstream is(p); if (!is) return std::unexpected(std::format("cannot open '{}'", p.string())); DepArtifacts a; std::string line; while (std::getline(is, line)) { while (!line.empty() && (line.back() == '\r' || line.back() == ' ')) line.pop_back(); if (line.empty() || line[0] == '#') continue; if (line.starts_with("gcm: ")) a.bmiFiles.push_back(line.substr(5)); else if (line.starts_with("pcm: ")) a.bmiFiles.push_back(line.substr(5)); else if (line.starts_with("obj: ")) a.objFiles.push_back(line.substr(5)); } return a; } } // namespace bool is_cached(const CacheKey& key) { auto mf = key.manifestFile(); if (!std::filesystem::exists(mf)) return false; auto arts = parse_manifest(mf); if (!arts) return false; // Verify every listed file actually exists on disk. for (auto& g : arts->bmiFiles) { if (!std::filesystem::exists(key.bmiDir() / g)) return false; } for (auto& o : arts->objFiles) { if (!std::filesystem::exists(key.objDir() / o)) return false; } return true; } std::expected read_manifest(const CacheKey& key) { return parse_manifest(key.manifestFile()); } std::expected stage_into(const CacheKey& key, const std::filesystem::path& projectTargetDir) { auto arts = parse_manifest(key.manifestFile()); if (!arts) return std::unexpected(arts.error()); auto projectBmi = projectTargetDir / key.bmiDirName; auto projectObj = projectTargetDir / "obj"; std::error_code ec; std::filesystem::create_directories(projectBmi, ec); std::filesystem::create_directories(projectObj, ec); for (auto& g : arts->bmiFiles) { auto from = key.bmiDir() / g; auto to = projectBmi / g; if (std::filesystem::exists(to, ec)) { ec.clear(); continue; } ec.clear(); if (!copy_one(from, to, ec)) { return std::unexpected(std::format( "stage bmi '{}': {}", g, ec.message())); } touch_now(to); } for (auto& o : arts->objFiles) { auto from = key.objDir() / o; auto to = projectObj / o; if (std::filesystem::exists(to, ec)) { ec.clear(); continue; } ec.clear(); if (!copy_one(from, to, ec)) { return std::unexpected(std::format( "stage obj '{}': {}", o, ec.message())); } touch_now(to); } return arts; } namespace { } // namespace std::expected populate_from(const CacheKey& key, const std::filesystem::path& projectTargetDir, const DepArtifacts& arts) { auto cacheDir = key.dir(); auto lock = mcpp::platform::fs::FileLock::try_acquire(cacheDir); if (!lock) { // Another writer holds the lock; treat as success (they'll do it). return {}; } auto cacheBmi = key.bmiDir(); auto cacheObj = key.objDir(); std::error_code ec; std::filesystem::create_directories(cacheBmi, ec); std::filesystem::create_directories(cacheObj, ec); auto projectBmi = projectTargetDir / key.bmiDirName; auto projectObj = projectTargetDir / "obj"; for (auto& g : arts.bmiFiles) { auto from = projectBmi / g; auto to = cacheBmi / g; if (!std::filesystem::exists(from)) { return std::unexpected(std::format( "expected build output missing: {}", from.string())); } if (!copy_one(from, to, ec)) { return std::unexpected(std::format( "populate bmi '{}': {}", g, ec.message())); } } for (auto& o : arts.objFiles) { auto from = projectObj / o; auto to = cacheObj / o; if (!std::filesystem::exists(from)) { return std::unexpected(std::format( "expected build output missing: {}", from.string())); } if (!copy_one(from, to, ec)) { return std::unexpected(std::format( "populate obj '{}': {}", o, ec.message())); } } // Write manifest.txt LAST as the sentinel — atomic via temp + rename. { auto tmp = key.manifestFile(); tmp += ".tmp"; { std::ofstream os(tmp); os << serialize_manifest(key.manifestTag, arts); } std::filesystem::rename(tmp, key.manifestFile(), ec); if (ec) { return std::unexpected(std::format( "populate manifest rename: {}", ec.message())); } } return {}; } } // namespace mcpp::bmi_cache