Some checks failed
Flatpak / flatpak (push) Failing after 1m12s
- 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>
192 lines
11 KiB
C++
192 lines
11 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;
|
|
|
|
// ResolveToolchain is pure path resolution, so this test needs no compiler:
|
|
// it stages fake prefixes under the temp dir and checks which one wins,
|
|
// what is picked up beside it, and what the defaults are when nothing is.
|
|
// The last block runs the real GetToolchain() once, which is the only part
|
|
// that spawns clang.
|
|
namespace {
|
|
std::int32_t Failures = 0;
|
|
|
|
void Check(bool cond, std::string_view msg) {
|
|
if (!cond) {
|
|
std::println(std::cerr, "FAIL: {}", msg);
|
|
++Failures;
|
|
}
|
|
}
|
|
|
|
void Touch(const fs::path& file) {
|
|
fs::create_directories(file.parent_path());
|
|
std::ofstream(file) << "";
|
|
}
|
|
|
|
bool Contains(std::string_view haystack, std::string_view needle) {
|
|
return haystack.find(needle) != std::string_view::npos;
|
|
}
|
|
}
|
|
|
|
int main() {
|
|
const fs::path scratch = fs::temp_directory_path() / "crafter-build-toolchain-resolve";
|
|
std::error_code ec;
|
|
fs::remove_all(scratch, ec);
|
|
fs::create_directories(scratch);
|
|
|
|
// A prefix with everything crafter-build looks for beside the compiler,
|
|
// and one with the compiler alone.
|
|
const fs::path full = scratch / "full";
|
|
for (const char* rel : {"bin/clang++", "bin/clang", "bin/llvm-ar", "lib/libclang.so", "include/clang-c/Index.h", "include/c++/v1/__config", "share/libc++/v1/std.cppm", "share/wasi-sysroot/include/.keep"}) {
|
|
Touch(full / rel);
|
|
}
|
|
// The prefix's libc++ headers state its release.
|
|
std::ofstream(full / "include/c++/v1/__config") << "# define _LIBCPP_VERSION 230102\n";
|
|
const fs::path bare = scratch / "bare";
|
|
Touch(bare / "bin/clang++");
|
|
|
|
{
|
|
// Nothing set and nothing beside crafter-build: bare names on PATH
|
|
// and the system locations.
|
|
Toolchain tc = ResolveToolchain({});
|
|
Check(tc.error.empty(), "the PATH fallback is not an error");
|
|
Check(tc.prefix.empty(), "the PATH fallback has no prefix");
|
|
Check(tc.clangxx == "clang++" && tc.clang == "clang" && tc.llvmAr == "llvm-ar", "the PATH fallback uses bare tool names");
|
|
Check(!tc.libclang.has_value(), "the PATH fallback searches libclang by name");
|
|
Check(tc.llvmIncludeDir.empty(), "the PATH fallback adds no include dir");
|
|
Check(tc.stdCppm == "/usr/share/libc++/v1/std.cppm", "the system std module source is the default");
|
|
Check(tc.libcxxVersion == LibcxxVersion("/usr/include/c++/v1"), "the system libc++ headers state the release of the system module source");
|
|
Check(tc.stdModulesDir.empty(), "no crafter-build prefix, no shipped module sources");
|
|
Check(tc.wasiSysroot == "/usr/share/wasi-sysroot", "the system wasi sysroot is the default");
|
|
Check(tc.mingwSysroot.empty(), "outside a sandbox, clang finds mingw-w64 itself: no --sysroot");
|
|
Check(tc.LibcxxCompileFlags().empty() && tc.LibcxxLinkFlags().empty(), "the PATH fallback adds no libc++ flags");
|
|
}
|
|
{
|
|
// A toolchain installed beside crafter-build wins over PATH, and
|
|
// everything found beside it is used.
|
|
ToolchainInputs in;
|
|
in.crafterPrefix = full;
|
|
Toolchain tc = ResolveToolchain(in);
|
|
Check(tc.prefix == full, "the prefix beside crafter-build is picked");
|
|
Check(tc.clangxx == full / "bin/clang++" && tc.clang == full / "bin/clang" && tc.llvmAr == full / "bin/llvm-ar", "the tools come from that prefix");
|
|
Check(tc.libclang == full / "lib/libclang.so", "libclang beside the compiler is pinned");
|
|
Check(tc.llvmIncludeDir == full / "include", "clang-c headers beside the compiler are added to the include path");
|
|
Check(tc.stdCppm == full / "share/libc++/v1/std.cppm", "the prefix's std module source is used");
|
|
Check(tc.libcxxVersion == 230102, "the prefix's libc++ headers state its release");
|
|
Check(tc.stdModulesDir == full / "share/crafter-build/libc++-modules", "module sources for other releases are looked up in crafter-build's own share directory");
|
|
Check(tc.wasiSysroot == full / "share/wasi-sysroot", "the prefix's wasi sysroot is used");
|
|
Check(tc.LibcxxCompileFlags().empty(), "the compiler finds its own libc++ headers without flags");
|
|
Check(Contains(tc.LibcxxLinkFlags(), std::format("-L{}", (full / "lib").string())) && Contains(tc.LibcxxLinkFlags(), std::format("-Wl,-rpath,{}", (full / "lib").string())), "binaries get -L and an rpath for the prefix's lib dir");
|
|
}
|
|
{
|
|
ToolchainInputs in;
|
|
in.crafterPrefix = scratch / "nothing-here";
|
|
Toolchain tc = ResolveToolchain(in);
|
|
Check(tc.prefix.empty() && tc.clangxx == "clang++", "a crafter-build prefix without clang++ falls back to PATH");
|
|
}
|
|
{
|
|
// The environment wins over the prefix beside crafter-build. A
|
|
// prefix with nothing but the compiler keeps the system defaults for
|
|
// everything else.
|
|
ToolchainInputs in;
|
|
in.crafterPrefix = full;
|
|
in.env["CRAFTER_BUILD_LLVM_DIR"] = bare.string();
|
|
Toolchain tc = ResolveToolchain(in);
|
|
Check(tc.error.empty(), "an explicit prefix with a clang++ is accepted");
|
|
Check(tc.prefix == bare, "CRAFTER_BUILD_LLVM_DIR wins over the prefix beside crafter-build");
|
|
Check(!tc.libclang.has_value(), "no libclang.so in the prefix: search by name");
|
|
Check(tc.llvmIncludeDir.empty(), "no clang-c headers in the prefix: nothing added");
|
|
Check(tc.stdCppm == "/usr/share/libc++/v1/std.cppm", "no std module in the prefix: the system copy");
|
|
Check(tc.wasiSysroot == "/usr/share/wasi-sysroot", "no wasi sysroot in the prefix: the system one");
|
|
Check(Contains(tc.LibcxxLinkFlags(), std::format("-L{}", (bare / "lib").string())) && Contains(tc.LibcxxLinkFlags(), std::format("-Wl,-rpath,{}", (bare / "lib").string())), "the link flags follow the explicit prefix");
|
|
}
|
|
{
|
|
ToolchainInputs in;
|
|
in.env["CRAFTER_BUILD_LLVM_DIR"] = (scratch / "missing").string();
|
|
Toolchain tc = ResolveToolchain(in);
|
|
Check(!tc.error.empty() && Contains(tc.error, "CRAFTER_BUILD_LLVM_DIR"), "an explicit prefix without clang++ is an error, not a fallback");
|
|
}
|
|
{
|
|
ToolchainInputs in;
|
|
in.env["CRAFTER_BUILD_LLVM_DIR"] = "/usr";
|
|
Toolchain tc = ResolveToolchain(in);
|
|
Check(tc.LibcxxLinkFlags().empty(), "the system prefix needs no rpath");
|
|
Check(tc.llvmIncludeDir.empty(), "the system prefix needs no include dir");
|
|
}
|
|
{
|
|
ToolchainInputs in;
|
|
in.crafterPrefix = full;
|
|
in.env["CRAFTER_BUILD_LIBCLANG"] = "/nonexistent/libclang.so";
|
|
Check(ResolveToolchain(in).libclang == fs::path("/nonexistent/libclang.so"), "CRAFTER_BUILD_LIBCLANG is taken verbatim, even when it does not exist");
|
|
}
|
|
{
|
|
// An explicit libc++ install: headers, library and std module all
|
|
// come from it, whatever the compiler would have used.
|
|
const fs::path libcxx = scratch / "libcxx";
|
|
fs::create_directories(libcxx / "include/c++/v1");
|
|
std::ofstream(libcxx / "include/c++/v1/__config") << "# define _LIBCPP_VERSION 210108\n";
|
|
ToolchainInputs in;
|
|
in.crafterPrefix = full;
|
|
in.env["CRAFTER_BUILD_LIBCXX_DIR"] = libcxx.string();
|
|
Toolchain tc = ResolveToolchain(in);
|
|
Check(tc.libcxxDir == libcxx, "CRAFTER_BUILD_LIBCXX_DIR is recorded");
|
|
Check(tc.stdCppm == libcxx / "share/libc++/v1/std.cppm", "the std module source follows the explicit libc++");
|
|
Check(tc.libcxxVersion == 210108, "the release is read from the explicit libc++, not the compiler's");
|
|
Check(Contains(tc.LibcxxCompileFlags(), std::format("-nostdinc++ -isystem {}", (libcxx / "include/c++/v1").string())), "compiles use the explicit libc++ headers");
|
|
Check(Contains(tc.LibcxxLinkFlags(), std::format("-L{}", (libcxx / "lib").string())) && Contains(tc.LibcxxLinkFlags(), std::format("-Wl,-rpath,{}", (libcxx / "lib").string())), "links use the explicit libc++ library dir, with an rpath");
|
|
}
|
|
{
|
|
ToolchainInputs in;
|
|
in.env["CRAFTER_BUILD_WASI_SYSROOT"] = "/opt/wasi";
|
|
in.env["CRAFTER_MINGW_DIR"] = "/opt/mingw";
|
|
Toolchain tc = ResolveToolchain(in);
|
|
Check(tc.wasiSysroot == "/opt/wasi", "CRAFTER_BUILD_WASI_SYSROOT overrides the wasi sysroot");
|
|
Check(tc.mingwSysroot == "/opt/mingw", "CRAFTER_MINGW_DIR names the mingw prefix passed as --sysroot");
|
|
}
|
|
{
|
|
// Sandboxed, the host's /usr stands in for the missing mingw-w64,
|
|
// unless CRAFTER_MINGW_DIR says otherwise.
|
|
const fs::path host = fs::temp_directory_path() / "crafter-build-toolchain-host";
|
|
fs::remove_all(host);
|
|
ToolchainInputs in;
|
|
in.hostRoot = host;
|
|
Check(ResolveToolchain(in).mingwSysroot.empty(), "a host without mingw-w64 gives no --sysroot");
|
|
fs::create_directories(host / "usr" / "x86_64-w64-mingw32");
|
|
Check(ResolveToolchain(in).mingwSysroot == host / "usr", "a host with mingw-w64 gives its /usr as the mingw prefix");
|
|
in.env["CRAFTER_MINGW_DIR"] = "/opt/mingw";
|
|
Check(ResolveToolchain(in).mingwSysroot == "/opt/mingw", "CRAFTER_MINGW_DIR wins over the host's");
|
|
fs::remove_all(host);
|
|
}
|
|
{
|
|
// The cache key separates toolchains that would build different
|
|
// PCMs and nothing else.
|
|
Toolchain a = ResolveToolchain({});
|
|
a.clangVersion = "clang version 22.1.8";
|
|
Toolchain b = a;
|
|
b.clangVersion = "Arch Linux clang version 22.1.9";
|
|
Toolchain c = a;
|
|
c.libcxxDir = "/opt/libcxx";
|
|
Toolchain d = ResolveToolchain({});
|
|
d.clangVersion = a.clangVersion;
|
|
Check(a.Key().starts_with("22.1.8-"), std::format("the key starts with the short clang version (got '{}')", a.Key()));
|
|
Check(b.Key().starts_with("22.1.9-"), "the short version is parsed past a distro prefix");
|
|
Check(a.Key() != b.Key(), "a different clang version changes the key");
|
|
Check(a.Key() != c.Key(), "a different libc++ location changes the key");
|
|
Check(a.Key() == d.Key(), "the key is deterministic");
|
|
}
|
|
{
|
|
// The real toolchain on this machine resolves and the probes work;
|
|
// HostTarget is the probe's answer.
|
|
const Toolchain& real = GetToolchain();
|
|
Check(real.error.empty(), std::format("the real toolchain resolves (error: '{}')", real.error));
|
|
Check(!real.hostTriple.empty() && real.hostTriple == HostTarget(), "HostTarget comes from the toolchain probe");
|
|
Check(!real.clangVersion.empty(), "the clang version was probed");
|
|
Check(!real.Key().starts_with("unknown-"), std::format("the real clang version parses (got '{}')", real.Key()));
|
|
}
|
|
|
|
fs::remove_all(scratch, ec);
|
|
return Failures == 0 ? 0 : 1;
|
|
}
|