Race condition: concurrent builds clobber each other's external dep clones #3

Merged
jorijnvdgraaf merged 1 commit from claude/issue-2 into master 2026-05-27 04:06:53 +02:00

View file

@ -103,9 +103,13 @@ std::string FetchGit(const GitSource& source, const fs::path& cloneDir) {
} }
} }
// Random per-invocation tmp suffix so concurrent crafter-build processes
// resolving to the same cache key don't share an in-flight `.tmp`
// directory — git's pack tempfiles would get yanked when one process
// removes or renames the dir out from under the other.
std::random_device rd;
fs::path tmpDir = cloneDir; fs::path tmpDir = cloneDir;
tmpDir += ".tmp"; tmpDir += std::format(".tmp.{:08x}{:08x}", rd(), rd());
if (fs::exists(tmpDir)) fs::remove_all(tmpDir);
if (auto err = runGit(std::format("git clone --recursive {} {}", ShellQuote(source.url), ShellQuote(tmpDir.string())))) { if (auto err = runGit(std::format("git clone --recursive {} {}", ShellQuote(source.url), ShellQuote(tmpDir.string())))) {
if (fs::exists(tmpDir)) fs::remove_all(tmpDir); if (fs::exists(tmpDir)) fs::remove_all(tmpDir);
@ -128,6 +132,10 @@ std::string FetchGit(const GitSource& source, const fs::path& cloneDir) {
fs::rename(tmpDir, cloneDir, ec); fs::rename(tmpDir, cloneDir, ec);
if (ec) { if (ec) {
fs::remove_all(tmpDir); fs::remove_all(tmpDir);
// If a concurrent build won the race, cloneDir now exists with the
// same source revision (cache key includes url+branch+commit), so
// discard our clone and reuse theirs.
if (fs::exists(cloneDir)) return "";
return std::format("rename {} -> {} failed: {}", tmpDir.string(), cloneDir.string(), ec.message()); return std::format("rename {} -> {} failed: {}", tmpDir.string(), cloneDir.string(), ec.message());
} }
return ""; return "";