// 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("-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"; 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; }