// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® import std; import Crafter.Build; namespace fs = std::filesystem; using namespace Crafter; // Builds that run at the same time share their dependencies' clones in the // cache, and two crafter-builds can run at once. Fetching a clone that // already exists ran `git pull` (and the CMake build) in it with nothing to // keep a second fetch out, and the second git fails on the first one's // index.lock. A clone's fetch and build now hold a lock of their own. Within // one run, the configurations that declare the same dependency share one // fetch of it instead (SharedExternalFetch). // // Eight threads fetch the same tracked branch of a local repository at // once, a few rounds over, the branch moving between rounds so every pull // has work to do; every fetch has to succeed. Each thread fetches through // GitFetch and through BuildExternal, which every build of an // ExternalDependency calls, each into a clone of its own. Everything is // local; XDG_CACHE_HOME keeps the clones out of the real cache. extern "C" int setenv(const char* name, const char* value, int overwrite); namespace { std::int32_t failures = 0; void Check(bool cond, std::string_view msg) { if (!cond) { std::println(std::cerr, "FAIL: {}", msg); ++failures; } } void Git(const fs::path& dir, std::string_view args) { const std::string command = std::format("git -C {} {}", ShellQuote(dir.string()), args); auto result = RunCommandWithTimeout(command, std::chrono::seconds(60)); if (result.exitCode != 0) throw std::runtime_error(std::format("{} failed: {}", command, result.output)); } void Commit(const fs::path& repo, std::uint32_t round) { std::ofstream(repo / "round.txt", std::ios::binary | std::ios::trunc) << round; Git(repo, "add round.txt"); Git(repo, std::format("commit -q -m round-{}", round)); } std::uint32_t Round(const fs::path& clone) { std::ifstream in(clone / "round.txt"); std::uint32_t round = 0; in >> round; return round; } } int main() { const fs::path scratch = fs::temp_directory_path() / "crafter-build-concurrent-external-fetch"; fs::remove_all(scratch); fs::create_directories(scratch); setenv("XDG_CACHE_HOME", (scratch / "cache").c_str(), 1); setenv("GIT_CONFIG_NOSYSTEM", "1", 1); setenv("GIT_CONFIG_GLOBAL", "/dev/null", 1); for (const char* name : { "GIT_AUTHOR_NAME", "GIT_COMMITTER_NAME" }) setenv(name, "crafter-build test", 1); for (const char* name : { "GIT_AUTHOR_EMAIL", "GIT_COMMITTER_EMAIL" }) setenv(name, "test@example.invalid", 1); const fs::path repo = scratch / "shared"; fs::create_directories(repo); Git(repo, "init -q -b main"); Commit(repo, 0); const GitSource source { .url = std::format("file://{}", repo.string()), .branch = "main" }; const fs::path clone = GitFetch(source); // A clone of its own: BuildExternal keys the cache differently. ExternalDependency dep; dep.name = "shared"; dep.source = source; const std::string cloned = BuildExternal(dep, HostTarget()); Check(cloned.empty(), std::format("BuildExternal clones the repository: {}", cloned)); constexpr std::uint32_t threads = 8; constexpr std::uint32_t rounds = 4; std::vector errors; std::mutex errorsMutex; for (std::uint32_t round = 1; round <= rounds; ++round) { Commit(repo, round); std::vector fetchers; for (std::uint32_t i = 0; i < threads; ++i) { fetchers.emplace_back([&] { std::string error; try { (void)GitFetch(source); error = BuildExternal(dep, HostTarget()); } catch (const std::exception& e) { error = e.what(); } if (error.empty()) return; std::lock_guard lock(errorsMutex); errors.push_back(std::move(error)); }); } } Check(errors.empty(), std::format("{} threads fetching at once, {} rounds: every fetch succeeds ({} failed{}{})", threads, rounds, errors.size(), errors.empty() ? "" : ", the first: ", errors.empty() ? "" : errors.front())); Check(Round(clone) == rounds, std::format("and GitFetch's clone ends at the last round ({})", Round(clone))); const fs::path externalClone = ExternalCloneDir(dep, HostTarget()); Check(Round(externalClone) == rounds, std::format("and so does BuildExternal's ({})", Round(externalClone))); std::error_code ec; fs::remove_all(scratch, ec); if (failures > 0) { std::println(std::cerr, "{} assertions failed", failures); return 1; } return 0; }