diff --git a/AGENTS.md b/AGENTS.md index 1c445f4..d6be6d2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -117,7 +117,7 @@ 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. - 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. -- 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, wasm variant flags, uninterpreted project args and the toolchain (`Toolchain::Key()`). ## Design rules @@ -191,7 +191,5 @@ Local builds are two scripts in `project.cpp`. Run them with the native binary: - The bootstrap binary is compiled without `CRAFTER_BUILD_HAS_ASSET`, so `cfg.assets` only works in the self-hosted binary. Changing asset extensions means updating two maps in `Crafter.Build-Clang.cpp`: `assetOutput` and the transitive copy loop. - A warning is printed only when its unit actually recompiles. A build with nothing to do is silent, which is not evidence that a warning is gone. - A std module source only compiles against the headers of its own libc++ release, and a sysroot does not always ship one (Alpine). The Flatpak therefore carries the sources of the release next to the bundled one under `share/crafter-build/libc++-modules//`, one `libcxx--modules` manifest module each. When the bundled LLVM is bumped, keep the release it leaves behind as such a module: sysroots on the stable distro stay on it for months. The native loop has no such directory; stage one by hand under `share/crafter-build/` to test against a foreign sysroot. -- The wasm compile flags live in one place, `WasmBaselineFlags` in `Crafter.Build-Clang.cppm`: the std module and every unit have to agree on them, and `VariantId()` and the std PCM directory hash them, so a change rebuilds instead of mixing old and new BMIs. They enable C++ exceptions, so the link takes the exception-enabled libc++ and libunwind from the sysroot's `lib//eh/` (wasi-sdk 33 and later). The Flatpak's sysroot has them and a distro's may not; where they are missing the wasm tests skip, so run those through the Flatpak. A browser page checks the engine with the `WasmBaselineProbes` named in `variants.json`. -- `WasiBrowserRuntimeExit` runs `wasi-runtime/runtime.js` under node and skips where there is none. The Flatpak has no node, so CI never runs it: after touching `runtime.js`, run the suite in the native loop with node on `PATH`. - `VariantId()` names the output directory, so CI globs `bin/crafter.build-exe---*/` rather than predicting the hash. The hash includes the compiler, so after a toolchain update the previous variant's directories stay in `bin/` and `build/` until `clean`, and such a glob matches both. - The self-host build and `test` keep the dependency notes section of this very file. None of Crafter.Math, Crafter.Asset and Crafter.LintRules ships an `AGENTS.consumer.md` yet. Once one does, a build here can change AGENTS.md whenever that repository changes its notes, since all three are unpinned. The tree is then dirty, so the next build's `git describe --dirty` version differs, which is a new `crafter.build-lib` variant and a full rebuild. Commit the section to settle it. diff --git a/README.md b/README.md index 77514ec..53844fe 100644 --- a/README.md +++ b/README.md @@ -187,16 +187,6 @@ For cross-arch builds, set `cfg.sysroot` on the `Configuration` to point at a sy 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//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. - -A wasm build refuses three things before it starts, each with a message that says why: `-flto` in `compileFlags` or `linkFlags` (LTO code generation drops the exception handling, [WebAssembly/wasi-sdk#629](https://github.com/WebAssembly/wasi-sdk/issues/629)), `-fno-exceptions` (the std module is built with exceptions, and a unit without them cannot import it) and a shared library (the exception-enabled libc++ is static archives only). `setjmp` and `longjmp` work in C and C++ sources, with `-lsetjmp` linked automatically like `-lunwind`; a `longjmp` across a `noexcept` function ends the program, because it is a wasm exception and the function's terminate handler catches it. Outside the Flatpak, a wasmtime older than 47 has exception handling off and needs `-W exceptions=y`, and a `wasm-opt` on `PATH` from a binaryen that cannot read the standardized encoding breaks release links, since clang runs it on optimized wasm links when it finds one. - -In a page from `EnableWasiBrowserRuntime`, `exit` and `_Exit` end the program as they would anywhere else: none of its code runs on the way out, no destructor and no `catch (...)`, its status is in `window.crafter_wasi.exitCode`, and bridges that call into it on their own are expected to stop. A program that hands the thread to the browser for good and goes on serving calls, as Crafter.Graphics's `Window::StartSync()` does, parks instead of exiting. Parking never returns, runs nothing of the program either, and leaves `main`'s locals valid for the calls that follow: - -```cpp -extern "C" [[noreturn]] __attribute__((import_module("crafter_runtime"), import_name("park"))) void Park(); -``` - ### Configuring runners Two layers, CLI wins over env, both override the per-test default of `Local()`: diff --git a/examples/wasi/README.md b/examples/wasi/README.md index 8a8db56..7ac0f42 100644 --- a/examples/wasi/README.md +++ b/examples/wasi/README.md @@ -4,14 +4,9 @@ Build a C++26 program for the `wasm32-wasi` target and run it in a browser. ## Prerequisites -- A WASI sysroot with the exception-enabled libc++, `lib/wasm32-wasip1/eh/` - from wasi-sdk 33 or later: wasm builds compile C++ exceptions and link those - libraries. The Flatpak bundles one, which `crafter-build` uses as it is. - Elsewhere, point `CRAFTER_BUILD_WASI_SYSROOT` at a wasi-sdk sysroot, or set - `cfg.sysroot` in `project.cpp`. Arch's `wasi-libc++` packages have no such - libraries, and the build stops with a message saying so. -- For the browser: WebAssembly exception handling, in Chrome 137, Firefox 131, - Safari 18.4 or newer. +- `wasi-libc`, `wasi-libc++`, `wasi-libc++abi` (Arch packages — provide + `/usr/share/wasi-sysroot/`, which `crafter-build` finds automatically). + Other distros: install the WASI SDK and set `cfg.sysroot` in `project.cpp`. ## Build diff --git a/implementations/Crafter.Build-Clang.cpp b/implementations/Crafter.Build-Clang.cpp index e0d8329..35b64d7 100644 --- a/implementations/Crafter.Build-Clang.cpp +++ b/implementations/Crafter.Build-Clang.cpp @@ -256,15 +256,6 @@ fs::path Crafter::StdPcmDir(const Configuration& config) { // the consuming TUs see a target-feature mismatch. Suffix the cache dir // with the variant flags when present. std::string stdPcmKey = std::format("{}-{}-{}", config.target, config.march, GetToolchain().Key()); - // The flags every wasm unit gets are compiled into the std PCM as well, - // and the importing units have to match them. Two releases that share a - // cache and a compiler, as the Flatpak's branches do, would otherwise - // take turns rebuilding this one std PCM, each time underneath the PCMs - // built against the previous one, and clang crashes on those rather than - // rejecting them. - if (config.target.starts_with("wasm32")) { - stdPcmKey += std::format("-{:08x}", static_cast(std::hash{}(WasmBaselineFlags))); - } for (const std::string& f : config.wasmVariantFlags) { stdPcmKey += "+"; for (char c : f) stdPcmKey += (c == '/' || c == '\\') ? '_' : c; @@ -272,12 +263,6 @@ fs::path Crafter::StdPcmDir(const Configuration& config) { return GetCacheDir()/stdPcmKey; } -std::string Crafter::WasmCompileFlags(const Configuration& config) { - std::string flags = std::format(" {}", WasmBaselineFlags); - for (const std::string& f : config.wasmVariantFlags) flags += std::format(" {}", f); - return flags; -} - fs::path Crafter::ExpectedOutput(const Configuration& config) { const fs::path dir = config.BinDir(); const bool mingwTarget = config.target == "x86_64-w64-mingw32"; @@ -332,11 +317,15 @@ CompileCommand Crafter::GetCompileCommand(const Configuration& config) { if (isWasm) { // -mllvm is consumed by codegen but not the link driver, which is the // same command line; quiet the unused-flag warning rather than split - // compile and link commands. An active variant pass (see the variant - // driver at the end of Build) adds its codegen flags (e.g. - // -mrelaxed-simd) to every TU; they are part of VariantId, so those - // objects/PCMs land in their own dir. - out.command += std::format("{} -Wno-unused-command-line-argument", WasmCompileFlags(config)); + // compile and link commands. + out.command += " -fno-exceptions -msimd128 -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL -Wno-unused-command-line-argument"; + // Active variant pass (see the variant driver at the end of Build): + // extra codegen flags (e.g. -mrelaxed-simd) applied to every TU. Empty + // for the baseline build. Part of VariantId, so these objects/PCMs + // land in their own dir. + for (const std::string& f : config.wasmVariantFlags) { + out.command += std::format(" {}", f); + } } if (config.target == "x86_64-w64-mingw32") { // mingw libstdc++ defines TLS via __emutls_v.* (emulated TLS); without @@ -416,14 +405,12 @@ CompileCommand Crafter::GetCompileCommand(const Configuration& config) { // (cross-TU inlining) plus dead-section GC and safe identical-code folding. // Default-on in Release, never Debug (keeps builds fast and debuggable). // Excluded for wasm32 — its -mllvm/sjlj codegen flags don't compose with - // LTO here, and wasi-sdk lists exceptions under LTO as broken - // (WebAssembly/wasi-sdk#629) — and for nvcc .cu objects below (they can't - // emit LLVM bitcode, so they link in as ordinary objects alongside the - // bitcode ones). ThinLTO rather than monolithic -flto so link time and - // memory scale with arbitrary user project sizes. Compile-side flags - // (-flto, -ffunction/-fdata-sections) ride on `command`, which is reused - // as the link base; the link-only -Wl flags go on linkExtras so they - // don't warn on each -c compile. + // LTO here — and for nvcc .cu objects below (they can't emit LLVM bitcode, + // so they link in as ordinary objects alongside the bitcode ones). ThinLTO + // rather than monolithic -flto so link time and memory scale with arbitrary + // user project sizes. Compile-side flags (-flto, -ffunction/-fdata-sections) + // ride on `command`, which is reused as the link base; the link-only -Wl + // flags go on linkExtras so they don't warn on each -c compile. out.useLto = !config.debug && !isWasm; out.ltoCompileFlags = out.useLto ? " -flto=thin -ffunction-sections -fdata-sections" : ""; out.ltoLinkFlags = out.useLto ? " -flto=thin -Wl,--gc-sections -Wl,--icf=safe" : ""; @@ -493,54 +480,6 @@ namespace { walk(config, config.name); return error; } - - // What a wasm32 configuration cannot be built with now that every unit - // has C++ exceptions, and what its sysroot has to have for that; empty - // for any other target and when nothing is wrong. Each would otherwise - // fail at the first compile or link with a message that does not say - // why, or not fail at all. - std::string WasmBuildError(const Configuration& config) { - if (!config.target.starts_with("wasm32")) return ""; - for (const std::vector* flags : { &config.compileFlags, &config.linkFlags }) { - for (const std::string& flag : *flags) { - // The compile has the backend options that keep exception - // handling; LTO code generation at the link does not, so it - // drops every landing pad and the build succeeds with throws - // that escape their catch blocks (WebAssembly/wasi-sdk#629). - if (flag.starts_with("-flto")) { - return std::format("{}: {} is not supported on wasm32: LTO code generation drops C++ exception handling (WebAssembly/wasi-sdk#629), so every throw would escape its catch", config.name, flag); - } - // The std module is built with exceptions, and a unit built - // without them cannot import it. - if (flag == "-fno-exceptions" || flag == "-fno-cxx-exceptions") { - return std::format("{}: {} is not supported on wasm32: wasm builds have C++ exceptions, the std module included, and a unit compiled without them cannot import std", config.name, flag); - } - } - } - // wasi-sdk's exception support does not cover shared libraries: the - // libc++ under lib//eh is static archives built without -fPIC. - if (config.type == ConfigurationType::LibraryDynamic) { - return std::format("{}: wasm32 shared libraries are not supported: the exception-enabled libc++ they would link has static archives only", config.name); - } - const fs::path sysroot = config.sysroot; - if (!fs::is_directory(sysroot)) { - return std::format("no WASI sysroot at {}: the Flatpak bundles one, and CRAFTER_BUILD_WASI_SYSROOT selects another", sysroot.string()); - } - const fs::path libDir = sysroot / "lib" / NormalizeTarget(config.target); - if (!fs::is_directory(libDir)) { - return std::format("the WASI sysroot at {} has nothing for {} (no {})", sysroot.string(), config.target, libDir.string()); - } - // Only a link takes the exception-enabled libraries: a static - // library's objects compile against the headers alone. The driver - // looks in eh/ first and then in the triple's own directory, where a - // sysroot built with exceptions only (WASI_SDK_EXCEPTIONS=ON) has them. - // Without them the first link fails with "unable to find library - // -lunwind", which says nothing about why it was asked for. - if (config.type == ConfigurationType::Executable && !fs::exists(libDir / "eh" / "libunwind.a") && !fs::exists(libDir / "libunwind.a")) { - return std::format("the WASI sysroot at {} has no C++ exception support (no {}): wasm builds use WebAssembly exception handling and need the exception-enabled libc++ and libunwind that wasi-sdk 33 and later ship. The Flatpak bundles such a sysroot; CRAFTER_BUILD_WASI_SYSROOT selects another", sysroot.string(), (libDir / "eh" / "libunwind.a").string()); - } - return ""; - } } BuildResult Crafter::Build(Configuration& config, std::unordered_map>& depResults, std::mutex& depMutex) { @@ -601,11 +540,6 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map.wasm (produced by Build's variant - // driver); the baseline maps to outputName.wasm. Even with no declared - // variants we still emit a one-entry manifest so the runtime has a + // as the universal fallback (empty probes always pass). A variant with a + // non-empty label maps to outputName.