diff --git a/AGENTS.template.md b/AGENTS.template.md index cb23933..b60a81a 100644 --- a/AGENTS.template.md +++ b/AGENTS.template.md @@ -22,7 +22,7 @@ crafter-build is a C++26 modules build system. A project is described by a `proj Partitions and what they own: -- `:Toolchain` resolves the compiler once per process (`GetToolchain`): clang++, clang, llvm-ar, libclang, libc++'s std module source and the default sysroots, from `CRAFTER_BUILD_LLVM_DIR`, else a toolchain installed beside crafter-build, else PATH. It also holds the version baked in at build time. `SysrootStdModule` picks the std module source for a sysroot build by the sysroot's libc++ release. +- `:Toolchain` resolves the compiler once per process (`GetToolchain`): clang++, clang, llvm-ar, libclang, libc++'s std module source and the default sysroots, from `CRAFTER_BUILD_LLVM_DIR`, else a toolchain installed beside crafter-build, else PATH. It also holds the version baked in at build time. `SysrootStdModule` picks the std module source for a sysroot build by the sysroot's libc++ release. `MingwSysroot` makes the mingw target's `--sysroot`, a view in the cache of the mingw-w64 prefix's own directories: the prefix itself is never a sysroot, since clang would search its `include/` and `lib/`, and for `/usr` those are the Linux system's (issue #43). - `:Clang` holds `Configuration`, `Build()`, `VariantId()`, `ApplyStandardArgs`, `HostTarget`, the CLI entry point `Crafter::Run`, and the lint, test and script data types. - `:Platform` runs commands, implements `LoadProject` (compile and load `project.cpp`), the host PCM cache, `#pragma crafter dependency`, and all OS-specific code. - `:Interface` and `:Implementation` model module units, their staleness checks and their compiles. @@ -118,14 +118,14 @@ Beyond the rules: - There is no host-platform preprocessor split: the host is Linux. Target differences (mingw, wasm) branch on `cfg.target` strings at runtime, in the library as well as in `project.cpp` and tests. - A build step fails when its command exits nonzero, never because it printed. Use `RunBuildCommand` or `RunCommandChecked`; warnings go to `Progress::Diagnostic`. - Every output path goes through `Configuration::BinDir()`, `BuildDir()` and `PcmDir()`. Hand-rolled `path / "bin" / ...` clobbered variants before; do not add new ones. -- Anything that changes what gets compiled or bundled belongs in `VariantId()`. It already hashes type, debug, sysroot, defines, compile flags, the wasm baseline and variant flags, uninterpreted project args and the toolchain (`Toolchain::Key()`). +- Anything that changes what gets compiled or bundled belongs in `VariantId()`. It already hashes type, debug, sysroot, defines, compile flags, the wasm baseline and variant flags, the mingw-w64 prefix for the mingw target, uninterpreted project args and the toolchain (`Toolchain::Key()`). ## Design rules These were decided deliberately. Do not undo them without the maintainer's say-so. 1. **Configuration is C++.** No JSON, YAML, TOML or DSL for project, runner or lint configuration. Lint rules are C++ callbacks; there is no clang-format or clang-tidy integration and no separate "format rule" type. A rule that calls `ctx.SetContent` is a transform, and that is the whole mechanism. -2. **Per-project artifacts go in `/build` and `/bin`. Only cross-project things go in `~/.cache/crafter.build/`:** the std PCM and Crafter.Build PCMs per target and march, and external clones keyed by their spec hash. +2. **Per-project artifacts go in `/build` and `/bin`. Only cross-project things go in `~/.cache/crafter.build/`:** the std PCM and Crafter.Build PCMs per target and march, external clones keyed by their spec hash, and the mingw sysroot views keyed by prefix. 3. **Sources ship, PCMs build locally.** The Flatpak installs `*.cppm` under `/app/share/crafter-build/`. Nothing target-baked ships except the executable and the toolchain beside it, and the executable is built for a portable `-march`. 4. **crafter-build is a monolithic executable with `--export-dynamic`.** `project.so` resolves `Crafter::` symbols from the running binary at dlopen time. There is no Windows host; `x86_64-w64-mingw32` is a cross-compile target for user projects only. 5. **Convention over configuration.** Lead with the zero-config path. Env var, then CLI flag, then full `project.cpp` are the escalation, not the entry point. diff --git a/README.md b/README.md index c57beb6..11248aa 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,7 @@ For elaborate setups (rsync, ssh ControlMaster, env vars on the spawn, `QEMU_LD_ For cross-arch builds, set `cfg.sysroot` on the `Configuration` to point at a sysroot for the target. `BuildStdPcm` and the compile/link commands both consume it: `--sysroot=` is passed to clang, and the std module is precompiled against the sysroot's libc++ headers. Ordinary compiles are pointed at the same headers (`-nostdinc++ -isystem /usr/include/c++/v1`), so a unit that `#include`s a standard header gets the sysroot's libc++ and not the host toolchain's. The module source has to come from the same libc++ release as those headers, so it is taken from `/usr/share/libc++/v1/std.cppm` when the sysroot ships it, else from the toolchain when both are the same major release, else from the sources crafter-build carries for other releases (`share/crafter-build/libc++-modules//`, `crafter-build --version` lists them). Alpine is the common case for the last two: its `libc++-dev` has the headers and no module source. When nothing matches, the build stops with one line naming both releases, installing that release's `share/libc++/v1` directory into the sysroot resolves it. The cheapest aarch64 sysroot is an extracted [Arch Linux ARM rootfs](http://os.archlinuxarm.org/) plus `libc++` + `libc++abi` + `gcc` installed into it via `pacman --root --config --dbpath /var/lib/pacman -Sy …` (point pacman at ALARM's mirrors via explicit config to avoid pulling host-arch packages). -For `--target=x86_64-w64-mingw32` (Windows from Linux), the pipeline cross-compiles as long as `mingw-w64` is installed, with libstdc++ 15 or newer for its std module. clang finds it by the `x86_64-w64-mingw32-gcc` on `PATH`; `CRAFTER_MINGW_DIR` names another prefix (the directory holding `x86_64-w64-mingw32/` and `lib/gcc/`), and inside the Flatpak the host's `/usr` is used when it has mingw-w64. Without one the build stops with a message saying where it looked. Pair it with `--runner=cmd:wine` to run the result. Only the MinGW ABI is supported, there is no MSVC target and no Windows host. `-fuse-ld=lld` is auto-injected on every link, and `-lstdc++exp` is auto-linked for the mingw target so user projects don't need to declare those flags. +For `--target=x86_64-w64-mingw32` (Windows from Linux), the pipeline cross-compiles as long as `mingw-w64` is installed, with libstdc++ 15 or newer for its std module. clang finds it by the `x86_64-w64-mingw32-gcc` on `PATH`; `CRAFTER_MINGW_DIR` names another prefix (the directory holding `x86_64-w64-mingw32/` and `lib/gcc/`), and inside the Flatpak the host's `/usr` is used when it has mingw-w64. Without one the build stops with a message saying where it looked. clang is shown only the prefix's `x86_64-w64-mingw32/` and `lib/gcc/`, as links in `~/.cache/crafter.build/mingw-sysroot/`: it searches the `include/` and `lib/` of whatever it gets as the sysroot, and those of a prefix like `/usr` are the Linux system's own, glibc's headers among them. Pair it with `--runner=cmd:wine` to run the result. Only the MinGW ABI is supported, there is no MSVC target and no Windows host. `-fuse-ld=lld` is auto-injected on every link, and `-lstdc++exp` is auto-linked for the mingw target so user projects don't need to declare those flags. For `--target=wasm32-wasip1`, the WASI sysroot is the one beside the toolchain (the Flatpak bundles one) unless `CRAFTER_BUILD_WASI_SYSROOT` names another, and tests run under `wasmtime`. C++ exceptions work as on the other targets: every unit and the std module are compiled with `-fwasm-exceptions` in the standardized form of WebAssembly exception handling (exnref), against the exception-enabled libc++ in the sysroot's `lib//eh/`, and `-lunwind` is linked automatically. A sysroot without that libc++ (wasi-sdk before 33) stops the build with one line saying so. The result runs on engines with that proposal: Chrome 137, Firefox 131 and Safari 18.4 or newer, and the wasmtime the Flatpak bundles. A page from `EnableWasiBrowserRuntime` checks first and names what an older browser lacks in the console, instead of failing to compile the module. An exception that escapes `main` leaves the program as an uncaught WebAssembly exception (wasmtime reports `thrown Wasm exception` and exits 1, a page gets a `WebAssembly.Exception`) without calling `std::terminate`, so a `std::set_terminate` handler does not run and nothing prints the exception's type or `what()`: wasm exception handling unwinds in a single phase, and nothing finds out that no handler is left until the stack is gone. Catch it in `main` to report it. 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/project.cpp b/project.cpp index 83f70d6..adedaef 100644 --- a/project.cpp +++ b/project.cpp @@ -253,6 +253,7 @@ extern "C" Configuration CrafterBuildProject(std::span a cfg.AddTest("ShaderVariantBuild").Dependencies({ CrafterBuildLib.get() }); cfg.AddTest("ShaderIncludeChange").Dependencies({ CrafterBuildLib.get() }); cfg.AddTest("MingwMissing").Dependencies({ CrafterBuildLib.get() }); + cfg.AddTest("MingwSysroot").Dependencies({ CrafterBuildLib.get() }); cfg.AddTest("GitSubmodules").Dependencies({ CrafterBuildLib.get() }); cfg.AddTest("LinkFlagsChange").Dependencies({ CrafterBuildLib.get() }); cfg.AddTest("ExternalArtifactTime").Dependencies({ CrafterBuildLib.get() }); diff --git a/tests/MingwSysroot/fixture/cprobe.c b/tests/MingwSysroot/fixture/cprobe.c new file mode 100644 index 0000000..3cd5215 --- /dev/null +++ b/tests/MingwSysroot/fixture/cprobe.c @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® + +// probe.cpp's checks for the C sources, which are compiled by a command of +// their own. +#if __has_include() +#error the include/ of the prefix is on the C include path: a Windows build sees the headers of the Linux system +#endif +#if !__has_include(<_mingw.h>) +#error the mingw-w64 headers are not on the C include path +#endif + +void CProbe(void) {} diff --git a/tests/MingwSysroot/fixture/probe.cpp b/tests/MingwSysroot/fixture/probe.cpp new file mode 100644 index 0000000..704d42b --- /dev/null +++ b/tests/MingwSysroot/fixture/probe.cpp @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® + +// Built for x86_64-w64-mingw32 against the prefix tests/MingwSysroot stages: +// a Linux system's /usr with a mingw-w64 cross toolchain in it. +#if __has_include() +#error the include/ of the prefix is on the include path: a Windows build sees the headers of the Linux system +#endif +#if !__has_include(<_mingw.h>) +#error the mingw-w64 headers are not on the include path +#endif +#if !__has_include() +#error the libstdc++ headers are not on the include path: clang found no gcc version under lib/gcc of the prefix +#endif + +void Probe() {} diff --git a/tests/MingwSysroot/main.cpp b/tests/MingwSysroot/main.cpp new file mode 100644 index 0000000..2388acf --- /dev/null +++ b/tests/MingwSysroot/main.cpp @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® + +import std; +import Crafter.Build; +namespace fs = std::filesystem; +using namespace Crafter; + +// A mingw-w64 prefix that is a whole Linux system's /usr must not hand that +// system's headers to a Windows build. Inside the Flatpak the prefix is the +// host's /usr, and clang got it as the --sysroot. clang's mingw driver takes +// a sysroot for a tree that holds mingw-w64 alone and searches its include/ +// after mingw-w64's own headers, so every header mingw-w64 lacks came from +// glibc: libstdc++ found and had shared_ptr read +// __libc_single_threaded, which nothing on Windows defines, and every +// program that used one failed to link (issue #43). CRAFTER_MINGW_DIR=/usr +// 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. +// +// 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. + +extern "C" int setenv(const char* name, const char* value, int overwrite); + +namespace { + std::int32_t failures = 0; + + void Check(bool cond, std::string_view msg) { + if (!cond) { + std::println(std::cerr, "FAIL: {}", msg); + ++failures; + } + } + + void Write(const fs::path& file, std::string_view text) { + fs::create_directories(file.parent_path()); + std::ofstream(file, std::ios::binary | std::ios::trunc) << text; + } +} + +int main() { + const fs::path scratch = fs::temp_directory_path() / "crafter-build-mingw-sysroot"; + std::error_code ec; + fs::remove_all(scratch, ec); + fs::create_directories(scratch); + const fs::path project = scratch / "project"; + fs::copy(fs::current_path() / "tests" / "MingwSysroot" / "fixture", project, fs::copy_options::recursive); + + const fs::path prefix = scratch / "usr"; + 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" / "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}]})"); + // Before anything resolves the toolchain, which reads both once. The + // std module cache goes 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); + 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())); + + fs::remove_all(scratch, ec); + if (failures > 0) { + std::println(std::cerr, "{} assertions failed", failures); + return 1; + } + return 0; +} 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); } { diff --git a/tests/VariantId/main.cpp b/tests/VariantId/main.cpp index e4d2a36..e658b3e 100644 --- a/tests/VariantId/main.cpp +++ b/tests/VariantId/main.cpp @@ -160,6 +160,25 @@ int main() { Check(cfg.VariantId() == cfg.VariantId(GetToolchain()), "VariantId() is the variant under the running toolchain"); Check(cfg.BuildDir().filename() == cfg.VariantId(GetToolchain()), "BuildDir is named for the running toolchain's variant"); } + { + // The mingw-w64 prefix decides what a mingw build compiles against. + // Before issue #43 it was clang's sysroot as is, which put the host's + // headers on the include path, and with the prefix left out of the + // id, the release that fixed that reused the objects built against + // them: the next build only relinked, and failed to link as before. + Configuration mingw = MakeBase(); + mingw.target = "x86_64-w64-mingw32"; + Toolchain discovered; + discovered.clangxx = "/app/bin/clang++"; + Toolchain hostUsr = discovered; + hostUsr.mingwPrefix = "/run/host/usr"; + Toolchain optMingw = discovered; + optMingw.mingwPrefix = "/opt/mingw"; + Check(mingw.VariantId(discovered) != mingw.VariantId(hostUsr), "a mingw prefix perturbs a mingw configuration's VariantId"); + Check(mingw.VariantId(hostUsr) != mingw.VariantId(optMingw), "two mingw prefixes are two variants"); + Configuration host = MakeBase(); + Check(host.VariantId(discovered) == host.VariantId(hostUsr), "the mingw prefix leaves other targets' variants alone"); + } if (Failures > 0) { std::println(std::cerr, "{} assertions failed", Failures);