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/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);