SysrootStdModule runs over staged layouts that need no compiler: a sysroot with its own module source, one on a point release of the toolchain's libc++, one on the next major release with a shipped source, one nothing was shipped for, one without libc++ at all, and the two cases where a release cannot be read. The last block drives Build() against a sysroot claiming libc++ 99 and expects the one-line explanation back instead of compiler output, which is the failure the fix replaces. ToolchainResolve additionally checks that the release is read from the libc++ the module source comes from (the prefix's, an explicit libc++ install's, the system's) and where the shipped sources are looked up. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
178 lines
10 KiB
C++
178 lines
10 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 == "/usr/x86_64-w64-mingw32" && !tc.mingwSysrootExplicit, "the default mingw dir is not passed as --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" && tc.mingwSysrootExplicit, "CRAFTER_MINGW_DIR overrides the mingw dir and is passed as --sysroot");
|
|
}
|
|
{
|
|
// 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;
|
|
}
|