Compare commits

..
14 changed files with 26 additions and 256 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. `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). - `: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.
- `: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, the mingw-w64 prefix for the mingw target, 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, 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, external clones keyed by their spec hash, and the mingw sysroot views keyed by prefix. 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.
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. 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=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=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,13 +265,6 @@ 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;
@ -935,9 +928,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" && !MingwSysroot().empty()) { } else if (config.target == "x86_64-w64-mingw32" && !GetToolchain().mingwSysroot.empty()) {
// The C++ side gets it through GetBaseCommand. // The C++ side gets it through GetBaseCommand.
cArchFlags += std::format(" --sysroot={}", MingwSysroot().string()); cArchFlags += std::format(" --sysroot={}", GetToolchain().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.
@ -1875,9 +1868,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/). Default: the mingw gcc on and lib/gcc/), passed as --sysroot. Default:
PATH; in the Flatpak, the host's /usr when the mingw gcc on PATH; in the Flatpak, the
it has mingw-w64. host's /usr when 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 (const fs::path& sysroot = MingwSysroot(); !sysroot.empty()) out += std::format(" -DCMAKE_SYSROOT={}", ShellQuote(sysroot.string())); if (!tc.mingwSysroot.empty()) out += std::format(" -DCMAKE_SYSROOT={}", ShellQuote(tc.mingwSysroot.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.mingwPrefix.empty() const std::string where = tc.mingwSysroot.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.mingwPrefix.string()); : std::format("under {}", tc.mingwSysroot.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,13 +775,6 @@ 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();
@ -869,9 +862,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 prefix. // Found by clang's own mingw discovery unless there is a sysroot.
command += " -stdlib=libstdc++"; command += " -stdlib=libstdc++";
if (const fs::path& sysroot = MingwSysroot(); !sysroot.empty()) command += std::format(" --sysroot={}", sysroot.string()); if (!tc.mingwSysroot.empty()) command += std::format(" --sysroot={}", tc.mingwSysroot.string());
return command; return command;
} }
command += " -stdlib=libc++"; command += " -stdlib=libc++";

View file

@ -43,20 +43,6 @@ 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 {
@ -150,14 +136,11 @@ 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()) {
// Made absolute here, once: the prefix becomes the target of links tc.mingwSysroot = fs::path(dir);
// 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.mingwPrefix = in.hostRoot / "usr"; tc.mingwSysroot = in.hostRoot / "usr";
} }
return tc; return tc;
} }
@ -203,31 +186,6 @@ 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,17 +628,6 @@ 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,14 +39,11 @@ 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), as an absolute path. Empty means clang's own // distro package), passed to clang as --sysroot for the mingw target.
// discovery, which follows the mingw gcc on PATH. Set by // Empty means clang's own discovery, which follows the mingw gcc on
// CRAFTER_MINGW_DIR, else the host's /usr when running sandboxed and // PATH. Set by CRAFTER_MINGW_DIR, else the host's /usr when running
// it has mingw-w64 (the sandbox's own /usr never does). clang never // sandboxed and it has mingw-w64 (the sandbox's own /usr never does).
// gets the prefix itself: the mingw target's --sysroot is fs::path mingwSysroot;
// 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;
@ -89,18 +86,6 @@ 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,7 +253,6 @@ 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

@ -1,13 +0,0 @@
// 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

@ -1,16 +0,0 @@
// 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

@ -1,97 +0,0 @@
// 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.mingwPrefix.empty(), "outside a sandbox, clang finds mingw-w64 itself: no prefix"); Check(tc.mingwSysroot.empty(), "outside a sandbox, clang finds mingw-w64 itself: no --sysroot");
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,9 +144,7 @@ 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.mingwPrefix == "/opt/mingw", "CRAFTER_MINGW_DIR names the mingw prefix"); Check(tc.mingwSysroot == "/opt/mingw", "CRAFTER_MINGW_DIR names the mingw prefix passed as --sysroot");
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,
@ -155,11 +153,11 @@ int main() {
fs::remove_all(host); fs::remove_all(host);
ToolchainInputs in; ToolchainInputs in;
in.hostRoot = host; in.hostRoot = host;
Check(ResolveToolchain(in).mingwPrefix.empty(), "a host without mingw-w64 gives no mingw prefix"); Check(ResolveToolchain(in).mingwSysroot.empty(), "a host without mingw-w64 gives no --sysroot");
fs::create_directories(host / "usr" / "x86_64-w64-mingw32"); 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).mingwSysroot == 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).mingwPrefix == "/opt/mingw", "CRAFTER_MINGW_DIR wins over the host's"); Check(ResolveToolchain(in).mingwSysroot == "/opt/mingw", "CRAFTER_MINGW_DIR wins over the host's");
fs::remove_all(host); fs::remove_all(host);
} }
{ {

View file

@ -160,25 +160,6 @@ 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);