The first fix showed clang every mingw prefix through a view of its mingw-w64 parts. That broke a prefix that holds mingw-w64 alone, where Windows libraries installed with --prefix land in include/ and lib/: with CRAFTER_MINGW_DIR=/opt/mingw, <SDL2/SDL.h> and -lSDL2 were no longer found. Such a prefix is clang's sysroot whole again. Only a prefix that also holds a Linux system gets the view (Toolchain::mingwPrefixIsSystem): the host's /usr inside the Flatpak, whatever its include/ holds, and a prefix whose include/ has a C library's features.h, which every glibc and musl header includes and mingw-w64 has no use for, as CRAFTER_MINGW_DIR=/usr has. MakeMingwSysroot makes the view in the cache, or in $XDG_RUNTIME_DIR/crafter.build when the cache's filesystem takes no symbolic links (vfat, exFAT, CIFS without unix extensions): the XDG spec promises them there, and nothing else in the cache needs them. Its path is absolute, because CMake runs the compiler from directories of its own and a relative cache did not resolve from there. The prefix is resolved once, absolute and lexically normal, so /opt/mingw/ and /opt/mingw are one prefix and not two builds. clang names a header by the path it found it at, so every diagnostic from a viewed mingw-w64 header named a hashed cache directory. RunBuildCommand now writes paths into the view as paths into the prefix, as BuildStdPcm did for its manifest messages already. Also from the review of PR #44: SysrootCompileFlags builds a mingw command, which can make a view, so it redirects the cache like the other tests; the manifest filter's comment names what it still keeps out; the C path asks MingwSysroot() once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
343 lines
16 KiB
C++
343 lines
16 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;
|
|
}
|
|
|
|
// `link` made a symbolic link to `target`, or found one already. Another
|
|
// crafter-build may make the same link at the same moment, which is as
|
|
// good as making it; a link to anything else is an error.
|
|
void EnsureSymlink(const fs::path& target, const fs::path& link) {
|
|
std::error_code ec;
|
|
if (fs::read_symlink(link, ec) == target) return;
|
|
fs::create_directories(link.parent_path());
|
|
fs::create_directory_symlink(target, link, ec);
|
|
std::error_code ignored;
|
|
if (ec && fs::read_symlink(link, ignored) != target) {
|
|
throw fs::filesystem_error("cannot make a link", target, link, ec);
|
|
}
|
|
}
|
|
|
|
// `dir` made absolute and spelled one way, once, when the toolchain is
|
|
// resolved. The mingw prefix ends up as the target of links and in the
|
|
// names of build and cache directories: a relative path read later from
|
|
// another working directory would name something else, and /opt/mingw/
|
|
// next to /opt/mingw would rebuild everything for the same directory.
|
|
fs::path PrefixPath(const fs::path& dir) {
|
|
fs::path normal = fs::absolute(dir).lexically_normal();
|
|
if (!normal.has_filename() && normal.has_relative_path()) normal = normal.parent_path();
|
|
return normal;
|
|
}
|
|
|
|
// The view MingwSysroot() made and the prefix it shows, for
|
|
// MingwViewToPrefix. Written once, before ViewMade is set.
|
|
fs::path MadeView;
|
|
fs::path ViewedPrefix;
|
|
std::atomic<bool> ViewMade = false;
|
|
}
|
|
|
|
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.mingwPrefix = PrefixPath(fs::path(dir));
|
|
// A prefix that holds a Linux system as well as mingw-w64, /usr
|
|
// itself, has that system's C library in include/: features.h, which
|
|
// every glibc and musl header includes and mingw-w64 has no use for.
|
|
// One that holds mingw-w64 alone stays the sysroot, so Windows
|
|
// libraries installed into its include/ and lib/ stay visible.
|
|
tc.mingwPrefixIsSystem = fs::exists(tc.mingwPrefix / "include" / "features.h");
|
|
} else if (!in.hostRoot.empty() && fs::exists(in.hostRoot / "usr" / "x86_64-w64-mingw32")) {
|
|
// The sandbox has no mingw-w64 of its own, and no mingw gcc on PATH
|
|
// for clang to find one by. The host's /usr is the host's system,
|
|
// whether or not its C library's headers are installed.
|
|
tc.mingwPrefix = PrefixPath(in.hostRoot / "usr");
|
|
tc.mingwPrefixIsSystem = true;
|
|
}
|
|
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&) {}
|
|
if (fs::exists("/.flatpak-info")) in.hostRoot = "/run/host";
|
|
|
|
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;
|
|
}
|
|
|
|
fs::path Crafter::MakeMingwSysroot(const fs::path& prefix, std::span<const fs::path> parents) {
|
|
std::string reasons;
|
|
for (const fs::path& parent : parents) {
|
|
// One view per prefix: two shells with different CRAFTER_MINGW_DIRs
|
|
// share one cache and would otherwise rewrite each other's.
|
|
const fs::path sysroot = parent / "mingw-sysroot" / std::format("{:016x}", std::hash<std::string>{}(prefix.string()));
|
|
try {
|
|
// All clang's mingw driver takes from a sysroot other than its
|
|
// include/ and the rest of its lib/: the mingw-w64 tree (headers,
|
|
// libraries, and libstdc++ where a distro's cross gcc installs
|
|
// it) and gcc's per-target directory (crtbegin.o, libgcc, and the
|
|
// version that names libstdc++'s header directory), which
|
|
// openSUSE keeps under lib64. A link to nothing is skipped like a
|
|
// missing directory.
|
|
for (std::string_view entry : {"x86_64-w64-mingw32", "lib/gcc", "lib64/gcc"}) {
|
|
EnsureSymlink(prefix / entry, sysroot / entry);
|
|
}
|
|
return sysroot;
|
|
} catch (const std::exception& e) {
|
|
reasons += std::format("; {}", e.what());
|
|
}
|
|
}
|
|
throw std::runtime_error(std::format("cannot make a sysroot that shows clang only the mingw-w64 parts of {}{}", prefix.string(), reasons));
|
|
}
|
|
|
|
const fs::path& Crafter::MingwSysroot() {
|
|
// Made on first use rather than by GetToolchain(), so that only what
|
|
// compiles for the mingw target, a build or an AST lint of one, writes
|
|
// anything for it, and a failure surfaces as that command's error
|
|
// instead of on every command.
|
|
static const fs::path Sysroot = []() -> fs::path {
|
|
const Toolchain& tc = GetToolchain();
|
|
if (!tc.mingwPrefixIsSystem) return tc.mingwPrefix;
|
|
// In the cache, or where the XDG spec promises symbolic links when
|
|
// the cache's filesystem takes none (vfat, exFAT, CIFS without unix
|
|
// extensions), which costs nothing else crafter-build caches.
|
|
// Absolute, because CMake runs the compiler from directories of its
|
|
// own.
|
|
std::vector<fs::path> parents = { fs::absolute(GetCacheDir()) };
|
|
if (const char* runtime = std::getenv("XDG_RUNTIME_DIR"); runtime && *runtime) {
|
|
parents.push_back(fs::absolute(fs::path(runtime) / "crafter.build"));
|
|
}
|
|
fs::path view = MakeMingwSysroot(tc.mingwPrefix, parents);
|
|
MadeView = view;
|
|
ViewedPrefix = tc.mingwPrefix;
|
|
ViewMade.store(true, std::memory_order_release);
|
|
return view;
|
|
}();
|
|
return Sysroot;
|
|
}
|
|
|
|
std::string Crafter::MingwViewToPrefix(std::string text) {
|
|
if (!ViewMade.load(std::memory_order_acquire)) return text;
|
|
// With the separator, so a --sysroot= naming the view itself, in a
|
|
// command line the output quotes, stays what was run.
|
|
const std::string view = (MadeView / "").string();
|
|
const std::string prefix = (ViewedPrefix / "").string();
|
|
for (std::size_t at = text.find(view); at != std::string::npos; at = text.find(view, at + prefix.size())) {
|
|
text.replace(at, view.size(), prefix);
|
|
}
|
|
return text;
|
|
}
|
|
|
|
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;
|
|
}
|