All checks were successful
CI / build-test-release (pull_request) Successful in 11m50s
Two crafter-build invocations sharing XDG_CACHE_HOME used to clobber each other's writes to <cache>/<target>-<march>/std.pcm and the Crafter.Build-*.pcm modules: each LoadProject path wrote directly to the final path, so a reader could see a half-written file and die with "malformed or corrupted precompiled file: 'can't skip to bit X from Y'" (issue #14). Every BuildStdPcm / EnsureCrafterBuildPcms write now goes via <final>.tmp.<pid>.<seq> and atomic-renames into place; concurrent writers always see either the old or the new file, never torn bytes. The mingw-on- Linux std.cppm copy is per-PID for the same reason. Adds a regression test (ConcurrentCacheRace) that races four LoadProject() calls against a cold scratch cache — reproduces the race 5/5 without the fix and passes 5/5 with it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
117 lines
4.1 KiB
C++
117 lines
4.1 KiB
C++
#include <stdlib.h>
|
||
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 `<cache>/<target>-<march>/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 routes every PCM
|
||
// write through a per-process temp + atomic rename so a reader always sees
|
||
// either the old or the new file, never a half-written 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<fs::path> 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<const std::string_view>) {{\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<std::string, N> errors;
|
||
std::vector<std::thread> threads;
|
||
threads.reserve(N);
|
||
for (int i = 0; i < N; ++i) {
|
||
threads.emplace_back([&, i]() {
|
||
try {
|
||
std::array<std::string_view, 0> 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;
|
||
}
|