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;
|
|||
|
|
}
|