Compare commits

..
Author SHA1 Message Date
5fdbf2baee
docs: the mingw sysroot view in the README and AGENTS.template.md
All checks were successful
Flatpak / flatpak (pull_request) Successful in 7m2s
The README says which directories of a mingw-w64 prefix clang sees, and
why the rest of a prefix like /usr is kept out of a Windows build.
AGENTS.template.md names MingwSysroot in the :Toolchain partition, with
the reason the prefix itself is never a sysroot, counts the views among
what goes in the cache, and adds the mingw prefix to what VariantId
hashes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:48:06 +01:00
b162cc0552
test: a mingw build sees no header from its prefix's include/
MingwSysroot stages a prefix shaped like a Linux system's /usr with a
mingw-w64 cross toolchain in it, without needing a real one: glibc's
<sys/single_threaded.h> in include/, gcc's per-target directory, and a
stand-in mingw-w64 tree whose libstdc++ has an empty std module. A
static library for x86_64-w64-mingw32, so that nothing is linked,
compiles a C++ and a C source that stop with #error when they see the
prefix's include/ or miss mingw-w64's or libstdc++'s headers. Against
the library before the fix, the C++ source stopped on
<sys/single_threaded.h>. The test also checks that the std module is
not cached in the directory older releases built it in against the
prefix itself. It needs only clang, so it runs in CI too, where no
mingw-w64 is installed.

The stand-in std module's source is written through std::format: the
build's import scanner reads string literals as code, and the literal
"export module std;" made the test itself a unit of a module std.

VariantId checks that a prefix gives a mingw configuration a variant of
its own, that two prefixes give two, and that other targets ignore it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:48:06 +01:00
ab998081e1
fix: give the mingw target a sysroot that holds mingw-w64 alone
Inside the Flatpak the mingw-w64 prefix is the host's /usr, and it was
clang's --sysroot as is. clang's mingw driver takes a sysroot for a tree
that holds mingw-w64 alone, and adds its include/ to the include path
and its lib/ to the library path. For /usr those are the Linux system's
own: every header mingw-w64 lacks came from glibc, and libstdc++'s
<ext/atomicity.h>, finding <sys/single_threaded.h>, had shared_ptr read
__libc_single_threaded, which nothing on Windows defines. Every program
that used one failed to link, among them every Crafter.Graphics Win32
executable, through Crafter.Asset's GLB loader. CRAFTER_MINGW_DIR=/usr
did the same outside the Flatpak (#43).

The sysroot is now MingwSysroot(): a directory in the cache, one per
prefix, holding only links to the prefix's x86_64-w64-mingw32/, lib/gcc/
and lib64/gcc/ (where openSUSE keeps gcc). Through it clang finds
mingw-w64, gcc's version and libstdc++ exactly as it does through the
mingw gcc on PATH, and nothing else. It is made by the first mingw
compile, link or CMake configure of a process, so no other command
writes to the cache for it and a failure to make it is that build's
error. The Toolchain field is now mingwPrefix, since clang never gets
the prefix itself, and it is made absolute when the toolchain is
resolved, as it ends up in link targets and directory names. Messages
keep naming paths under the prefix rather than under the view.

The prefix is also part of the mingw target's VariantId and of its std
PCM directory. Units do not track the std PCM they import: when the
fixed release built in a directory a broken build had left, the std
module was rebuilt and the objects compiled against glibc's headers
were only relinked, failing as before. As with a compiler update, a new
directory is what gets them rebuilt. It also keeps two releases that
share a cache, as the Flatpak's branches do, from rebuilding one std PCM
under each other's PCMs, and two prefixes apart. Without a prefix both
stay as they were: clang's own discovery has not changed.

Two shorter ways do not work: --sysroot=<prefix>/x86_64-w64-mingw32
loses the directory gcc's version names, so <string> is not found, and
clang ignores --gcc-toolchain and --gcc-install-dir for this target.

Fixes #43

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:48:06 +01:00
14 changed files with 256 additions and 26 deletions

View file

@ -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: 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. - `: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. - `: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. - `: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. - 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`. - 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. - 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 ## Design rules
These were decided deliberately. Do not undo them without the maintainer's say-so. 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. 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 `<project>/build` and `<project>/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 `<project>/build` and `<project>/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`. 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. 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. 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.

View file

@ -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=<path>` 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 <sysroot>/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 `<sysroot>/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/<major>/`, `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 <rootfs> --config <alarm-pacman.conf> --dbpath <rootfs>/var/lib/pacman -Sy …` (point pacman at ALARM's mirrors via explicit config to avoid pulling host-arch packages). 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=<path>` 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 <sysroot>/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 `<sysroot>/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/<major>/`, `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 <rootfs> --config <alarm-pacman.conf> --dbpath <rootfs>/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/<triple>/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. 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/<triple>/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.

View file

@ -265,6 +265,13 @@ fs::path Crafter::StdPcmDir(const Configuration& config) {
if (config.target.starts_with("wasm32")) { if (config.target.starts_with("wasm32")) {
stdPcmKey += std::format("-{:08x}", static_cast<std::uint32_t>(std::hash<std::string_view>{}(WasmBaselineFlags))); stdPcmKey += std::format("-{:08x}", static_cast<std::uint32_t>(std::hash<std::string_view>{}(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::uint32_t>(std::hash<std::string>{}(GetToolchain().mingwPrefix.string())));
}
for (const std::string& f : config.wasmVariantFlags) { for (const std::string& f : config.wasmVariantFlags) {
stdPcmKey += "+"; stdPcmKey += "+";
for (char c : f) stdPcmKey += (c == '/' || c == '\\') ? '_' : c; for (char c : f) stdPcmKey += (c == '/' || c == '\\') ? '_' : c;
@ -928,9 +935,9 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
: std::format(" -march={} -mtune={}", config.march, config.mtune); : std::format(" -march={} -mtune={}", config.march, config.mtune);
if (!config.sysroot.empty()) { if (!config.sysroot.empty()) {
cArchFlags += std::format(" --sysroot={}", config.sysroot); cArchFlags += std::format(" --sysroot={}", config.sysroot);
} else if (config.target == "x86_64-w64-mingw32" && !GetToolchain().mingwSysroot.empty()) { } else if (config.target == "x86_64-w64-mingw32" && !MingwSysroot().empty()) {
// The C++ side gets it through GetBaseCommand. // The C++ side gets it through GetBaseCommand.
cArchFlags += std::format(" --sysroot={}", GetToolchain().mingwSysroot.string()); cArchFlags += std::format(" --sysroot={}", MingwSysroot().string());
} }
if (isWasm) { if (isWasm) {
// The C++ path's wasm flags, all of them, a variant pass's included. // The C++ path's wasm flags, all of them, a variant pass's included.
@ -1868,9 +1875,9 @@ Environment:
CRAFTER_BUILD_LIBCLANG Exact libclang to load for lint. CRAFTER_BUILD_LIBCLANG Exact libclang to load for lint.
CRAFTER_BUILD_WASI_SYSROOT WASI sysroot for wasm32 targets. CRAFTER_BUILD_WASI_SYSROOT WASI sysroot for wasm32 targets.
CRAFTER_MINGW_DIR mingw-w64 prefix (holding x86_64-w64-mingw32/ CRAFTER_MINGW_DIR mingw-w64 prefix (holding x86_64-w64-mingw32/
and lib/gcc/), passed as --sysroot. Default: and lib/gcc/). Default: the mingw gcc on
the mingw gcc on PATH; in the Flatpak, the PATH; in the Flatpak, the host's /usr when
host's /usr when it has mingw-w64. it has mingw-w64.
CRAFTER_BUILD_HOME Directory holding the module sources. CRAFTER_BUILD_HOME Directory holding the module sources.
Exit status: Exit status:

View file

@ -149,7 +149,7 @@ std::string BuildInjectedCMakeFlags(const fs::path& cmakeBuildDir, std::string_v
// The libstdc++ and the mingw-w64 install the project's own compiles // The libstdc++ and the mingw-w64 install the project's own compiles
// use (GetBaseCommand). // use (GetBaseCommand).
out += " -DCMAKE_CXX_FLAGS=-stdlib=libstdc++"; out += " -DCMAKE_CXX_FLAGS=-stdlib=libstdc++";
if (!tc.mingwSysroot.empty()) out += std::format(" -DCMAKE_SYSROOT={}", ShellQuote(tc.mingwSysroot.string())); if (const fs::path& sysroot = MingwSysroot(); !sysroot.empty()) out += std::format(" -DCMAKE_SYSROOT={}", ShellQuote(sysroot.string()));
} }
fs::path absBuildDir = fs::absolute(cmakeBuildDir); fs::path absBuildDir = fs::absolute(cmakeBuildDir);
out += std::format(" -DCMAKE_ARCHIVE_OUTPUT_DIRECTORY={}", ShellQuote(absBuildDir.string())); out += std::format(" -DCMAKE_ARCHIVE_OUTPUT_DIRECTORY={}", ShellQuote(absBuildDir.string()));

View file

@ -742,9 +742,9 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
// its manifest. A toolchain that isn't there is a build error, not // its manifest. A toolchain that isn't there is a build error, not
// an exception: Build() runs this with its worker threads started. // an exception: Build() runs this with its worker threads started.
const Toolchain& tc = GetToolchain(); const Toolchain& tc = GetToolchain();
const std::string where = tc.mingwSysroot.empty() const std::string where = tc.mingwPrefix.empty()
? std::string("where clang finds it by the x86_64-w64-mingw32 gcc on PATH") ? std::string("where clang finds it by the x86_64-w64-mingw32 gcc on PATH")
: std::format("under {}", tc.mingwSysroot.string()); : std::format("under {}", tc.mingwPrefix.string());
const std::string missing = std::format("no mingw-w64 libstdc++ with the std module (libstdc++ 15 or newer) {}. Install mingw-w64, or point CRAFTER_MINGW_DIR at its prefix, the directory holding x86_64-w64-mingw32/ and lib/gcc/", where); const std::string missing = std::format("no mingw-w64 libstdc++ with the std module (libstdc++ 15 or newer) {}. Install mingw-w64, or point CRAFTER_MINGW_DIR at its prefix, the directory holding x86_64-w64-mingw32/ and lib/gcc/", where);
// Asked once a run for each compiler and target: every mingw // Asked once a run for each compiler and target: every mingw
// configuration comes through here, up to date or not, and the answer // configuration comes through here, up to date or not, and the answer
@ -775,6 +775,13 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
} }
} }
if (manifestPath.empty()) return missing; 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;
}
const std::optional<std::string> source = StdModuleSourcePath(FileBytes(manifestPath)); const std::optional<std::string> source = StdModuleSourcePath(FileBytes(manifestPath));
if (!source) return std::format("{} names no std module", manifestPath.string()); if (!source) return std::format("{} names no std module", manifestPath.string());
const fs::path stdCc = (manifestPath.parent_path() / *source).lexically_normal(); 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") { if (config.target == "x86_64-w64-mingw32") {
// libstdc++ from the mingw toolchain, said outright: a clang built to // libstdc++ from the mingw toolchain, said outright: a clang built to
// default to libc++ (the Flatpak's) would otherwise reach for that. // 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++"; 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; return command;
} }
command += " -stdlib=libc++"; command += " -stdlib=libc++";

View file

@ -43,6 +43,20 @@ namespace {
while (!s.empty() && (s.back() == '\n' || s.back() == '\r' || s.back() == ' ')) s.pop_back(); while (!s.empty() && (s.back() == '\n' || s.back() == '\r' || s.back() == ' ')) s.pop_back();
return s; 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 { std::string Toolchain::LibcxxCompileFlags() const {
@ -136,11 +150,14 @@ Toolchain Crafter::ResolveToolchain(const ToolchainInputs& in) {
tc.wasiSysroot = "/usr/share/wasi-sysroot"; tc.wasiSysroot = "/usr/share/wasi-sysroot";
} }
if (std::string_view dir = Env(in, "CRAFTER_MINGW_DIR"); !dir.empty()) { 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")) { } 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 // The sandbox has no mingw-w64 of its own, and no mingw gcc on PATH
// for clang to find one by. // for clang to find one by.
tc.mingwSysroot = in.hostRoot / "usr"; tc.mingwPrefix = in.hostRoot / "usr";
} }
return tc; return tc;
} }
@ -186,6 +203,31 @@ const Toolchain& Crafter::RequireToolchain() {
return tc; 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<std::string>{}(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) { std::int32_t Crafter::LibcxxVersion(const fs::path& includeDir) {
// "# define _LIBCPP_VERSION 230102", the one place libc++ states its // "# define _LIBCPP_VERSION 230102", the one place libc++ states its
// release in a form the preprocessor does not have to be run for. // release in a form the preprocessor does not have to be run for.

View file

@ -628,6 +628,17 @@ export namespace Crafter {
compileKey += "|B:"; compileKey += "|B:";
compileKey += WasmBaselineFlags; 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 // Sorted by ApplyStandardArgs, so flag order on the command line
// doesn't split the cache. // doesn't split the cache.
for (const std::string& a : projectArgs) { for (const std::string& a : projectArgs) {

View file

@ -39,11 +39,14 @@ export namespace Crafter {
fs::path wasiSysroot; fs::path wasiSysroot;
// The mingw-w64 install prefix, the directory that holds // The mingw-w64 install prefix, the directory that holds
// x86_64-w64-mingw32/ and lib/gcc/x86_64-w64-mingw32/ (/usr for a // 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. // distro package), as an absolute path. Empty means clang's own
// Empty means clang's own discovery, which follows the mingw gcc on // discovery, which follows the mingw gcc on PATH. Set by
// PATH. Set by CRAFTER_MINGW_DIR, else the host's /usr when running // CRAFTER_MINGW_DIR, else the host's /usr when running sandboxed and
// sandboxed and it has mingw-w64 (the sandbox's own /usr never does). // it has mingw-w64 (the sandbox's own /usr never does). clang never
fs::path mingwSysroot; // 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++ -print-target-triple` and the first line of
// `clang++ --version`; both empty when clang++ could not be run. // `clang++ --version`; both empty when clang++ could not be run.
std::string hostTriple; std::string hostTriple;
@ -86,6 +89,18 @@ export namespace Crafter {
const Toolchain& GetToolchain(); const Toolchain& GetToolchain();
// GetToolchain(), or a std::runtime_error carrying its error. // GetToolchain(), or a std::runtime_error carrying its error.
const Toolchain& RequireToolchain(); 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 <sys/single_threaded.h>, 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) // _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. // defines it, XXYYZZ; 0 when the file is missing or has no such line.
std::int32_t LibcxxVersion(const fs::path& includeDir); std::int32_t LibcxxVersion(const fs::path& includeDir);

View file

@ -253,6 +253,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
cfg.AddTest("ShaderVariantBuild").Dependencies({ CrafterBuildLib.get() }); cfg.AddTest("ShaderVariantBuild").Dependencies({ CrafterBuildLib.get() });
cfg.AddTest("ShaderIncludeChange").Dependencies({ CrafterBuildLib.get() }); cfg.AddTest("ShaderIncludeChange").Dependencies({ CrafterBuildLib.get() });
cfg.AddTest("MingwMissing").Dependencies({ CrafterBuildLib.get() }); cfg.AddTest("MingwMissing").Dependencies({ CrafterBuildLib.get() });
cfg.AddTest("MingwSysroot").Dependencies({ CrafterBuildLib.get() });
cfg.AddTest("GitSubmodules").Dependencies({ CrafterBuildLib.get() }); cfg.AddTest("GitSubmodules").Dependencies({ CrafterBuildLib.get() });
cfg.AddTest("LinkFlagsChange").Dependencies({ CrafterBuildLib.get() }); cfg.AddTest("LinkFlagsChange").Dependencies({ CrafterBuildLib.get() });
cfg.AddTest("ExternalArtifactTime").Dependencies({ CrafterBuildLib.get() }); cfg.AddTest("ExternalArtifactTime").Dependencies({ CrafterBuildLib.get() });

View file

@ -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(<sys/single_threaded.h>)
#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) {}

View file

@ -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(<sys/single_threaded.h>)
#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(<memory>)
#error the libstdc++ headers are not on the include path: clang found no gcc version under lib/gcc of the prefix
#endif
void Probe() {}

View file

@ -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 <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 <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.
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<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);
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;
}

View file

@ -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.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.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.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"); 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"; in.env["CRAFTER_MINGW_DIR"] = "/opt/mingw";
Toolchain tc = ResolveToolchain(in); Toolchain tc = ResolveToolchain(in);
Check(tc.wasiSysroot == "/opt/wasi", "CRAFTER_BUILD_WASI_SYSROOT overrides the wasi sysroot"); 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, // Sandboxed, the host's /usr stands in for the missing mingw-w64,
@ -153,11 +155,11 @@ int main() {
fs::remove_all(host); fs::remove_all(host);
ToolchainInputs in; ToolchainInputs in;
in.hostRoot = host; 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"); 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"; 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); fs::remove_all(host);
} }
{ {

View file

@ -160,6 +160,25 @@ int main() {
Check(cfg.VariantId() == cfg.VariantId(GetToolchain()), "VariantId() is the variant under the running toolchain"); 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"); 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) { if (Failures > 0) {
std::println(std::cerr, "{} assertions failed", Failures); std::println(std::cerr, "{} assertions failed", Failures);