feat(toolchain): resolve the compiler in one place
Every clang++, clang and llvm-ar the tool spawned was a bare name on PATH, the std module source was /usr/share/libc++/v1/std.cppm, the WASI sysroot /usr/share/wasi-sysroot and the mingw headers /usr/x86_64-w64-mingw32, spelled out at eighteen call sites. A toolchain installed anywhere else, which is what the Flatpak ships under /app, had no way in. Crafter.Build:Toolchain resolves all of it once per process: CRAFTER_BUILD_LLVM_DIR, else a clang++ installed beside crafter-build (<prefix>/bin, found through the same walk GetCrafterBuildHome does), else PATH. What sits beside the compiler is used when present: its libclang.so, its clang-c headers, its std module source, its WASI sysroot. An explicit prefix without a clang++ is an error rather than a fallback, for the same reason CRAFTER_BUILD_LIBCLANG was already exclusive. CRAFTER_BUILD_LIBCXX_DIR points at a libc++ install other than the compiler's own, and CRAFTER_MINGW_DIR now does what its help line always claimed: it is passed as --sysroot for the mingw target. Binaries linked by a toolchain outside /usr get an rpath to its lib dir, since its libc++.so.1 is not on the loader's default path. HostTarget() is the resolved clang's -print-target-triple. A clang built without a vendor reports x86_64-unknown-linux-gnu where Arch's reports x86_64-pc-linux-gnu, so target equality that decides whether a test runs locally and which targets a bare `test` sweeps goes through SameTarget, which ignores the vendor field. The strings themselves are never rewritten: clang rejects a PCM whose triple differs from the importing unit's. The examples and README stop hardcoding the host triple; the default is already the host. `crafter-build --version` prints the version baked in at build time (CRAFTER_BUILD_VERSION, else `git describe`, else 0.0.0-dev), the compiler and the host triple, and is answered before any argument is forwarded to project.cpp. The version is a define on the library, so each version is its own library variant and relinks the executable and tests once per commit. build.sh gains a shebang and `set -eu`, honours CRAFTER_BUILD_LLVM_DIR and CRAFTER_BUILD_VERSION, skips the glslang clone when it is already there, and writes a fresh archive instead of appending to one that may hold members from an older layout. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
68e9b6bfa5
commit
046609ff6f
18 changed files with 767 additions and 190 deletions
|
|
@ -92,6 +92,32 @@ int main() {
|
|||
Check(r.name == "wine", "ForTarget(mingw) uses wine");
|
||||
}
|
||||
|
||||
// SameTarget: the vendor field is spelling, not identity. A clang built
|
||||
// without a vendor reports x86_64-unknown-linux-gnu where Arch's reports
|
||||
// x86_64-pc-linux-gnu; both are the host, and a project that wrote the
|
||||
// other spelling must still run its tests locally.
|
||||
{
|
||||
Check(SameTarget("x86_64-pc-linux-gnu", "x86_64-unknown-linux-gnu"), "pc and unknown vendors compare equal");
|
||||
Check(SameTarget("x86_64-pc-linux-gnu", "x86_64-linux-gnu"), "a vendorless triple compares equal");
|
||||
Check(SameTarget("x86_64-alpine-linux-musl", "x86_64-linux-musl"), "a distro vendor compares equal");
|
||||
Check(SameTarget("aarch64-unknown-linux-gnu", "aarch64-linux-gnu"), "the vendor is dropped on other arches too");
|
||||
Check(SameTarget("wasm32-unknown-wasip1", "wasm32-wasip1"), "a three-part triple with a vendor compares equal");
|
||||
Check(!SameTarget("x86_64-pc-linux-gnu", "x86_64-linux-musl"), "a different environment stays different");
|
||||
Check(!SameTarget("x86_64-pc-linux-gnu", "aarch64-unknown-linux-gnu"), "a different arch stays different");
|
||||
Check(!SameTarget("x86_64-w64-mingw32", "x86_64-linux-gnu"), "mingw's w64 is not a vendor to drop");
|
||||
Check(NormalizeTarget("x86_64-w64-mingw32") == "x86_64-w64-mingw32", "a mingw triple is left alone");
|
||||
Check(NormalizeTarget("wasm32-wasip1") == "wasm32-wasip1", "a two-part triple is left alone");
|
||||
|
||||
Configuration pc;
|
||||
pc.target = "x86_64-pc-linux-gnu";
|
||||
Configuration unknown;
|
||||
unknown.target = "x86_64-unknown-linux-gnu";
|
||||
if (SameTarget(HostTarget(), "x86_64-linux-gnu")) {
|
||||
Check(TestRunner::ForTarget(pc).IsLocal(), "ForTarget runs the pc spelling locally on an x86_64 glibc host");
|
||||
Check(TestRunner::ForTarget(unknown).IsLocal(), "ForTarget runs the unknown spelling locally on an x86_64 glibc host");
|
||||
}
|
||||
}
|
||||
|
||||
// FromEnv: when the env var is set, it wins over the fallback.
|
||||
{
|
||||
// Use a unique fake triple so we don't stomp on a real env var the
|
||||
|
|
|
|||
169
tests/ToolchainResolve/main.cpp
Normal file
169
tests/ToolchainResolve/main.cpp
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
// 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;
|
||||
}
|
||||
46
tests/VersionFlag/main.cpp
Normal file
46
tests/VersionFlag/main.cpp
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// 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;
|
||||
|
||||
// `--version` has to work from any directory, before a project file is
|
||||
// looked for, and must be answered rather than forwarded to project.cpp as
|
||||
// one more unknown argument.
|
||||
namespace {
|
||||
std::int32_t Failures = 0;
|
||||
|
||||
void Check(bool cond, std::string_view msg) {
|
||||
if (!cond) {
|
||||
std::println(std::cerr, "FAIL: {}", msg);
|
||||
++Failures;
|
||||
}
|
||||
}
|
||||
|
||||
std::int32_t RunWith(std::vector<std::string> args) {
|
||||
std::vector<char*> argv;
|
||||
for (std::string& arg : args) argv.push_back(arg.data());
|
||||
return Run(static_cast<std::int32_t>(argv.size()), argv.data());
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
Check(!CrafterBuildVersion().empty(), "a version string is always baked in");
|
||||
|
||||
// An empty directory: no project.cpp to fall back to.
|
||||
const fs::path scratch = fs::temp_directory_path() / "crafter-build-version-flag";
|
||||
std::error_code ec;
|
||||
fs::remove_all(scratch, ec);
|
||||
fs::create_directories(scratch);
|
||||
fs::current_path(scratch);
|
||||
|
||||
Check(RunWith({"crafter-build", "--version"}) == 0, "--version exits 0 without a project file");
|
||||
Check(RunWith({"crafter-build", "--version", "--some-project-flag"}) == 0, "--version is answered before unknown arguments are forwarded");
|
||||
Check(RunWith({"crafter-build", "--some-project-flag", "--version"}) == 0, "--version is answered wherever it appears");
|
||||
|
||||
fs::current_path(fs::temp_directory_path());
|
||||
fs::remove_all(scratch, ec);
|
||||
return Failures == 0 ? 0 : 1;
|
||||
}
|
||||
Loading…
Reference in a new issue