Crafter.Build/implementations/Crafter.Build-Toolchain.cpp

343 lines
16 KiB
C++
Raw Permalink Normal View History

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>
2026-10-02 22:26:31 +02:00
// 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;
}
fix: give the mingw target a sysroot that holds mingw-w64 alone Inside the Flatpak the mingw-w64 prefix is the host's /usr, and it was clang's --sysroot as is. clang's mingw driver takes a sysroot for a tree that holds mingw-w64 alone, and adds its include/ to the include path and its lib/ to the library path. For /usr those are the Linux system's own: every header mingw-w64 lacks came from glibc, and libstdc++'s <ext/atomicity.h>, finding <sys/single_threaded.h>, had shared_ptr read __libc_single_threaded, which nothing on Windows defines. Every program that used one failed to link, among them every Crafter.Graphics Win32 executable, through Crafter.Asset's GLB loader. CRAFTER_MINGW_DIR=/usr did the same outside the Flatpak (#43). The sysroot is now MingwSysroot(): a directory in the cache, one per prefix, holding only links to the prefix's x86_64-w64-mingw32/, lib/gcc/ and lib64/gcc/ (where openSUSE keeps gcc). Through it clang finds mingw-w64, gcc's version and libstdc++ exactly as it does through the mingw gcc on PATH, and nothing else. It is made by the first mingw compile, link or CMake configure of a process, so no other command writes to the cache for it and a failure to make it is that build's error. The Toolchain field is now mingwPrefix, since clang never gets the prefix itself, and it is made absolute when the toolchain is resolved, as it ends up in link targets and directory names. Messages keep naming paths under the prefix rather than under the view. The prefix is also part of the mingw target's VariantId and of its std PCM directory. Units do not track the std PCM they import: when the fixed release built in a directory a broken build had left, the std module was rebuilt and the objects compiled against glibc's headers were only relinked, failing as before. As with a compiler update, a new directory is what gets them rebuilt. It also keeps two releases that share a cache, as the Flatpak's branches do, from rebuilding one std PCM under each other's PCMs, and two prefixes apart. Without a prefix both stay as they were: clang's own discovery has not changed. Two shorter ways do not work: --sysroot=<prefix>/x86_64-w64-mingw32 loses the directory gcc's version names, so <string> is not found, and clang ignores --gcc-toolchain and --gcc-install-dir for this target. Fixes #43 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:48:06 +01:00
// `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) {
fix(mingw): view only a prefix that holds a Linux system 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>
2026-10-31 22:58:30 +01:00
throw fs::filesystem_error("cannot make a link", target, link, ec);
fix: give the mingw target a sysroot that holds mingw-w64 alone Inside the Flatpak the mingw-w64 prefix is the host's /usr, and it was clang's --sysroot as is. clang's mingw driver takes a sysroot for a tree that holds mingw-w64 alone, and adds its include/ to the include path and its lib/ to the library path. For /usr those are the Linux system's own: every header mingw-w64 lacks came from glibc, and libstdc++'s <ext/atomicity.h>, finding <sys/single_threaded.h>, had shared_ptr read __libc_single_threaded, which nothing on Windows defines. Every program that used one failed to link, among them every Crafter.Graphics Win32 executable, through Crafter.Asset's GLB loader. CRAFTER_MINGW_DIR=/usr did the same outside the Flatpak (#43). The sysroot is now MingwSysroot(): a directory in the cache, one per prefix, holding only links to the prefix's x86_64-w64-mingw32/, lib/gcc/ and lib64/gcc/ (where openSUSE keeps gcc). Through it clang finds mingw-w64, gcc's version and libstdc++ exactly as it does through the mingw gcc on PATH, and nothing else. It is made by the first mingw compile, link or CMake configure of a process, so no other command writes to the cache for it and a failure to make it is that build's error. The Toolchain field is now mingwPrefix, since clang never gets the prefix itself, and it is made absolute when the toolchain is resolved, as it ends up in link targets and directory names. Messages keep naming paths under the prefix rather than under the view. The prefix is also part of the mingw target's VariantId and of its std PCM directory. Units do not track the std PCM they import: when the fixed release built in a directory a broken build had left, the std module was rebuilt and the objects compiled against glibc's headers were only relinked, failing as before. As with a compiler update, a new directory is what gets them rebuilt. It also keeps two releases that share a cache, as the Flatpak's branches do, from rebuilding one std PCM under each other's PCMs, and two prefixes apart. Without a prefix both stay as they were: clang's own discovery has not changed. Two shorter ways do not work: --sysroot=<prefix>/x86_64-w64-mingw32 loses the directory gcc's version names, so <string> is not found, and clang ignores --gcc-toolchain and --gcc-install-dir for this target. Fixes #43 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:48:06 +01:00
}
}
fix(mingw): view only a prefix that holds a Linux system 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>
2026-10-31 22:58:30 +01:00
// `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;
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>
2026-10-02 22:26:31 +02:00
}
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;
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>
2026-10-02 22:26:31 +02:00
if (!libcxxDir.empty()) {
lib = libcxxDir / "lib";
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>
2026-10-02 22:26:31 +02:00
} else if (!prefix.empty() && prefix != "/usr") {
lib = prefix / "lib";
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>
2026-10-02 22:26:31 +02:00
}
if (lib.empty()) return {};
return std::format(" -L{} -Wl,-rpath,{}", lib.string(), lib.string());
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>
2026-10-02 22:26:31 +02:00
}
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
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
// copy is the last resort either way. The headers of the same install
// say which release the source belongs to.
fs::path libcxxRoot = "/usr";
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>
2026-10-02 22:26:31 +02:00
if (std::string_view dir = Env(in, "CRAFTER_BUILD_LIBCXX_DIR"); !dir.empty()) {
tc.libcxxDir = fs::path(dir);
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
libcxxRoot = tc.libcxxDir;
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>
2026-10-02 22:26:31 +02:00
} else if (!tc.prefix.empty() && fs::exists(tc.prefix / "share" / "libc++" / "v1" / "std.cppm")) {
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
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";
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>
2026-10-02 22:26:31 +02:00
}
// 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()) {
fix(mingw): view only a prefix that holds a Linux system 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>
2026-10-31 22:58:30 +01:00
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");
fix: rebuild and relink on what changed, submodules at their revisions, and mingw found where it is - Shaders record the files glslang opened (a depfile) and their settings (source path, stage, entry point, defines, include directories) in the build directory, and rebuild when an included file or a setting changes, not only the shader's own source. - An executable relinks when its link command changed, not only when an object is newer: a link flag in project.cpp, or a dependency's library that moved (an ExternalDependency built into a new cache directory). The command is recorded beside the objects; its flags come from ordered sets, so the order the dependency threads finish in can't change it. - An external's newest artifact counts its libDirs and versioned .so names. - Submodules are checked out at the revisions the fetched commit records, from the URLs its .gitmodules gives, after every clone, switch, checkout and pull, so a pinned commit builds against its own submodules, and one moved by hand is put back; with no .gitmodules, git isn't asked. - Each external clone's fetch and build hold a file lock: parallel builds pulled into one clone at once ("cannot lock ref"). - mingw: - -stdlib=libstdc++ always; - the std module's source is found through libstdc++.modules.json in the mingw toolchain's own library directories (the generic lib/ holds the host's Linux one), asking the compiler once a run; - a toolchain that isn't there is a build error saying where it looked, not std::terminate; - CRAFTER_MINGW_DIR names the prefix, the directory holding x86_64-w64-mingw32/ and lib/gcc/; - inside the Flatpak the host's /usr mingw is used. - RunScript's exit-status check reads as the one range it is. - Tests: ConcurrentExternalFetch, ExternalArtifactTime, GitSubmodules, LinkFlagsChange, MingwMissing, ShaderIncludeChange; ShaderCompile and ToolchainResolve extended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:08:13 +02:00
} 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
fix(mingw): view only a prefix that holds a Linux system 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>
2026-10-31 22:58:30 +01:00
// 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;
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>
2026-10-02 22:26:31 +02:00
}
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&) {}
fix: rebuild and relink on what changed, submodules at their revisions, and mingw found where it is - Shaders record the files glslang opened (a depfile) and their settings (source path, stage, entry point, defines, include directories) in the build directory, and rebuild when an included file or a setting changes, not only the shader's own source. - An executable relinks when its link command changed, not only when an object is newer: a link flag in project.cpp, or a dependency's library that moved (an ExternalDependency built into a new cache directory). The command is recorded beside the objects; its flags come from ordered sets, so the order the dependency threads finish in can't change it. - An external's newest artifact counts its libDirs and versioned .so names. - Submodules are checked out at the revisions the fetched commit records, from the URLs its .gitmodules gives, after every clone, switch, checkout and pull, so a pinned commit builds against its own submodules, and one moved by hand is put back; with no .gitmodules, git isn't asked. - Each external clone's fetch and build hold a file lock: parallel builds pulled into one clone at once ("cannot lock ref"). - mingw: - -stdlib=libstdc++ always; - the std module's source is found through libstdc++.modules.json in the mingw toolchain's own library directories (the generic lib/ holds the host's Linux one), asking the compiler once a run; - a toolchain that isn't there is a build error saying where it looked, not std::terminate; - CRAFTER_MINGW_DIR names the prefix, the directory holding x86_64-w64-mingw32/ and lib/gcc/; - inside the Flatpak the host's /usr mingw is used. - RunScript's exit-status check reads as the one range it is. - Tests: ConcurrentExternalFetch, ExternalArtifactTime, GitSubmodules, LinkFlagsChange, MingwMissing, ShaderIncludeChange; ShaderCompile and ToolchainResolve extended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:08:13 +02:00
if (fs::exists("/.flatpak-info")) in.hostRoot = "/run/host";
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>
2026-10-02 22:26:31 +02:00
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;
}
fix(mingw): view only a prefix that holds a Linux system 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>
2026-10-31 22:58:30 +01:00
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));
}
fix: give the mingw target a sysroot that holds mingw-w64 alone Inside the Flatpak the mingw-w64 prefix is the host's /usr, and it was clang's --sysroot as is. clang's mingw driver takes a sysroot for a tree that holds mingw-w64 alone, and adds its include/ to the include path and its lib/ to the library path. For /usr those are the Linux system's own: every header mingw-w64 lacks came from glibc, and libstdc++'s <ext/atomicity.h>, finding <sys/single_threaded.h>, had shared_ptr read __libc_single_threaded, which nothing on Windows defines. Every program that used one failed to link, among them every Crafter.Graphics Win32 executable, through Crafter.Asset's GLB loader. CRAFTER_MINGW_DIR=/usr did the same outside the Flatpak (#43). The sysroot is now MingwSysroot(): a directory in the cache, one per prefix, holding only links to the prefix's x86_64-w64-mingw32/, lib/gcc/ and lib64/gcc/ (where openSUSE keeps gcc). Through it clang finds mingw-w64, gcc's version and libstdc++ exactly as it does through the mingw gcc on PATH, and nothing else. It is made by the first mingw compile, link or CMake configure of a process, so no other command writes to the cache for it and a failure to make it is that build's error. The Toolchain field is now mingwPrefix, since clang never gets the prefix itself, and it is made absolute when the toolchain is resolved, as it ends up in link targets and directory names. Messages keep naming paths under the prefix rather than under the view. The prefix is also part of the mingw target's VariantId and of its std PCM directory. Units do not track the std PCM they import: when the fixed release built in a directory a broken build had left, the std module was rebuilt and the objects compiled against glibc's headers were only relinked, failing as before. As with a compiler update, a new directory is what gets them rebuilt. It also keeps two releases that share a cache, as the Flatpak's branches do, from rebuilding one std PCM under each other's PCMs, and two prefixes apart. Without a prefix both stay as they were: clang's own discovery has not changed. Two shorter ways do not work: --sysroot=<prefix>/x86_64-w64-mingw32 loses the directory gcc's version names, so <string> is not found, and clang ignores --gcc-toolchain and --gcc-install-dir for this target. Fixes #43 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:48:06 +01:00
const fs::path& Crafter::MingwSysroot() {
fix(mingw): view only a prefix that holds a Linux system 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>
2026-10-31 22:58:30 +01:00
// 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.
fix: give the mingw target a sysroot that holds mingw-w64 alone Inside the Flatpak the mingw-w64 prefix is the host's /usr, and it was clang's --sysroot as is. clang's mingw driver takes a sysroot for a tree that holds mingw-w64 alone, and adds its include/ to the include path and its lib/ to the library path. For /usr those are the Linux system's own: every header mingw-w64 lacks came from glibc, and libstdc++'s <ext/atomicity.h>, finding <sys/single_threaded.h>, had shared_ptr read __libc_single_threaded, which nothing on Windows defines. Every program that used one failed to link, among them every Crafter.Graphics Win32 executable, through Crafter.Asset's GLB loader. CRAFTER_MINGW_DIR=/usr did the same outside the Flatpak (#43). The sysroot is now MingwSysroot(): a directory in the cache, one per prefix, holding only links to the prefix's x86_64-w64-mingw32/, lib/gcc/ and lib64/gcc/ (where openSUSE keeps gcc). Through it clang finds mingw-w64, gcc's version and libstdc++ exactly as it does through the mingw gcc on PATH, and nothing else. It is made by the first mingw compile, link or CMake configure of a process, so no other command writes to the cache for it and a failure to make it is that build's error. The Toolchain field is now mingwPrefix, since clang never gets the prefix itself, and it is made absolute when the toolchain is resolved, as it ends up in link targets and directory names. Messages keep naming paths under the prefix rather than under the view. The prefix is also part of the mingw target's VariantId and of its std PCM directory. Units do not track the std PCM they import: when the fixed release built in a directory a broken build had left, the std module was rebuilt and the objects compiled against glibc's headers were only relinked, failing as before. As with a compiler update, a new directory is what gets them rebuilt. It also keeps two releases that share a cache, as the Flatpak's branches do, from rebuilding one std PCM under each other's PCMs, and two prefixes apart. Without a prefix both stay as they were: clang's own discovery has not changed. Two shorter ways do not work: --sysroot=<prefix>/x86_64-w64-mingw32 loses the directory gcc's version names, so <string> is not found, and clang ignores --gcc-toolchain and --gcc-install-dir for this target. Fixes #43 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:48:06 +01:00
static const fs::path Sysroot = []() -> fs::path {
fix(mingw): view only a prefix that holds a Linux system 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>
2026-10-31 22:58:30 +01:00
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"));
fix: give the mingw target a sysroot that holds mingw-w64 alone Inside the Flatpak the mingw-w64 prefix is the host's /usr, and it was clang's --sysroot as is. clang's mingw driver takes a sysroot for a tree that holds mingw-w64 alone, and adds its include/ to the include path and its lib/ to the library path. For /usr those are the Linux system's own: every header mingw-w64 lacks came from glibc, and libstdc++'s <ext/atomicity.h>, finding <sys/single_threaded.h>, had shared_ptr read __libc_single_threaded, which nothing on Windows defines. Every program that used one failed to link, among them every Crafter.Graphics Win32 executable, through Crafter.Asset's GLB loader. CRAFTER_MINGW_DIR=/usr did the same outside the Flatpak (#43). The sysroot is now MingwSysroot(): a directory in the cache, one per prefix, holding only links to the prefix's x86_64-w64-mingw32/, lib/gcc/ and lib64/gcc/ (where openSUSE keeps gcc). Through it clang finds mingw-w64, gcc's version and libstdc++ exactly as it does through the mingw gcc on PATH, and nothing else. It is made by the first mingw compile, link or CMake configure of a process, so no other command writes to the cache for it and a failure to make it is that build's error. The Toolchain field is now mingwPrefix, since clang never gets the prefix itself, and it is made absolute when the toolchain is resolved, as it ends up in link targets and directory names. Messages keep naming paths under the prefix rather than under the view. The prefix is also part of the mingw target's VariantId and of its std PCM directory. Units do not track the std PCM they import: when the fixed release built in a directory a broken build had left, the std module was rebuilt and the objects compiled against glibc's headers were only relinked, failing as before. As with a compiler update, a new directory is what gets them rebuilt. It also keeps two releases that share a cache, as the Flatpak's branches do, from rebuilding one std PCM under each other's PCMs, and two prefixes apart. Without a prefix both stay as they were: clang's own discovery has not changed. Two shorter ways do not work: --sysroot=<prefix>/x86_64-w64-mingw32 loses the directory gcc's version names, so <string> is not found, and clang ignores --gcc-toolchain and --gcc-install-dir for this target. Fixes #43 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:48:06 +01:00
}
fix(mingw): view only a prefix that holds a Linux system 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>
2026-10-31 22:58:30 +01:00
fs::path view = MakeMingwSysroot(tc.mingwPrefix, parents);
MadeView = view;
ViewedPrefix = tc.mingwPrefix;
ViewMade.store(true, std::memory_order_release);
return view;
fix: give the mingw target a sysroot that holds mingw-w64 alone Inside the Flatpak the mingw-w64 prefix is the host's /usr, and it was clang's --sysroot as is. clang's mingw driver takes a sysroot for a tree that holds mingw-w64 alone, and adds its include/ to the include path and its lib/ to the library path. For /usr those are the Linux system's own: every header mingw-w64 lacks came from glibc, and libstdc++'s <ext/atomicity.h>, finding <sys/single_threaded.h>, had shared_ptr read __libc_single_threaded, which nothing on Windows defines. Every program that used one failed to link, among them every Crafter.Graphics Win32 executable, through Crafter.Asset's GLB loader. CRAFTER_MINGW_DIR=/usr did the same outside the Flatpak (#43). The sysroot is now MingwSysroot(): a directory in the cache, one per prefix, holding only links to the prefix's x86_64-w64-mingw32/, lib/gcc/ and lib64/gcc/ (where openSUSE keeps gcc). Through it clang finds mingw-w64, gcc's version and libstdc++ exactly as it does through the mingw gcc on PATH, and nothing else. It is made by the first mingw compile, link or CMake configure of a process, so no other command writes to the cache for it and a failure to make it is that build's error. The Toolchain field is now mingwPrefix, since clang never gets the prefix itself, and it is made absolute when the toolchain is resolved, as it ends up in link targets and directory names. Messages keep naming paths under the prefix rather than under the view. The prefix is also part of the mingw target's VariantId and of its std PCM directory. Units do not track the std PCM they import: when the fixed release built in a directory a broken build had left, the std module was rebuilt and the objects compiled against glibc's headers were only relinked, failing as before. As with a compiler update, a new directory is what gets them rebuilt. It also keeps two releases that share a cache, as the Flatpak's branches do, from rebuilding one std PCM under each other's PCMs, and two prefixes apart. Without a prefix both stay as they were: clang's own discovery has not changed. Two shorter ways do not work: --sysroot=<prefix>/x86_64-w64-mingw32 loses the directory gcc's version names, so <string> is not found, and clang ignores --gcc-toolchain and --gcc-install-dir for this target. Fixes #43 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:48:06 +01:00
}();
return Sysroot;
}
fix(mingw): view only a prefix that holds a Linux system 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>
2026-10-31 22:58:30 +01:00
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;
}
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
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())};
}
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>
2026-10-02 22:26:31 +02:00
std::string_view Crafter::CrafterBuildVersion() {
return CRAFTER_BUILD_VERSION;
}