From 06b61e84f9c8caa450ffe51e1bd135e47133eba9 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sat, 31 Oct 2026 22:58:30 +0100 Subject: [PATCH] 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, 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 --- implementations/Crafter.Build-Clang.cpp | 8 +- implementations/Crafter.Build-Platform.cpp | 25 +++-- implementations/Crafter.Build-Toolchain.cpp | 114 +++++++++++++++----- interfaces/Crafter.Build-Toolchain.cppm | 46 +++++--- tests/MingwSysroot/fixture/failing.cpp | 6 ++ tests/MingwSysroot/main.cpp | 106 +++++++++++++----- tests/SysrootCompileFlags/main.cpp | 13 +++ tests/ToolchainResolve/main.cpp | 26 +++++ 8 files changed, 264 insertions(+), 80 deletions(-) create mode 100644 tests/MingwSysroot/fixture/failing.cpp diff --git a/implementations/Crafter.Build-Clang.cpp b/implementations/Crafter.Build-Clang.cpp index f5943ca..138ffd7 100644 --- a/implementations/Crafter.Build-Clang.cpp +++ b/implementations/Crafter.Build-Clang.cpp @@ -935,9 +935,9 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map globs, std::string_view name std::string Crafter::RunBuildCommand(std::string_view cmd) { Progress::EchoCommand(cmd); CommandResult r = RunCommandChecked(cmd); + // A mingw build against a system prefix sees mingw-w64 through a view in + // the cache, and clang names every header by the path it found it at. + r.output = MingwViewToPrefix(std::move(r.output)); if (r.exitCode == 0) { // Warnings from a step that succeeded. Show them — until now they were @@ -737,10 +740,13 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) { // library, naming the std module's source relative to itself. It is // looked for in the library directories the compiler searches for // this target, as it would look for libstdc++ itself, but only in - // those that belong to the mingw toolchain: the list also has the - // generic /lib, where the host's own (Linux) libstdc++ keeps - // its manifest. A toolchain that isn't there is a build error, not - // an exception: Build() runs this with its worker threads started. + // those that belong to the mingw toolchain. The list also holds + // clang's resource directory, the sysroot's generic lib/, where a + // prefix given whole can keep a libstdc++ built for another target, + // and, when clang finds no gcc directory, an empty entry that would + // name a manifest in the working directory. A toolchain that isn't + // there is a build error, not an exception: Build() runs this with + // its worker threads started. const Toolchain& tc = GetToolchain(); const std::string where = tc.mingwPrefix.empty() ? std::string("where clang finds it by the x86_64-w64-mingw32 gcc on PATH") @@ -775,13 +781,10 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) { } } if (manifestPath.empty()) return missing; - // Found through the view MingwSysroot() makes of the prefix, and - // named from here on by its place in the prefix, which is the - // directory the user knows and can go and look at. - if (!tc.mingwPrefix.empty()) { - const fs::path inPrefix = manifestPath.lexically_relative(MingwSysroot()); - if (!inPrefix.empty() && *inPrefix.begin() != "..") manifestPath = tc.mingwPrefix / inPrefix; - } + // Found through the view MingwSysroot() may have made of the prefix, + // and named from here on by its place in the prefix, the directory + // the user knows and can go and look at. + manifestPath = fs::path(MingwViewToPrefix(manifestPath.string())); const std::optional 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(); diff --git a/implementations/Crafter.Build-Toolchain.cpp b/implementations/Crafter.Build-Toolchain.cpp index c75f627..84a1de3 100644 --- a/implementations/Crafter.Build-Toolchain.cpp +++ b/implementations/Crafter.Build-Toolchain.cpp @@ -54,9 +54,26 @@ namespace { 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); + throw fs::filesystem_error("cannot make a link", target, link, ec); } } + + // `dir` made absolute and spelled one way, once, when the toolchain is + // resolved. The mingw prefix ends up as the target of links and in the + // names of build and cache directories: a relative path read later from + // another working directory would name something else, and /opt/mingw/ + // next to /opt/mingw would rebuild everything for the same directory. + fs::path PrefixPath(const fs::path& dir) { + fs::path normal = fs::absolute(dir).lexically_normal(); + if (!normal.has_filename() && normal.has_relative_path()) normal = normal.parent_path(); + return normal; + } + + // The view MingwSysroot() made and the prefix it shows, for + // MingwViewToPrefix. Written once, before ViewMade is set. + fs::path MadeView; + fs::path ViewedPrefix; + std::atomic ViewMade = false; } std::string Toolchain::LibcxxCompileFlags() const { @@ -150,14 +167,19 @@ Toolchain Crafter::ResolveToolchain(const ToolchainInputs& in) { tc.wasiSysroot = "/usr/share/wasi-sysroot"; } if (std::string_view dir = Env(in, "CRAFTER_MINGW_DIR"); !dir.empty()) { - // 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)); + tc.mingwPrefix = PrefixPath(fs::path(dir)); + // A prefix that holds a Linux system as well as mingw-w64, /usr + // itself, has that system's C library in include/: features.h, which + // every glibc and musl header includes and mingw-w64 has no use for. + // One that holds mingw-w64 alone stays the sysroot, so Windows + // libraries installed into its include/ and lib/ stay visible. + tc.mingwPrefixIsSystem = fs::exists(tc.mingwPrefix / "include" / "features.h"); } else if (!in.hostRoot.empty() && fs::exists(in.hostRoot / "usr" / "x86_64-w64-mingw32")) { // The sandbox has no mingw-w64 of its own, and no mingw gcc on PATH - // for clang to find one by. - tc.mingwPrefix = in.hostRoot / "usr"; + // for clang to find one by. The host's /usr is the host's system, + // whether or not its C library's headers are installed. + tc.mingwPrefix = PrefixPath(in.hostRoot / "usr"); + tc.mingwPrefixIsSystem = true; } return tc; } @@ -203,31 +225,69 @@ 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); +fs::path Crafter::MakeMingwSysroot(const fs::path& prefix, std::span 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{}(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()); } - return sysroot; + } + throw std::runtime_error(std::format("cannot make a sysroot that shows clang only the mingw-w64 parts of {}{}", prefix.string(), reasons)); +} + +const fs::path& Crafter::MingwSysroot() { + // Made on first use rather than by GetToolchain(), so that only what + // compiles for the mingw target, a build or an AST lint of one, writes + // anything for it, and a failure surfaces as that command's error + // instead of on every command. + static const fs::path Sysroot = []() -> fs::path { + const Toolchain& tc = GetToolchain(); + if (!tc.mingwPrefixIsSystem) return tc.mingwPrefix; + // In the cache, or where the XDG spec promises symbolic links when + // the cache's filesystem takes none (vfat, exFAT, CIFS without unix + // extensions), which costs nothing else crafter-build caches. + // Absolute, because CMake runs the compiler from directories of its + // own. + std::vector parents = { fs::absolute(GetCacheDir()) }; + if (const char* runtime = std::getenv("XDG_RUNTIME_DIR"); runtime && *runtime) { + parents.push_back(fs::absolute(fs::path(runtime) / "crafter.build")); + } + fs::path view = MakeMingwSysroot(tc.mingwPrefix, parents); + MadeView = view; + ViewedPrefix = tc.mingwPrefix; + ViewMade.store(true, std::memory_order_release); + return view; }(); return Sysroot; } +std::string Crafter::MingwViewToPrefix(std::string text) { + if (!ViewMade.load(std::memory_order_acquire)) return text; + // With the separator, so a --sysroot= naming the view itself, in a + // command line the output quotes, stays what was run. + const std::string view = (MadeView / "").string(); + const std::string prefix = (ViewedPrefix / "").string(); + for (std::size_t at = text.find(view); at != std::string::npos; at = text.find(view, at + prefix.size())) { + text.replace(at, view.size(), prefix); + } + return text; +} + std::int32_t Crafter::LibcxxVersion(const fs::path& includeDir) { // "# define _LIBCPP_VERSION 230102", the one place libc++ states its // release in a form the preprocessor does not have to be run for. diff --git a/interfaces/Crafter.Build-Toolchain.cppm b/interfaces/Crafter.Build-Toolchain.cppm index d6b360a..6f1b68c 100644 --- a/interfaces/Crafter.Build-Toolchain.cppm +++ b/interfaces/Crafter.Build-Toolchain.cppm @@ -39,14 +39,20 @@ 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), as an absolute path. Empty means clang's own - // discovery, which follows the mingw gcc on PATH. Set by + // distro package), absolute and lexically normal. 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. + // it has mingw-w64 (the sandbox's own /usr never does). What the + // mingw target passes as --sysroot is MingwSysroot(), not this: + // see mingwPrefixIsSystem. fs::path mingwPrefix; + // The prefix holds a Linux system as well as mingw-w64: the host's + // /usr in the sandbox, or a prefix whose include/ has a C library's + // features.h, as /usr has. clang searches a mingw sysroot's include/ + // and lib/, so such a prefix is shown to it only through a view of + // its mingw-w64 parts; any other prefix is the sysroot itself, and + // Windows libraries installed into its include/ and lib/ are found. + bool mingwPrefixIsSystem = false; // `clang++ -print-target-triple` and the first line of // `clang++ --version`; both empty when clang++ could not be run. std::string hostTriple; @@ -89,18 +95,32 @@ export namespace Crafter { const Toolchain& GetToolchain(); // GetToolchain(), or a std::runtime_error carrying its error. const Toolchain& RequireToolchain(); + // Makes, in the first of `parents` that can hold one, a directory that + // shows clang only the mingw-w64 parts of `prefix`: links named + // x86_64-w64-mingw32, lib/gcc and lib64/gcc to the same paths under + // `prefix`, at /mingw-sysroot/, and returns it. + // A view already there is reused; one another process makes at the same + // moment is as good as one made here. Throws when no parent can hold + // one, with the reason for each. + fs::path MakeMingwSysroot(const fs::path& prefix, std::span parents); // 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 + // target, settled on the first call of a process: empty without a + // mingwPrefix, the prefix itself for one that holds mingw-w64 alone, and + // for a system prefix the view MakeMingwSysroot makes in crafter-build's + // cache, or in $XDG_RUNTIME_DIR/crafter.build when the cache's + // filesystem takes no symbolic links. A system 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 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(); + // `text` with every path into the view MingwSysroot() made written as + // the same path into the prefix it shows, so that compiler messages name + // a header where the user installed it rather than in a hashed cache + // directory; `text` unchanged when no view was made. + std::string MingwViewToPrefix(std::string text); // _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/MingwSysroot/fixture/failing.cpp b/tests/MingwSysroot/fixture/failing.cpp new file mode 100644 index 0000000..254ec5b --- /dev/null +++ b/tests/MingwSysroot/fixture/failing.cpp @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® + +// Stops in a header of the stand-in mingw-w64, so that the error clang +// reports is located in one. +#include diff --git a/tests/MingwSysroot/main.cpp b/tests/MingwSysroot/main.cpp index 2388acf..3964b36 100644 --- a/tests/MingwSysroot/main.cpp +++ b/tests/MingwSysroot/main.cpp @@ -17,17 +17,17 @@ using namespace Crafter; // did the same outside the Flatpak. // // The prefix staged here has the shape of such a /usr and needs no real -// mingw-w64: glibc's in include/, gcc's per-target -// directory, which clang derives libstdc++'s header directory from, and a -// stand-in mingw-w64 tree whose libstdc++ has a std module with nothing in -// it. The fixture is a static library, so nothing is linked, and its C++ and -// C sources stop the compile when they see the prefix's include/ or miss -// mingw-w64's or libstdc++'s headers. +// mingw-w64: glibc's features.h and in include/, +// gcc's per-target directory, which clang derives libstdc++'s header +// directory from, and a stand-in mingw-w64 tree whose libstdc++ has a std +// module with nothing in it. The fixture is a static library, so nothing is +// linked, and its C++ and C sources stop the compile when they see the +// prefix's include/ or miss mingw-w64's or libstdc++'s headers. // -// The std module such a build compiles is not the one releases before the -// fix compiled against the prefix itself, so it has to live in a directory -// of its own: the Flatpak's branches share one cache, and two releases -// taking turns rebuilding one PCM do it under the PCMs built against it. +// clang reaches such a prefix through a view of it (MakeMingwSysroot), which +// has to be made where symbolic links can be, and a compile error in one of +// its headers has to name the header where it is installed, not in the +// view. extern "C" int setenv(const char* name, const char* value, int overwrite); @@ -45,6 +45,26 @@ namespace { fs::create_directories(file.parent_path()); std::ofstream(file, std::ios::binary | std::ios::trunc) << text; } + + BuildResult BuildLibrary(const fs::path& project, std::string_view name, const fs::path& cFile) { + Configuration lib; + lib.path = project; + lib.name = std::string(name); + lib.outputName = std::string(name); + lib.target = "x86_64-w64-mingw32"; + // Spelled out: the default, native, names the host's CPU, which is + // no x86 CPU on an aarch64 host. + lib.march = "x86-64"; + lib.mtune = "x86-64"; + lib.type = ConfigurationType::LibraryStatic; + std::array ifaces = {}; + std::array impls = { fs::path(name) }; + lib.GetInterfacesAndImplementations(ifaces, impls); + if (!cFile.empty()) lib.cFiles = { cFile }; + std::unordered_map> depResults; + std::mutex depMutex; + return Build(lib, depResults, depMutex); + } } int main() { @@ -56,37 +76,71 @@ int main() { fs::copy(fs::current_path() / "tests" / "MingwSysroot" / "fixture", project, fs::copy_options::recursive); const fs::path prefix = scratch / "usr"; + Write(prefix / "include" / "features.h", "#define __GLIBC__ 2\n"); Write(prefix / "include" / "sys" / "single_threaded.h", "extern char __libc_single_threaded;\n"); fs::create_directories(prefix / "lib" / "gcc" / "x86_64-w64-mingw32" / "15.1.0"); const fs::path mingw = prefix / "x86_64-w64-mingw32"; Write(mingw / "include" / "_mingw.h", ""); + Write(mingw / "include" / "crafter_probe_error.h", "#error stopped in a mingw-w64 header\n"); Write(mingw / "include" / "c++" / "15.1.0" / "memory", ""); // Through std::format because the build's import scanner reads string // literals as code: one holding "module std;" made this file a unit of a // module std, and the test did not build. Write(mingw / "include" / "c++" / "15.1.0" / "bits" / "std.cc", std::format("export module {};\n", "std")); Write(mingw / "lib" / "libstdc++.modules.json", R"({"version": 1, "modules": [{"logical-name": "std", "source-path": "../include/c++/15.1.0/bits/std.cc", "is-std-library": true}]})"); + + { + // The view is made in the first place that can hold one. A cache on + // a filesystem that takes no symbolic links (vfat, exFAT, some CIFS + // and FUSE mounts) holds everything else crafter-build caches, so + // its failure must not be every mingw build's: this one can make + // nothing under mingw-sysroot, which is a file. + const fs::path blocked = scratch / "blocked"; + Write(blocked / "mingw-sysroot", ""); + const fs::path runtime = scratch / "runtime"; + const std::array parents = { blocked, runtime }; + fs::path view; + try { + view = MakeMingwSysroot(prefix, parents); + } catch (const std::exception& e) { + Check(false, std::format("a view is made in the second place when the first can hold none: {}", e.what())); + } + if (!view.empty()) { + Check(view.string().starts_with(runtime.string()), std::format("the view is in the place that could hold it: {}", view.string())); + Check(fs::equivalent(view / "x86_64-w64-mingw32", mingw, ec), "the view shows the prefix's mingw-w64 tree"); + Check(fs::equivalent(view / "lib" / "gcc", prefix / "lib" / "gcc", ec), "and gcc's directories"); + Check(!fs::exists(view / "include") && !fs::exists(view / "lib" / "libstdc++.modules.json"), "and nothing else of the prefix's include/ and lib/"); + fs::path again; + try { + again = MakeMingwSysroot(prefix, parents); + } catch (const std::exception& e) { + Check(false, std::format("a view already there is reused: {}", e.what())); + } + Check(again == view, "a view already there is reused"); + } + const std::array nowhere = { blocked }; + std::string error; + try { + MakeMingwSysroot(prefix, nowhere); + } catch (const std::exception& e) { + error = e.what(); + } + Check(error.contains(prefix.string()) && error.contains((blocked / "mingw-sysroot").string()), std::format("with no place for a view, the error names the prefix and why each place failed: {}", error)); + } + // Before anything resolves the toolchain, which reads both once. The - // std module cache goes to the scratch directory too. + // view and the std module cache go to the scratch directory too. setenv("CRAFTER_MINGW_DIR", prefix.c_str(), 1); setenv("XDG_CACHE_HOME", (scratch / "cache").c_str(), 1); - Configuration lib; - lib.path = project; - lib.name = "probe"; - lib.outputName = "probe"; - lib.target = "x86_64-w64-mingw32"; - lib.type = ConfigurationType::LibraryStatic; - std::array ifaces = {}; - std::array impls = { "probe" }; - lib.GetInterfacesAndImplementations(ifaces, impls); - lib.cFiles = { project / "cprobe" }; - std::unordered_map> depResults; - std::mutex depMutex; - const BuildResult built = Build(lib, depResults, depMutex); + const BuildResult built = BuildLibrary(project, "probe", project / "cprobe"); Check(built.result.empty(), std::format("a Windows build sees mingw-w64's and libstdc++'s headers and not the prefix's own: {}", built.result)); - const std::string unkeyed = std::format("{}-{}-{}", lib.target, lib.march, GetToolchain().Key()); - Check(StdPcmDir(lib).filename() != unkeyed, std::format("the std module of a mingw build with a prefix is not cached where older releases built it against the prefix itself: {}", StdPcmDir(lib).string())); + Check(MingwSysroot() != prefix && MingwSysroot().string().starts_with((scratch / "cache").string()), std::format("a prefix holding a Linux system reaches clang through a view in the cache: {}", MingwSysroot().string())); + + const BuildResult failed = BuildLibrary(project, "failing", {}); + const std::string header = (mingw / "include" / "crafter_probe_error.h").string(); + Check(failed.result.contains("stopped in a mingw-w64 header"), std::format("an error in a mingw-w64 header fails the build: {}", failed.result)); + Check(failed.result.contains(header) && !failed.result.contains(MingwSysroot().string()), std::format("and names the header in the prefix, not in the view: {}", failed.result)); fs::remove_all(scratch, ec); if (failures > 0) { diff --git a/tests/SysrootCompileFlags/main.cpp b/tests/SysrootCompileFlags/main.cpp index bfaf95b..6734089 100644 --- a/tests/SysrootCompileFlags/main.cpp +++ b/tests/SysrootCompileFlags/main.cpp @@ -19,6 +19,9 @@ using namespace Crafter; // sysroot's copy. It must not do so where there is no sysroot, for wasm (which // has its own sysroot handling) or for mingw (libstdc++, no libc++ at all). // The command is checked as text: no compiler runs. + +extern "C" int setenv(const char* name, const char* value, int overwrite); + namespace { std::int32_t Failures = 0; @@ -44,6 +47,15 @@ namespace { } int main() { + // A compile command names the std module's directory in the cache, and + // the mingw one can make the view of a system mingw prefix there + // (MingwSysroot), as with CRAFTER_MINGW_DIR=/usr or inside the Flatpak + // on a host with mingw-w64. Set before anything resolves the toolchain. + const fs::path scratch = fs::temp_directory_path() / "crafter-build-sysroot-compile-flags-cache"; + std::error_code ec; + fs::remove_all(scratch, ec); + setenv("XDG_CACHE_HOME", scratch.c_str(), 1); + const std::string sysroot = "/opt/crafter-build-test-sysroot"; const std::string forced = std::format("-nostdinc++ -isystem {}/usr/include/c++/v1", sysroot); @@ -57,6 +69,7 @@ int main() { Check(!CommandFor("wasm32-wasip1", sysroot).contains(forced), "wasm: left to its own sysroot handling"); Check(!CommandFor("x86_64-w64-mingw32", sysroot).contains(forced), "mingw: libstdc++ target, libc++ headers are not forced"); + fs::remove_all(scratch, ec); if (Failures > 0) { std::println(std::cerr, "{} assertions failed", Failures); return 1; diff --git a/tests/ToolchainResolve/main.cpp b/tests/ToolchainResolve/main.cpp index 1912418..d92a1c8 100644 --- a/tests/ToolchainResolve/main.cpp +++ b/tests/ToolchainResolve/main.cpp @@ -147,6 +147,31 @@ int main() { 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"); + // The prefix names directories, so one directory has one spelling: + // otherwise a trailing slash rebuilds every mingw build for nothing. + for (const char* spelling : {"/opt/mingw/", "/opt//mingw", "/opt/./mingw", "/opt/x/../mingw"}) { + in.env["CRAFTER_MINGW_DIR"] = spelling; + Check(ResolveToolchain(in).mingwPrefix == "/opt/mingw", std::format("{} is spelled /opt/mingw (got '{}')", spelling, ResolveToolchain(in).mingwPrefix.string())); + } + } + { + // A prefix that holds a Linux system's C library, as /usr does, is + // shown to clang through a view; one that holds mingw-w64 alone is + // the sysroot itself, Windows libraries in its include/ and lib/ + // included. + const fs::path staged = fs::temp_directory_path() / "crafter-build-toolchain-mingw-prefixes"; + fs::remove_all(staged); + Touch(staged / "system" / "include" / "features.h"); + Touch(staged / "system" / "x86_64-w64-mingw32" / "include" / "_mingw.h"); + Touch(staged / "dedicated" / "include" / "SDL2" / "SDL.h"); + Touch(staged / "dedicated" / "x86_64-w64-mingw32" / "include" / "_mingw.h"); + ToolchainInputs in; + in.env["CRAFTER_MINGW_DIR"] = (staged / "system").string(); + Check(ResolveToolchain(in).mingwPrefixIsSystem, "a prefix with a C library's features.h holds a Linux system"); + in.env["CRAFTER_MINGW_DIR"] = (staged / "dedicated").string(); + Check(!ResolveToolchain(in).mingwPrefixIsSystem, "a prefix of mingw-w64 and Windows libraries does not"); + Check(!ResolveToolchain({}).mingwPrefixIsSystem, "no prefix, nothing to show"); + fs::remove_all(staged); } { // Sandboxed, the host's /usr stands in for the missing mingw-w64, @@ -158,6 +183,7 @@ int main() { 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).mingwPrefix == host / "usr", "a host with mingw-w64 gives its /usr as the mingw prefix"); + Check(ResolveToolchain(in).mingwPrefixIsSystem, "the host's /usr is the host's system, with or without its C library's headers"); in.env["CRAFTER_MINGW_DIR"] = "/opt/mingw"; Check(ResolveToolchain(in).mingwPrefix == "/opt/mingw", "CRAFTER_MINGW_DIR wins over the host's"); fs::remove_all(host);