#include import std; import Crafter.Build; namespace fs = std::filesystem; using namespace Crafter; // Regression for the host-cache write race: when several crafter-build // invocations shared the same XDG_CACHE_HOME, two LoadProject() calls would // clobber each other's writes to `/-/std.pcm` and the // `Crafter.Build-*.pcm` host modules. The loser's clang would then read a // torn file via -fprebuilt-module-path and die with "malformed or corrupted // precompiled file: 'can't skip to bit X from Y'". The fix serializes PCM // (re)builds through an exclusive advisory lock on the cache dir: only one // builder precompiles while the rest wait and then reuse the finished file, so // no two writers ever touch the same path and a reader never sees a torn one. // // LoadProject() is the right surface to test because it runs the full host // bootstrap (BuildStdPcm + EnsureCrafterBuildPcms × the Crafter.Build-* // modules), giving every concurrent thread enough writing-to-the-same-paths // time for the race to surface. A pure BuildStdPcm test wouldn't — the std // PCM write is too short to overlap reliably between threads. namespace { fs::path FindCrafterBuildHome() { if (const char* env = std::getenv("CRAFTER_BUILD_HOME"); env && *env) { return env; } for (const char* candidate : { "/usr/local/share/crafter-build", "/usr/share/crafter-build", }) { if (fs::exists(fs::path(candidate) / "Crafter.Build.cppm")) { return candidate; } } return {}; } void WriteFile(const fs::path& p, std::string_view contents) { std::ofstream out(p); out << contents; } } int main() { fs::path home = FindCrafterBuildHome(); if (home.empty()) { std::println(std::cerr, "SKIP: no installed share/crafter-build found and CRAFTER_BUILD_HOME unset"); return 77; } setenv("CRAFTER_BUILD_HOME", home.string().c_str(), 1); fs::path scratch = fs::temp_directory_path() / "crafter-build-concurrent-cache-race"; std::error_code ec; fs::remove_all(scratch, ec); fs::create_directories(scratch); // Cold scratch cache. setenv before any thread starts so every thread's // GetCacheDir() call sees the same override. fs::path cacheDir = scratch / "cache"; fs::create_directories(cacheDir); setenv("XDG_CACHE_HOME", cacheDir.string().c_str(), 1); constexpr int N = 4; std::vector projects; for (int i = 0; i < N; ++i) { fs::path dir = scratch / std::format("p{}", i); fs::create_directories(dir); WriteFile(dir / "project.cpp", std::format( "import std;\n" "import Crafter.Build;\n" "namespace fs = std::filesystem;\n" "using namespace Crafter;\n" "extern \"C\" Configuration CrafterBuildProject(std::span) {{\n" " Configuration cfg;\n" " cfg.path = \"./\";\n" " cfg.name = \"p{}\";\n" " cfg.outputName = \"p{}\";\n" " cfg.target = HostTarget();\n" " cfg.type = ConfigurationType::Executable;\n" " return cfg;\n" "}}\n", i, i)); projects.push_back(dir / "project.cpp"); } std::array errors; std::vector threads; threads.reserve(N); for (int i = 0; i < N; ++i) { threads.emplace_back([&, i]() { try { std::array args = {}; (void)LoadProject(projects[i], args); } catch (const std::exception& e) { errors[i] = e.what(); } }); } for (std::thread& t : threads) t.join(); int failures = 0; for (int i = 0; i < N; ++i) { if (!errors[i].empty()) { std::println(std::cerr, "FAIL p{}: {}", i, errors[i]); ++failures; } } fs::remove_all(scratch, ec); if (failures > 0) { std::println(std::cerr, "{}/{} concurrent LoadProject() invocations failed (host-cache race)", failures, N); return 1; } return 0; }