The first fix showed clang every mingw prefix through a view of its mingw-w64 parts. That broke a prefix that holds mingw-w64 alone, where Windows libraries installed with --prefix land in include/ and lib/: with CRAFTER_MINGW_DIR=/opt/mingw, <SDL2/SDL.h> and -lSDL2 were no longer found. Such a prefix is clang's sysroot whole again. Only a prefix that also holds a Linux system gets the view (Toolchain::mingwPrefixIsSystem): the host's /usr inside the Flatpak, whatever its include/ holds, and a prefix whose include/ has a C library's features.h, which every glibc and musl header includes and mingw-w64 has no use for, as CRAFTER_MINGW_DIR=/usr has. MakeMingwSysroot makes the view in the cache, or in $XDG_RUNTIME_DIR/crafter.build when the cache's filesystem takes no symbolic links (vfat, exFAT, CIFS without unix extensions): the XDG spec promises them there, and nothing else in the cache needs them. Its path is absolute, because CMake runs the compiler from directories of its own and a relative cache did not resolve from there. The prefix is resolved once, absolute and lexically normal, so /opt/mingw/ and /opt/mingw are one prefix and not two builds. clang names a header by the path it found it at, so every diagnostic from a viewed mingw-w64 header named a hashed cache directory. RunBuildCommand now writes paths into the view as paths into the prefix, as BuildStdPcm did for its manifest messages already. Also from the review of PR #44: SysrootCompileFlags builds a mingw command, which can make a view, so it redirects the cache like the other tests; the manifest filter's comment names what it still keeps out; the C path asks MingwSysroot() once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
220 lines
13 KiB
C++
220 lines
13 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.mingwPrefix.empty(), "outside a sandbox, clang finds mingw-w64 itself: no prefix");
|
|
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.mingwPrefix == "/opt/mingw", "CRAFTER_MINGW_DIR names the mingw prefix");
|
|
in.env["CRAFTER_MINGW_DIR"] = "mingw";
|
|
Check(ResolveToolchain(in).mingwPrefix == fs::absolute("mingw"), "a relative CRAFTER_MINGW_DIR is resolved against the working directory once, when the toolchain is");
|
|
// The prefix names directories, so one directory has one spelling:
|
|
// otherwise a trailing slash rebuilds every mingw build for nothing.
|
|
for (const char* spelling : {"/opt/mingw/", "/opt//mingw", "/opt/./mingw", "/opt/x/../mingw"}) {
|
|
in.env["CRAFTER_MINGW_DIR"] = spelling;
|
|
Check(ResolveToolchain(in).mingwPrefix == "/opt/mingw", std::format("{} is spelled /opt/mingw (got '{}')", spelling, ResolveToolchain(in).mingwPrefix.string()));
|
|
}
|
|
}
|
|
{
|
|
// A prefix that holds a Linux system's C library, as /usr does, is
|
|
// shown to clang through a view; one that holds mingw-w64 alone is
|
|
// the sysroot itself, Windows libraries in its include/ and lib/
|
|
// included.
|
|
const fs::path staged = fs::temp_directory_path() / "crafter-build-toolchain-mingw-prefixes";
|
|
fs::remove_all(staged);
|
|
Touch(staged / "system" / "include" / "features.h");
|
|
Touch(staged / "system" / "x86_64-w64-mingw32" / "include" / "_mingw.h");
|
|
Touch(staged / "dedicated" / "include" / "SDL2" / "SDL.h");
|
|
Touch(staged / "dedicated" / "x86_64-w64-mingw32" / "include" / "_mingw.h");
|
|
ToolchainInputs in;
|
|
in.env["CRAFTER_MINGW_DIR"] = (staged / "system").string();
|
|
Check(ResolveToolchain(in).mingwPrefixIsSystem, "a prefix with a C library's features.h holds a Linux system");
|
|
in.env["CRAFTER_MINGW_DIR"] = (staged / "dedicated").string();
|
|
Check(!ResolveToolchain(in).mingwPrefixIsSystem, "a prefix of mingw-w64 and Windows libraries does not");
|
|
Check(!ResolveToolchain({}).mingwPrefixIsSystem, "no prefix, nothing to show");
|
|
fs::remove_all(staged);
|
|
}
|
|
{
|
|
// 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).mingwPrefix.empty(), "a host without mingw-w64 gives no mingw prefix");
|
|
fs::create_directories(host / "usr" / "x86_64-w64-mingw32");
|
|
Check(ResolveToolchain(in).mingwPrefix == host / "usr", "a host with mingw-w64 gives its /usr as the mingw prefix");
|
|
Check(ResolveToolchain(in).mingwPrefixIsSystem, "the host's /usr is the host's system, with or without its C library's headers");
|
|
in.env["CRAFTER_MINGW_DIR"] = "/opt/mingw";
|
|
Check(ResolveToolchain(in).mingwPrefix == "/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;
|
|
}
|