Crafter.Build/interfaces/Crafter.Build-Toolchain.cppm
Jorijn van der Graaf e15cd3c14d
fix(sysroot): take the std module source from the sysroot's libc++ release
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>
2026-10-03 16:37:00 +02:00

111 lines
5.7 KiB
C++

// 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;
// The _LIBCPP_VERSION of the libc++ stdCppm belongs to (220108 for
// 22.1.8); 0 when its <__config> could not be read.
std::int32_t libcxxVersion = 0;
// Std module sources for libc++ releases other than the toolchain's
// own, one <major>/std.cppm each, for sysroots that carry libc++
// without them: <crafter prefix>/share/crafter-build/libc++-modules.
// Empty when the crafter prefix is unknown; need not exist.
fs::path stdModulesDir;
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: a -L
// and an rpath for <prefix>/lib when the prefix is not the system one
// (clang searches only its per-target runtime directory and the
// system paths, and a bundled toolchain's libc++.so.1 is on neither
// the linker's nor the loader's default path), or 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();
// _LIBCPP_VERSION as the <__config> in `includeDir` (an include/c++/v1)
// defines it, XXYYZZ; 0 when the file is missing or has no such line.
std::int32_t LibcxxVersion(const fs::path& includeDir);
// "23.1.2" for 230102.
std::string LibcxxVersionString(std::int32_t version);
struct StdModuleSource {
fs::path stdCppm;
// One line saying why no source fits; empty when stdCppm is set.
std::string error;
};
// The std module source to precompile for a native (non-wasm) build
// against `sysroot`. The module source is a list of using-declarations
// for names the headers of the same libc++ release declare, so one from
// another release fails on every name that release added or dropped. In
// order: the sysroot's own <sysroot>/usr/share/libc++/v1/std.cppm, the
// toolchain's when both are the same major release, the copy for the
// sysroot's release under toolchain.stdModulesDir. Not every distro
// packages the module sources with its libc++ (Alpine does not), which
// is why the sysroot's own copy cannot be the only answer. When the
// sysroot's release cannot be read, the toolchain's source is returned
// and the compile is the check.
StdModuleSource SysrootStdModule(const Toolchain& toolchain, const fs::path& sysroot);
// The version baked in when crafter-build was built; "0.0.0-dev" for a
// build that was given none.
std::string_view CrafterBuildVersion();
}