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
182
implementations/Crafter.Build-Toolchain.cpp
Normal file
182
implementations/Crafter.Build-Toolchain.cpp
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module;
|
||||
// Supplied on the command line by build.sh and project.cpp (from the
|
||||
// CRAFTER_BUILD_VERSION environment variable or `git describe`); a build
|
||||
// that passes nothing is a development build.
|
||||
#ifndef CRAFTER_BUILD_VERSION
|
||||
#define CRAFTER_BUILD_VERSION "0.0.0-dev"
|
||||
#endif
|
||||
module Crafter.Build:Toolchain_impl;
|
||||
import std;
|
||||
import :Toolchain;
|
||||
import :Platform;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
namespace {
|
||||
constexpr std::string_view EnvVars[] = {
|
||||
"CRAFTER_BUILD_LLVM_DIR",
|
||||
"CRAFTER_BUILD_LIBCXX_DIR",
|
||||
"CRAFTER_BUILD_LIBCLANG",
|
||||
"CRAFTER_BUILD_WASI_SYSROOT",
|
||||
"CRAFTER_MINGW_DIR",
|
||||
};
|
||||
|
||||
std::string_view Env(const ToolchainInputs& in, std::string_view name) {
|
||||
auto it = in.env.find(name);
|
||||
return it == in.env.end() ? std::string_view{} : std::string_view(it->second);
|
||||
}
|
||||
|
||||
// "Arch Linux clang version 22.1.8" / "clang version 22.1.8" -> "22.1.8".
|
||||
std::string ShortVersion(std::string_view versionLine) {
|
||||
constexpr std::string_view Marker = "version ";
|
||||
std::size_t at = versionLine.find(Marker);
|
||||
if (at == std::string_view::npos) return "unknown";
|
||||
std::string_view rest = versionLine.substr(at + Marker.size());
|
||||
std::size_t end = rest.find_first_of(" \t(");
|
||||
return std::string(rest.substr(0, end));
|
||||
}
|
||||
|
||||
std::string Trimmed(std::string s) {
|
||||
while (!s.empty() && (s.back() == '\n' || s.back() == '\r' || s.back() == ' ')) s.pop_back();
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
std::string Toolchain::LibcxxCompileFlags() const {
|
||||
if (libcxxDir.empty()) return {};
|
||||
return std::format(" -nostdinc++ -isystem {}", (libcxxDir / "include" / "c++" / "v1").string());
|
||||
}
|
||||
|
||||
std::string Toolchain::LibcxxLinkFlags() const {
|
||||
std::string flags;
|
||||
if (!libcxxDir.empty()) {
|
||||
fs::path lib = libcxxDir / "lib";
|
||||
flags += std::format(" -L{} -Wl,-rpath,{}", lib.string(), lib.string());
|
||||
} else if (!prefix.empty() && prefix != "/usr") {
|
||||
flags += std::format(" -Wl,-rpath,{}", (prefix / "lib").string());
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
std::string Toolchain::Key() const {
|
||||
std::string identity;
|
||||
for (const std::string& part : {clangxx.string(), clangVersion, libcxxDir.string(), stdCppm.string()}) {
|
||||
identity += part;
|
||||
identity += '\0';
|
||||
}
|
||||
return std::format("{}-{:08x}", ShortVersion(clangVersion), static_cast<std::uint32_t>(std::hash<std::string>{}(identity)));
|
||||
}
|
||||
|
||||
Toolchain Crafter::ResolveToolchain(const ToolchainInputs& in) {
|
||||
Toolchain tc;
|
||||
|
||||
// 1. The compiler: an explicit prefix, else one installed beside
|
||||
// crafter-build, else PATH. An explicit prefix without a clang++ is an
|
||||
// error rather than a fallback, for the same reason CRAFTER_BUILD_LIBCLANG
|
||||
// is exclusive: an override that silently yields to something else
|
||||
// cannot be used to diagnose which compiler is in play.
|
||||
if (std::string_view dir = Env(in, "CRAFTER_BUILD_LLVM_DIR"); !dir.empty()) {
|
||||
tc.prefix = fs::path(dir);
|
||||
if (!fs::exists(tc.prefix / "bin" / "clang++")) {
|
||||
tc.error = std::format("clang++ not found at {} (CRAFTER_BUILD_LLVM_DIR)", (tc.prefix / "bin" / "clang++").string());
|
||||
}
|
||||
} else if (!in.crafterPrefix.empty() && fs::exists(in.crafterPrefix / "bin" / "clang++")) {
|
||||
tc.prefix = in.crafterPrefix;
|
||||
}
|
||||
if (tc.prefix.empty()) {
|
||||
tc.clangxx = "clang++";
|
||||
tc.clang = "clang";
|
||||
tc.llvmAr = "llvm-ar";
|
||||
} else {
|
||||
tc.clangxx = tc.prefix / "bin" / "clang++";
|
||||
tc.clang = tc.prefix / "bin" / "clang";
|
||||
tc.llvmAr = tc.prefix / "bin" / "llvm-ar";
|
||||
if (tc.prefix != "/usr" && fs::exists(tc.prefix / "include" / "clang-c" / "Index.h")) {
|
||||
tc.llvmIncludeDir = tc.prefix / "include";
|
||||
}
|
||||
}
|
||||
|
||||
// 2. libclang. The override is exclusive and unchecked on purpose: a
|
||||
// broken path must fail loudly when dlopened, not be papered over.
|
||||
if (std::string_view pinned = Env(in, "CRAFTER_BUILD_LIBCLANG"); !pinned.empty()) {
|
||||
tc.libclang = fs::path(pinned);
|
||||
} else if (!tc.prefix.empty() && fs::exists(tc.prefix / "lib" / "libclang.so")) {
|
||||
tc.libclang = tc.prefix / "lib" / "libclang.so";
|
||||
}
|
||||
|
||||
// 3. libc++ and its std module source. The compiler's own copy lives at
|
||||
// <prefix>/share/libc++/v1 when the prefix carries libc++; the system
|
||||
// copy is the last resort either way.
|
||||
const fs::path systemStdCppm = "/usr/share/libc++/v1/std.cppm";
|
||||
if (std::string_view dir = Env(in, "CRAFTER_BUILD_LIBCXX_DIR"); !dir.empty()) {
|
||||
tc.libcxxDir = fs::path(dir);
|
||||
tc.stdCppm = tc.libcxxDir / "share" / "libc++" / "v1" / "std.cppm";
|
||||
} else if (!tc.prefix.empty() && fs::exists(tc.prefix / "share" / "libc++" / "v1" / "std.cppm")) {
|
||||
tc.stdCppm = tc.prefix / "share" / "libc++" / "v1" / "std.cppm";
|
||||
} else {
|
||||
tc.stdCppm = systemStdCppm;
|
||||
}
|
||||
|
||||
// 4. Sysroots for the cross targets that have a conventional location.
|
||||
if (std::string_view dir = Env(in, "CRAFTER_BUILD_WASI_SYSROOT"); !dir.empty()) {
|
||||
tc.wasiSysroot = fs::path(dir);
|
||||
} else if (!tc.prefix.empty() && fs::exists(tc.prefix / "share" / "wasi-sysroot")) {
|
||||
tc.wasiSysroot = tc.prefix / "share" / "wasi-sysroot";
|
||||
} else {
|
||||
tc.wasiSysroot = "/usr/share/wasi-sysroot";
|
||||
}
|
||||
if (std::string_view dir = Env(in, "CRAFTER_MINGW_DIR"); !dir.empty()) {
|
||||
tc.mingwSysroot = fs::path(dir);
|
||||
tc.mingwSysrootExplicit = true;
|
||||
} else {
|
||||
tc.mingwSysroot = "/usr/x86_64-w64-mingw32";
|
||||
}
|
||||
return tc;
|
||||
}
|
||||
|
||||
const Toolchain& Crafter::GetToolchain() {
|
||||
static const Toolchain Resolved = []() {
|
||||
ToolchainInputs in;
|
||||
for (std::string_view name : EnvVars) {
|
||||
if (const char* value = std::getenv(std::string(name).c_str()); value && *value) {
|
||||
in.env.emplace(std::string(name), value);
|
||||
}
|
||||
}
|
||||
// GetCrafterBuildHome throws when no share/crafter-build is found
|
||||
// near the executable; a toolchain can still come from PATH then.
|
||||
try {
|
||||
in.crafterPrefix = GetCrafterBuildHome().parent_path().parent_path();
|
||||
} catch (const std::exception&) {}
|
||||
|
||||
Toolchain tc = ResolveToolchain(in);
|
||||
if (!tc.error.empty()) return tc;
|
||||
|
||||
CommandResult triple = RunCommandChecked(std::format("{} -print-target-triple", tc.clangxx.string()));
|
||||
if (triple.exitCode != 0) {
|
||||
tc.error = std::format("could not run {} (exit {}): {}", tc.clangxx.string(), triple.exitCode, Trimmed(triple.output));
|
||||
if (tc.prefix.empty()) tc.error += "; install clang or set CRAFTER_BUILD_LLVM_DIR";
|
||||
return tc;
|
||||
}
|
||||
tc.hostTriple = Trimmed(std::move(triple.output));
|
||||
CommandResult version = RunCommandChecked(std::format("{} --version", tc.clangxx.string()));
|
||||
if (version.exitCode == 0) {
|
||||
std::string first = version.output.substr(0, version.output.find('\n'));
|
||||
tc.clangVersion = Trimmed(std::move(first));
|
||||
}
|
||||
return tc;
|
||||
}();
|
||||
return Resolved;
|
||||
}
|
||||
|
||||
const Toolchain& Crafter::RequireToolchain() {
|
||||
const Toolchain& tc = GetToolchain();
|
||||
if (!tc.error.empty()) throw std::runtime_error(tc.error);
|
||||
return tc;
|
||||
}
|
||||
|
||||
std::string_view Crafter::CrafterBuildVersion() {
|
||||
return CRAFTER_BUILD_VERSION;
|
||||
}
|
||||
Loading…
Reference in a new issue