169 lines
8.9 KiB
C++
169 lines
8.9 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", "share/libc++/v1/std.cppm", "share/wasi-sysroot/include/.keep"}) {
|
||
|
|
Touch(full / rel);
|
||
|
|
}
|
||
|
|
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.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.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("-Wl,-rpath,{}", (full / "lib").string())), "binaries get an rpath into 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("-Wl,-rpath,{}", (bare / "lib").string())), "the rpath follows 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";
|
||
|
|
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(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;
|
||
|
|
}
|