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:
Jorijn van der Graaf 2026-10-02 22:26:31 +02:00
commit 046609ff6f
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
18 changed files with 767 additions and 190 deletions

View file

@ -181,7 +181,7 @@ TestRunner TestRunner::ForTarget(const Configuration& cfg) {
// Same triple as the host → run the binary directly. Covers the common
// case (cfg.target defaulted to HostTarget()) without any wrapper.
if (target == HostTarget()) return Local();
if (SameTarget(target, HostTarget())) return Local();
// Windows targets run under Wine; there is no Windows host.
if (TargetIsWindows(target)) return Wine();
@ -466,13 +466,16 @@ TestSummary Crafter::RunTests(Configuration& projectCfg, const RunTestsOptions&
// user having to know which targets exist. An explicit --target=X
// bypasses the sweep and runs that target only.
if (opts.targetFilter.empty()) {
std::set<std::string> sweep;
sweep.insert(HostTarget());
// Keyed by the normalized triple so x86_64-pc-linux-gnu and
// x86_64-unknown-linux-gnu make one pass; the first spelling seen is
// the one shown and filtered on.
std::map<std::string, std::string> sweep;
sweep.emplace(NormalizeTarget(HostTarget()), HostTarget());
for (const Test& t : projectCfg.tests) {
if (!t.config.target.empty()) sweep.insert(t.config.target);
if (!t.config.target.empty()) sweep.emplace(NormalizeTarget(t.config.target), t.config.target);
}
TestSummary aggregate;
for (const auto& target : sweep) {
for (const auto& [normalized, target] : sweep) {
RunTestsOptions perTarget = opts;
perTarget.targetFilter = target;
if (sweep.size() > 1) {
@ -499,7 +502,7 @@ TestSummary Crafter::RunTests(Configuration& projectCfg, const RunTestsOptions&
std::vector<Test*> filtered;
filtered.reserve(projectCfg.tests.size());
for (auto& test : projectCfg.tests) {
if (test.config.target != opts.targetFilter) continue;
if (!SameTarget(test.config.target, opts.targetFilter)) continue;
if (!MatchAny(opts.globs, test.config.name)) continue;
test.runner = TestRunner::FromEnv(test.config.target, TestRunner::ForTarget(test.config));
if (opts.runnerOverride) {