From ab998081e1b0dbd4709bf6a74fae81fd7dad8563 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sat, 31 Oct 2026 20:48:06 +0100 Subject: [PATCH] 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 , finding , 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=/x86_64-w64-mingw32 loses the directory gcc's version names, so is not found, and clang ignores --gcc-toolchain and --gcc-install-dir for this target. Fixes #43 Co-Authored-By: Claude Opus 5.5 --- implementations/Crafter.Build-Clang.cpp | 17 +++++--- implementations/Crafter.Build-External.cpp | 2 +- implementations/Crafter.Build-Platform.cpp | 15 +++++-- implementations/Crafter.Build-Toolchain.cpp | 46 ++++++++++++++++++++- interfaces/Crafter.Build-Clang.cppm | 11 +++++ interfaces/Crafter.Build-Toolchain.cppm | 25 ++++++++--- tests/ToolchainResolve/main.cpp | 12 +++--- 7 files changed, 106 insertions(+), 22 deletions(-) diff --git a/implementations/Crafter.Build-Clang.cpp b/implementations/Crafter.Build-Clang.cpp index c37c362..f5943ca 100644 --- a/implementations/Crafter.Build-Clang.cpp +++ b/implementations/Crafter.Build-Clang.cpp @@ -265,6 +265,13 @@ fs::path Crafter::StdPcmDir(const Configuration& config) { if (config.target.starts_with("wasm32")) { stdPcmKey += std::format("-{:08x}", static_cast(std::hash{}(WasmBaselineFlags))); } + // The same goes for the mingw-w64 prefix the mingw std module is + // compiled against: releases up to the fix for issue #43 gave clang the + // prefix itself as the sysroot and the ones after give it a view of it, + // and two prefixes are two sets of headers. + if (config.target == "x86_64-w64-mingw32" && !GetToolchain().mingwPrefix.empty()) { + stdPcmKey += std::format("-{:08x}", static_cast(std::hash{}(GetToolchain().mingwPrefix.string()))); + } for (const std::string& f : config.wasmVariantFlags) { stdPcmKey += "+"; for (char c : f) stdPcmKey += (c == '/' || c == '\\') ? '_' : c; @@ -928,9 +935,9 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map source = StdModuleSourcePath(FileBytes(manifestPath)); if (!source) return std::format("{} names no std module", manifestPath.string()); const fs::path stdCc = (manifestPath.parent_path() / *source).lexically_normal(); @@ -862,9 +869,9 @@ std::string Crafter::GetBaseCommand(const Configuration& config) { if (config.target == "x86_64-w64-mingw32") { // libstdc++ from the mingw toolchain, said outright: a clang built to // default to libc++ (the Flatpak's) would otherwise reach for that. - // Found by clang's own mingw discovery unless there is a sysroot. + // Found by clang's own mingw discovery unless there is a prefix. command += " -stdlib=libstdc++"; - if (!tc.mingwSysroot.empty()) command += std::format(" --sysroot={}", tc.mingwSysroot.string()); + if (const fs::path& sysroot = MingwSysroot(); !sysroot.empty()) command += std::format(" --sysroot={}", sysroot.string()); return command; } command += " -stdlib=libc++"; diff --git a/implementations/Crafter.Build-Toolchain.cpp b/implementations/Crafter.Build-Toolchain.cpp index 68d9f8f..c75f627 100644 --- a/implementations/Crafter.Build-Toolchain.cpp +++ b/implementations/Crafter.Build-Toolchain.cpp @@ -43,6 +43,20 @@ namespace { 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 the mingw-w64 sysroot view", target, link, ec); + } + } } std::string Toolchain::LibcxxCompileFlags() const { @@ -136,11 +150,14 @@ Toolchain Crafter::ResolveToolchain(const ToolchainInputs& in) { tc.wasiSysroot = "/usr/share/wasi-sysroot"; } if (std::string_view dir = Env(in, "CRAFTER_MINGW_DIR"); !dir.empty()) { - tc.mingwSysroot = fs::path(dir); + // Made absolute here, once: the prefix becomes the target of links + // in the cache and part of directory names, and a relative path read + // later from another working directory would name something else. + tc.mingwPrefix = fs::absolute(fs::path(dir)); } 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. - tc.mingwSysroot = in.hostRoot / "usr"; + tc.mingwPrefix = in.hostRoot / "usr"; } return tc; } @@ -186,6 +203,31 @@ const Toolchain& Crafter::RequireToolchain() { return tc; } +const fs::path& Crafter::MingwSysroot() { + // Made on first use rather than by GetToolchain(), so only a mingw build + // writes to the cache for it, and a failure surfaces as that build's + // error instead of on every command. + static const fs::path Sysroot = []() -> fs::path { + const fs::path& prefix = GetToolchain().mingwPrefix; + if (prefix.empty()) return {}; + // A view per prefix keeps two shells with different + // CRAFTER_MINGW_DIRs, which share one cache, from rewriting each + // other's. + const fs::path sysroot = GetCacheDir() / "mingw-sysroot" / std::format("{:016x}", std::hash{}(prefix.string())); + // 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; + }(); + return Sysroot; +} + 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. diff --git a/interfaces/Crafter.Build-Clang.cppm b/interfaces/Crafter.Build-Clang.cppm index b4bbc68..89583cc 100644 --- a/interfaces/Crafter.Build-Clang.cppm +++ b/interfaces/Crafter.Build-Clang.cppm @@ -628,6 +628,17 @@ export namespace Crafter { compileKey += "|B:"; compileKey += WasmBaselineFlags; } + // The mingw-w64 prefix decides what the mingw target compiles + // against, and until issue #43 clang got it as the sysroot + // itself, which put the host's own headers on the include path. + // Unkeyed, the release that fixed it kept the objects compiled + // against glibc's headers: the build after the update only + // relinked them, and failed to link as before. No prefix keeps + // the id as it was, since clang's own discovery has not changed. + if (target == "x86_64-w64-mingw32" && !toolchain.mingwPrefix.empty()) { + compileKey += "|M:"; + compileKey += toolchain.mingwPrefix.string(); + } // Sorted by ApplyStandardArgs, so flag order on the command line // doesn't split the cache. for (const std::string& a : projectArgs) { diff --git a/interfaces/Crafter.Build-Toolchain.cppm b/interfaces/Crafter.Build-Toolchain.cppm index 797fd94..d6b360a 100644 --- a/interfaces/Crafter.Build-Toolchain.cppm +++ b/interfaces/Crafter.Build-Toolchain.cppm @@ -39,11 +39,14 @@ export namespace Crafter { fs::path wasiSysroot; // The mingw-w64 install prefix, the directory that holds // x86_64-w64-mingw32/ and lib/gcc/x86_64-w64-mingw32/ (/usr for a - // distro package), passed to clang as --sysroot for the mingw target. - // Empty means clang's own discovery, which follows the mingw gcc on - // PATH. Set by CRAFTER_MINGW_DIR, else the host's /usr when running - // sandboxed and it has mingw-w64 (the sandbox's own /usr never does). - fs::path mingwSysroot; + // distro package), as an absolute path. Empty means clang's own + // discovery, which follows the mingw gcc on PATH. Set by + // CRAFTER_MINGW_DIR, else the host's /usr when running sandboxed and + // it has mingw-w64 (the sandbox's own /usr never does). clang never + // gets the prefix itself: the mingw target's --sysroot is + // MingwSysroot(), a view of it. Keyed into the mingw target's + // VariantId and std PCM directory. + fs::path mingwPrefix; // `clang++ -print-target-triple` and the first line of // `clang++ --version`; both empty when clang++ could not be run. std::string hostTriple; @@ -86,6 +89,18 @@ export namespace Crafter { const Toolchain& GetToolchain(); // GetToolchain(), or a std::runtime_error carrying its error. const Toolchain& RequireToolchain(); + // The --sysroot of every compile, link and CMake configure for the mingw + // target: a directory in crafter-build's cache holding only links to the + // x86_64-w64-mingw32/, lib/gcc/ and lib64/gcc/ of GetToolchain()'s + // mingwPrefix, made on the first call of a process. Empty without a + // prefix. The prefix cannot be the sysroot: clang takes a mingw sysroot + // for a tree that holds mingw-w64 alone and searches its include/ and + // lib/ as well, which for /usr (the host's, in the Flatpak) are the + // Linux system's own. A Windows build then saw glibc's headers, and + // libstdc++, finding , had shared_ptr read + // __libc_single_threaded, which nothing on Windows defines (issue #43). + // Throws when the view cannot be made. + const fs::path& MingwSysroot(); // _LIBCPP_VERSION as the <__config> in `includeDir` (an include/c++/v1) // defines it, XXYYZZ; 0 when the file is missing or has no such line. std::int32_t LibcxxVersion(const fs::path& includeDir); diff --git a/tests/ToolchainResolve/main.cpp b/tests/ToolchainResolve/main.cpp index 81969eb..1912418 100644 --- a/tests/ToolchainResolve/main.cpp +++ b/tests/ToolchainResolve/main.cpp @@ -61,7 +61,7 @@ int main() { Check(tc.libcxxVersion == LibcxxVersion("/usr/include/c++/v1"), "the system libc++ headers state the release of the system module source"); Check(tc.stdModulesDir.empty(), "no crafter-build prefix, no shipped module sources"); Check(tc.wasiSysroot == "/usr/share/wasi-sysroot", "the system wasi sysroot is the default"); - Check(tc.mingwSysroot.empty(), "outside a sandbox, clang finds mingw-w64 itself: no --sysroot"); + Check(tc.mingwPrefix.empty(), "outside a sandbox, clang finds mingw-w64 itself: no prefix"); Check(tc.LibcxxCompileFlags().empty() && tc.LibcxxLinkFlags().empty(), "the PATH fallback adds no libc++ flags"); } { @@ -144,7 +144,9 @@ int main() { in.env["CRAFTER_MINGW_DIR"] = "/opt/mingw"; Toolchain tc = ResolveToolchain(in); Check(tc.wasiSysroot == "/opt/wasi", "CRAFTER_BUILD_WASI_SYSROOT overrides the wasi sysroot"); - Check(tc.mingwSysroot == "/opt/mingw", "CRAFTER_MINGW_DIR names the mingw prefix passed as --sysroot"); + Check(tc.mingwPrefix == "/opt/mingw", "CRAFTER_MINGW_DIR names the mingw prefix"); + in.env["CRAFTER_MINGW_DIR"] = "mingw"; + Check(ResolveToolchain(in).mingwPrefix == fs::absolute("mingw"), "a relative CRAFTER_MINGW_DIR is resolved against the working directory once, when the toolchain is"); } { // Sandboxed, the host's /usr stands in for the missing mingw-w64, @@ -153,11 +155,11 @@ int main() { fs::remove_all(host); ToolchainInputs in; in.hostRoot = host; - Check(ResolveToolchain(in).mingwSysroot.empty(), "a host without mingw-w64 gives no --sysroot"); + Check(ResolveToolchain(in).mingwPrefix.empty(), "a host without mingw-w64 gives no mingw prefix"); fs::create_directories(host / "usr" / "x86_64-w64-mingw32"); - Check(ResolveToolchain(in).mingwSysroot == host / "usr", "a host with mingw-w64 gives its /usr as the mingw prefix"); + Check(ResolveToolchain(in).mingwPrefix == host / "usr", "a host with mingw-w64 gives its /usr as the mingw prefix"); in.env["CRAFTER_MINGW_DIR"] = "/opt/mingw"; - Check(ResolveToolchain(in).mingwSysroot == "/opt/mingw", "CRAFTER_MINGW_DIR wins over the host's"); + Check(ResolveToolchain(in).mingwPrefix == "/opt/mingw", "CRAFTER_MINGW_DIR wins over the host's"); fs::remove_all(host); } {