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>
139 lines
6.1 KiB
C++
139 lines
6.1 KiB
C++
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
import std;
|
|
import Crafter.Build;
|
|
using namespace Crafter;
|
|
|
|
// POSIX env helpers — libc++ exports only std::getenv via `import std`, so
|
|
// forward-declare the setters we need against the C library directly.
|
|
extern "C" int setenv(const char* name, const char* value, int overwrite);
|
|
extern "C" int unsetenv(const char* name);
|
|
|
|
namespace {
|
|
std::int32_t Failures = 0;
|
|
|
|
void Check(bool cond, std::string_view msg) {
|
|
if (!cond) {
|
|
std::println(std::cerr, "FAIL: {}", msg);
|
|
++Failures;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pure-data tests for TestRunner factories. No tests are actually executed;
|
|
// we just confirm the routing tables in ForTarget / FromSpec / FromEnv.
|
|
int main() {
|
|
// FromSpec: empty -> nullopt; recognized -> populated; garbage -> throws.
|
|
{
|
|
auto empty = TestRunner::FromSpec("");
|
|
Check(!empty.has_value(), "FromSpec(\"\") returns nullopt");
|
|
}
|
|
{
|
|
auto local = TestRunner::FromSpec("local");
|
|
Check(local.has_value() && local->IsLocal(), "FromSpec(\"local\") returns Local");
|
|
Check(local.has_value() && local->name == "local", "Local name is 'local'");
|
|
}
|
|
{
|
|
auto cmd = TestRunner::FromSpec("cmd:wasmtime");
|
|
Check(cmd.has_value(), "FromSpec(\"cmd:wasmtime\") returns a runner");
|
|
Check(cmd.has_value() && cmd->name == "cmd:wasmtime", "Cmd name carries the binary");
|
|
Check(cmd.has_value() && cmd->exec.find("wasmtime") != std::string::npos, "Cmd exec carries the binary");
|
|
Check(cmd.has_value() && !cmd->probe.empty(), "Cmd probe is set so harness can detect missing tools");
|
|
}
|
|
{
|
|
bool threw = false;
|
|
try {
|
|
(void)TestRunner::FromSpec("garbage");
|
|
} catch (const std::exception&) {
|
|
threw = true;
|
|
}
|
|
Check(threw, "FromSpec rejects unrecognized non-empty specs");
|
|
}
|
|
|
|
// ForTarget routing.
|
|
{
|
|
Configuration cfg;
|
|
cfg.target = HostTarget();
|
|
TestRunner r = TestRunner::ForTarget(cfg);
|
|
Check(r.IsLocal(), "ForTarget(host) is Local");
|
|
}
|
|
{
|
|
Configuration cfg;
|
|
cfg.target = "wasm32-wasip1";
|
|
TestRunner r = TestRunner::ForTarget(cfg);
|
|
Check(r.name == "cmd:wasmtime", "ForTarget(wasm32-wasip1) routes through wasmtime");
|
|
}
|
|
{
|
|
Configuration cfg;
|
|
cfg.target = "aarch64-linux-gnu";
|
|
TestRunner r = TestRunner::ForTarget(cfg);
|
|
Check(r.name == "cmd:qemu-aarch64", "ForTarget(aarch64-linux-gnu) routes through qemu-aarch64");
|
|
}
|
|
{
|
|
Configuration cfg;
|
|
cfg.target = "i686-linux-gnu";
|
|
TestRunner r = TestRunner::ForTarget(cfg);
|
|
Check(r.name == "cmd:qemu-i386", "ForTarget(i686-linux-gnu) rewrites arch to i386");
|
|
}
|
|
{
|
|
// Sysroot propagates to QEMU_LD_PREFIX so the dynamic linker is reachable.
|
|
Configuration cfg;
|
|
cfg.target = "aarch64-linux-gnu";
|
|
cfg.sysroot = "/opt/alarm-sysroot";
|
|
TestRunner r = TestRunner::ForTarget(cfg);
|
|
Check(r.exec.find("QEMU_LD_PREFIX=/opt/alarm-sysroot") != std::string::npos, "ForTarget propagates sysroot to QEMU_LD_PREFIX");
|
|
}
|
|
{
|
|
// The only way to run x86_64-w64-mingw32 output is wine.
|
|
Configuration cfg;
|
|
cfg.target = "x86_64-w64-mingw32";
|
|
TestRunner r = TestRunner::ForTarget(cfg);
|
|
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
|
|
// CI/dev shell may have set.
|
|
const std::string name = "CRAFTER_BUILD_RUNNER_fake_target_for_unit_test";
|
|
setenv(name.c_str(), "cmd:fake-tool", 1);
|
|
TestRunner r = TestRunner::FromEnv("fake-target-for-unit-test", TestRunner::Local());
|
|
Check(r.name == "cmd:fake-tool", "FromEnv reads CRAFTER_BUILD_RUNNER_<target>");
|
|
unsetenv(name.c_str());
|
|
TestRunner fallback = TestRunner::FromEnv("fake-target-for-unit-test", TestRunner::Local());
|
|
Check(fallback.IsLocal(), "FromEnv falls back when env var is unset");
|
|
}
|
|
|
|
if (Failures > 0) {
|
|
std::println(std::cerr, "{} assertions failed", Failures);
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|