A cross build against a sysroot whose libc++ differs from the toolchain's failed while precompiling the std module, with a page of "no member named 'runtime_format' in namespace 'std'". The module source is a list of using-declarations for what one libc++ release declares, so it only compiles against that release's headers. BuildStdPcm wants the sysroot's own <sysroot>/usr/share/libc++/v1/std.cppm, but Alpine packages the headers without it, and the fallback was the toolchain's copy whatever its release. That held while the compiler came from the same distro as the sysroot; with the Flatpak's libc++ 22.1.8 and Alpine edge on 23.1.2 it no longer does. The release is now read from _LIBCPP_VERSION in the sysroot's and the toolchain's <__config>, and SysrootStdModule picks in this order: the sysroot's own source, the toolchain's when both are the same major release (a release branch only takes fixes), the copy crafter-build ships for that release under share/crafter-build/libc++-modules/<major>. When none fits the build stops with one line naming both releases and the directory the matching source belongs in, before clang is run. If either release cannot be read the toolchain's source is tried as before, since there is nothing to compare. Matching sources live in crafter-build's own share directory rather than the LLVM prefix: they are data this tool ships for sysroots, not part of any one compiler install, and "sources ship" already describes that directory. `--version` prints the toolchain's libc++ release and the releases sources are shipped for, the first thing to look at when such a build fails. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
239 lines
11 KiB
C++
239 lines
11 KiB
C++
// 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 {
|
|
// clang searches its per-target runtime directory and the system library
|
|
// paths for -lc++, not <prefix>/lib, so a toolchain outside /usr with the
|
|
// classic layout needs the search path as well as the run-time path.
|
|
fs::path lib;
|
|
if (!libcxxDir.empty()) {
|
|
lib = libcxxDir / "lib";
|
|
} else if (!prefix.empty() && prefix != "/usr") {
|
|
lib = prefix / "lib";
|
|
}
|
|
if (lib.empty()) return {};
|
|
return std::format(" -L{} -Wl,-rpath,{}", lib.string(), lib.string());
|
|
}
|
|
|
|
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. The headers of the same install
|
|
// say which release the source belongs to.
|
|
fs::path libcxxRoot = "/usr";
|
|
if (std::string_view dir = Env(in, "CRAFTER_BUILD_LIBCXX_DIR"); !dir.empty()) {
|
|
tc.libcxxDir = fs::path(dir);
|
|
libcxxRoot = tc.libcxxDir;
|
|
} else if (!tc.prefix.empty() && fs::exists(tc.prefix / "share" / "libc++" / "v1" / "std.cppm")) {
|
|
libcxxRoot = tc.prefix;
|
|
}
|
|
tc.stdCppm = libcxxRoot / "share" / "libc++" / "v1" / "std.cppm";
|
|
tc.libcxxVersion = LibcxxVersion(libcxxRoot / "include" / "c++" / "v1");
|
|
if (!in.crafterPrefix.empty()) {
|
|
tc.stdModulesDir = in.crafterPrefix / "share" / "crafter-build" / "libc++-modules";
|
|
}
|
|
|
|
// 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::int32_t Crafter::LibcxxVersion(const fs::path& includeDir) {
|
|
// "# define _LIBCPP_VERSION 230102", the one place libc++ states its
|
|
// release in a form the preprocessor does not have to be run for.
|
|
constexpr std::string_view Marker = "define _LIBCPP_VERSION ";
|
|
std::ifstream config(includeDir / "__config");
|
|
std::string line;
|
|
while (std::getline(config, line)) {
|
|
const std::size_t at = line.find(Marker);
|
|
if (at == std::string::npos) continue;
|
|
const std::string_view digits = std::string_view(line).substr(at + Marker.size());
|
|
std::int32_t version = 0;
|
|
if (std::from_chars(digits.data(), digits.data() + digits.size(), version).ec == std::errc{}) return version;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
std::string Crafter::LibcxxVersionString(std::int32_t version) {
|
|
return std::format("{}.{}.{}", version / 10000, version / 100 % 100, version % 100);
|
|
}
|
|
|
|
StdModuleSource Crafter::SysrootStdModule(const Toolchain& tc, const fs::path& sysroot) {
|
|
const fs::path own = sysroot / "usr" / "share" / "libc++" / "v1" / "std.cppm";
|
|
if (fs::exists(own)) return {own, {}};
|
|
|
|
const fs::path includeDir = sysroot / "usr" / "include" / "c++" / "v1";
|
|
if (!fs::exists(includeDir / "__config")) {
|
|
return {{}, std::format("sysroot {} has no libc++ headers at {}; install its libc++ development package", sysroot.string(), includeDir.string())};
|
|
}
|
|
const std::int32_t wanted = LibcxxVersion(includeDir);
|
|
const bool comparable = wanted != 0 && tc.libcxxVersion != 0;
|
|
// Releases are matched on the major version: a release branch only takes
|
|
// fixes, so 22.1.3 headers and the 22.1.8 module source name the same
|
|
// declarations.
|
|
if (comparable && wanted / 10000 == tc.libcxxVersion / 10000 && fs::exists(tc.stdCppm)) return {tc.stdCppm, {}};
|
|
if (wanted != 0 && !tc.stdModulesDir.empty()) {
|
|
const fs::path shipped = tc.stdModulesDir / std::to_string(wanted / 10000) / "std.cppm";
|
|
if (fs::exists(shipped)) return {shipped, {}};
|
|
}
|
|
// One of the two releases is unknown, so there is nothing to compare:
|
|
// hand over the toolchain's source and let the compile decide, which is
|
|
// what happened for every sysroot before releases were compared.
|
|
if (!comparable && fs::exists(tc.stdCppm)) return {tc.stdCppm, {}};
|
|
|
|
const std::string sysrootRelease = wanted != 0 ? LibcxxVersionString(wanted) : std::string("of an unknown release");
|
|
const std::string toolchainSource = !fs::exists(tc.stdCppm)
|
|
? std::string("the toolchain has no std module source at all")
|
|
: std::format("the toolchain's is for libc++ {}", LibcxxVersionString(tc.libcxxVersion));
|
|
return {{}, std::format("sysroot {} carries libc++ {} without its std module source, and crafter-build has none for that release ({}); install the share/libc++/v1 directory of that libc++ release (std.cppm and std/) as {}", sysroot.string(), sysrootRelease, toolchainSource, own.parent_path().string())};
|
|
}
|
|
|
|
std::string_view Crafter::CrafterBuildVersion() {
|
|
return CRAFTER_BUILD_VERSION;
|
|
}
|