Crafter.Build/tests/GitSubmodules/main.cpp
Jorijn van der Graaf 653dd0bb9b
Some checks failed
Flatpak / flatpak (push) Failing after 1m12s
fix: rebuild and relink on what changed, submodules at their revisions, and mingw found where it is
- Shaders record the files glslang opened (a depfile) and their settings
  (source path, stage, entry point, defines, include directories) in the
  build directory, and rebuild when an included file or a setting changes,
  not only the shader's own source.
- An executable relinks when its link command changed, not only when an
  object is newer: a link flag in project.cpp, or a dependency's library
  that moved (an ExternalDependency built into a new cache directory). The
  command is recorded beside the objects; its flags come from ordered sets,
  so the order the dependency threads finish in can't change it.
- An external's newest artifact counts its libDirs and versioned .so names.
- Submodules are checked out at the revisions the fetched commit records,
  from the URLs its .gitmodules gives, after every clone, switch, checkout
  and pull, so a pinned commit builds against its own submodules, and one
  moved by hand is put back; with no .gitmodules, git isn't asked.
- Each external clone's fetch and build hold a file lock: parallel builds
  pulled into one clone at once ("cannot lock ref").
- mingw:
  - -stdlib=libstdc++ always;
  - the std module's source is found through libstdc++.modules.json in the
    mingw toolchain's own library directories (the generic lib/ holds the
    host's Linux one), asking the compiler once a run;
  - a toolchain that isn't there is a build error saying where it looked,
    not std::terminate;
  - CRAFTER_MINGW_DIR names the prefix, the directory holding
    x86_64-w64-mingw32/ and lib/gcc/;
  - inside the Flatpak the host's /usr mingw is used.
- RunScript's exit-status check reads as the one range it is.
- Tests: ConcurrentExternalFetch, ExternalArtifactTime, GitSubmodules,
  LinkFlagsChange, MingwMissing, ShaderIncludeChange; ShaderCompile and
  ToolchainResolve extended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 20:20:29 +02:00

128 lines
6.5 KiB
C++

// 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;
// A fetched dependency's submodules have to match the revision it was fetched
// at. They used to come from the remote's default branch: a fresh clone was
// `git clone --recursive`, which checks out the default branch's submodules,
// and the `switch` or `checkout` to the requested branch or commit that came
// after it left them where they were. A branch that was pulled forward kept
// the submodules of its old tip as well. So pinning msquic to a release built
// that release against the main branch's quictls.
//
// The test makes a repository whose submodule moves between two of its
// commits, then fetches it four ways through GitFetch:
// - pinned to the older commit;
// - by a branch at the older commit;
// - by a branch that is then moved to the newer commit and fetched again;
// - pinned again after the cached clone's submodule was checked out
// elsewhere, as an older crafter-build left it.
// 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;
}
}
// Runs git in `dir`; the output, or an exception naming the command.
std::string 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));
while (!result.output.empty() && (result.output.back() == '\n' || result.output.back() == '\r')) result.output.pop_back();
return result.output;
}
void Write(const fs::path& file, std::string_view text) {
std::ofstream(file, std::ios::binary | std::ios::trunc) << text;
}
std::string Read(const fs::path& file) {
std::ifstream stream(file, std::ios::binary);
return std::string(std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>());
}
}
int main() {
const fs::path scratch = fs::temp_directory_path() / "crafter-build-git-submodules";
fs::remove_all(scratch);
fs::create_directories(scratch);
setenv("XDG_CACHE_HOME", (scratch / "cache").c_str(), 1);
// A known git: no user or system configuration, an identity for the
// commits, and file:// allowed for submodules (git refuses it by default),
// also for the git that GitFetch runs.
setenv("GIT_CONFIG_NOSYSTEM", "1", 1);
setenv("GIT_CONFIG_GLOBAL", "/dev/null", 1);
setenv("GIT_CONFIG_COUNT", "1", 1);
setenv("GIT_CONFIG_KEY_0", "protocol.file.allow", 1);
setenv("GIT_CONFIG_VALUE_0", "always", 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);
// The submodule: "one", then "two".
const fs::path sub = scratch / "sub";
fs::create_directories(sub);
Git(sub, "init -q -b main");
Write(sub / "value.txt", "one");
Git(sub, "add value.txt");
Git(sub, "commit -q -m one");
const std::string one = Git(sub, "rev-parse HEAD");
Write(sub / "value.txt", "two");
Git(sub, "commit -q -am two");
const std::string two = Git(sub, "rev-parse HEAD");
// The superproject: `older` has the submodule at "one", main at "two".
const fs::path super = scratch / "super";
fs::create_directories(super);
Git(super, "init -q -b main");
Git(super, std::format("submodule --quiet add {} lib", ShellQuote(std::format("file://{}", sub.string()))));
Git(super / "lib", std::format("checkout -q {}", one));
Git(super, "add lib");
Git(super, "commit -q -m older");
const std::string older = Git(super, "rev-parse HEAD");
Git(super, "branch older");
Git(super, "branch track");
Git(super / "lib", std::format("checkout -q {}", two));
Git(super, "add lib");
Git(super, "commit -q -m newer");
const std::string url = std::format("file://{}", super.string());
// ── Pinned to the older commit ───────────────────────────────────────────
const fs::path pinned = GitFetch({ .url = url, .commit = older });
Check(Read(pinned / "lib" / "value.txt") == "one", std::format("a commit pin gets that commit's submodule (lib says {})", Read(pinned / "lib" / "value.txt")));
// ── By a branch at the older commit ──────────────────────────────────────
const fs::path branch = GitFetch({ .url = url, .branch = "older" });
Check(Read(branch / "lib" / "value.txt") == "one", std::format("a branch gets its own tip's submodule (lib says {})", Read(branch / "lib" / "value.txt")));
// ── A tracked branch moving forward ──────────────────────────────────────
const fs::path tracked = GitFetch({ .url = url, .branch = "track" });
Check(Read(tracked / "lib" / "value.txt") == "one", "a tracked branch starts with its tip's submodule");
Git(super, "branch -f track main");
const fs::path pulled = GitFetch({ .url = url, .branch = "track" });
Check(pulled == tracked && Read(pulled / "lib" / "value.txt") == "two", std::format("pulling it forward moves the submodule along (lib says {})", Read(pulled / "lib" / "value.txt")));
// ── A cached clone with its submodule out of step ────────────────────────
Git(pinned / "lib", std::format("checkout -q {}", two));
const fs::path repaired = GitFetch({ .url = url, .commit = older });
Check(repaired == pinned && Read(repaired / "lib" / "value.txt") == "one", std::format("a cached clone at the pin gets its submodule put back (lib says {})", Read(repaired / "lib" / "value.txt")));
std::error_code ec;
fs::remove_all(scratch, ec);
if (failures > 0) {
std::println(std::cerr, "{} assertions failed", failures);
return 1;
}
return 0;
}