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>
This commit is contained in:
parent
5fdbf2baee
commit
06b61e84f9
8 changed files with 262 additions and 78 deletions
6
tests/MingwSysroot/fixture/failing.cpp
Normal file
6
tests/MingwSysroot/fixture/failing.cpp
Normal file
|
|
@ -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 <crafter_probe_error.h>
|
||||
|
|
@ -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 <sys/single_threaded.h> 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 <sys/single_threaded.h> 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<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { fs::path(name) };
|
||||
lib.GetInterfacesAndImplementations(ifaces, impls);
|
||||
if (!cFile.empty()) lib.cFiles = { cFile };
|
||||
std::unordered_map<fs::path, std::shared_future<BuildResult>> 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<fs::path, 2> 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<fs::path, 1> 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<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "probe" };
|
||||
lib.GetInterfacesAndImplementations(ifaces, impls);
|
||||
lib.cFiles = { project / "cprobe" };
|
||||
std::unordered_map<fs::path, std::shared_future<BuildResult>> 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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue