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

@ -355,12 +355,21 @@ export namespace Crafter {
bool needsAst = false;
};
// The host target triple, detected once per process by running
// `clang++ -print-target-triple` and cached. Used as the default for
// The host target triple: what the resolved toolchain's clang++ prints
// for -print-target-triple (see GetToolchain). Used as the default for
// Configuration::target so projects don't have to hardcode it for the
// no-cross-compile case. Returns "" if clang isn't on PATH or doesn't
// print a triple — those projects still need an explicit cfg.target.
// no-cross-compile case. Returns "" when no compiler could be resolved;
// those projects still need an explicit cfg.target, and the first build
// step reports the missing compiler.
std::string HostTarget();
// Target equality that ignores the vendor field: x86_64-pc-linux-gnu,
// x86_64-unknown-linux-gnu and x86_64-linux-gnu are one target as far as
// "does this test run on this host" goes, and which spelling a clang
// reports depends on how it was built. The strings themselves are never
// rewritten: clang rejects a PCM whose triple string differs from the
// importing unit's, so what the user wrote is what the compiler sees.
std::string NormalizeTarget(std::string_view triple);
bool SameTarget(std::string_view a, std::string_view b);
struct Configuration {
fs::path path;

View file

@ -0,0 +1,78 @@
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
export module Crafter.Build:Toolchain;
import std;
namespace fs = std::filesystem;
export namespace Crafter {
// The compiler crafter-build drives and what sits next to it, resolved
// once per process by GetToolchain(). Every clang++, clang and llvm-ar
// the tool spawns, the libclang the linter dlopens, the std module
// source and the default sysroots come from here, so a toolchain
// installed anywhere other than /usr (the Flatpak carries its own under
// /app) needs no per-call-site knowledge.
struct Toolchain {
// LLVM install prefix. Empty when the tools are bare names on PATH.
fs::path prefix;
fs::path clangxx;
fs::path clang;
fs::path llvmAr;
// Where clang-c/Index.h lives when it is not on the default include
// path; empty otherwise.
fs::path llvmIncludeDir;
// The exact libclang to dlopen; nullopt means search the usual names.
std::optional<fs::path> libclang;
// A libc++ install other than the compiler's own
// (CRAFTER_BUILD_LIBCXX_DIR); empty means clang finds its own.
fs::path libcxxDir;
// libc++'s std module source for host builds.
fs::path stdCppm;
fs::path wasiSysroot;
// Holds include/c++/<ver>/bits/std.cc for the mingw target. Passed to
// clang as --sysroot only when it was set explicitly: clang's own
// mingw discovery handles the distro layout, and a --sysroot pointing
// at the triple directory would lose crtbegin.o and libgcc.
fs::path mingwSysroot;
bool mingwSysrootExplicit = false;
// `clang++ -print-target-triple` and the first line of
// `clang++ --version`; both empty when clang++ could not be run.
std::string hostTriple;
std::string clangVersion;
// Why clang++ could not be resolved or run; empty when it could.
std::string error;
// Flags for compiling against libcxxDir; empty without one.
std::string LibcxxCompileFlags() const;
// Flags for linking against the libc++ the compiler will use: an
// rpath into the prefix when that is not the system one (a bundled
// toolchain's libc++.so.1 is not on the loader's default path), plus
// -L and an rpath for an explicit libcxxDir.
std::string LibcxxLinkFlags() const;
// Names this toolchain for cache directory names: the clang version
// followed by a hash of what decides which headers and libraries a
// PCM is built against. Two toolchains with different keys never
// share a PCM.
std::string Key() const;
};
// What ResolveToolchain reads, so a test can feed it a staged layout.
struct ToolchainInputs {
// The CRAFTER_* variables, present only when set and non-empty.
std::map<std::string, std::string, std::less<>> env;
// The <prefix> of <prefix>/share/crafter-build; empty when unknown.
fs::path crafterPrefix;
};
// Pure path resolution: an env override wins, then a toolchain installed
// beside crafter-build (<crafterPrefix>/bin/clang++), then bare names on
// PATH. Never runs a process, so hostTriple and clangVersion stay empty.
Toolchain ResolveToolchain(const ToolchainInputs& inputs);
// ResolveToolchain over the real environment plus the two clang probes,
// computed once and cached. Never throws.
const Toolchain& GetToolchain();
// GetToolchain(), or a std::runtime_error carrying its error.
const Toolchain& RequireToolchain();
// The version baked in when crafter-build was built; "0.0.0-dev" for a
// build that was given none.
std::string_view CrafterBuildVersion();
}

View file

@ -2,6 +2,7 @@
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
export module Crafter.Build;
export import :Toolchain;
export import :Clang;
export import :Platform;
export import :Implementation;