Crafter.Build/tests/MingwSysroot/main.cpp
Jorijn van der Graaf 06b61e84f9
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>
2026-10-31 22:58:30 +01:00

151 lines
7.8 KiB
C++

// 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 <sys/single_threaded.h> 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 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.
//
// 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);
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;
}
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() {
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" / "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
// 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);
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));
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) {
std::println(std::cerr, "{} assertions failed", failures);
return 1;
}
return 0;
}