diff --git a/implementations/Crafter.Build-Clang.cpp b/implementations/Crafter.Build-Clang.cpp index ba9f3d7..916abf8 100644 --- a/implementations/Crafter.Build-Clang.cpp +++ b/implementations/Crafter.Build-Clang.cpp @@ -493,6 +493,54 @@ 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) { @@ -553,6 +601,11 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map/eh and @@ -34,9 +34,10 @@ namespace Crafter { // The wasi-runtime/runtime.js probes an engine has to pass to run anything // built with WasmBaselineFlags; EnableWasiBrowserRuntime puts them on every // entry of variants.json. Exception handling with exnref is the newest - // feature those flags need (Chrome 137, Firefox 131, Safari 18.4), so an - // engine that has it has SIMD128 and the rest as well. - inline constexpr auto WasmBaselineProbes = std::to_array({"exception-handling"}); + // feature those flags need (Chrome 137, Firefox 131, Safari 18.4). SIMD128 + // is older, but V8 and SpiderMonkey switch it off on x86 CPUs without + // SSE4.1, which virtual machines often present. + inline constexpr auto WasmBaselineProbes = std::to_array({"simd", "exception-handling"}); } export namespace Crafter { @@ -750,7 +751,8 @@ export namespace Crafter { // it only to engines that report relaxed-SIMD support (Chrome 114+, Firefox // 120+) and falls back to the baseline outputName.wasm everywhere else // (e.g. Safari, which still gates relaxed SIMD behind a flag as of mid - // 2026). Idempotent. Call before EnableWasiBrowserRuntime so the emitted + // 2026). Both need what WasmBaselineProbes names, so an engine without + // exception handling gets neither but an error saying so. Idempotent. Call before EnableWasiBrowserRuntime so the emitted // variants.json manifest includes it. A no-op for non-wasm targets at build // time (no wasm is produced), but harmless to declare unconditionally. void EnableWasiRelaxedSimdVariant(Configuration& cfg); diff --git a/packaging/flatpak/net.catcrafts.CrafterBuild.yaml b/packaging/flatpak/net.catcrafts.CrafterBuild.yaml index 3584063..f812261 100644 --- a/packaging/flatpak/net.catcrafts.CrafterBuild.yaml +++ b/packaging/flatpak/net.catcrafts.CrafterBuild.yaml @@ -135,11 +135,22 @@ modules: - -DWASI_SDK_INSTALL_TO_CLANG_RESOURCE_DIR=ON - -DWASI_SDK_TARGETS=wasm32-wasip1;wasm32-wasip1-threads - -DWASI_SDK_INCLUDE_TESTS=OFF + # crafter-build compiles every wasm unit with C++ exceptions and links + # the libc++ and libunwind built with them, from lib//eh. The + # default is DUAL only when the driving clang is 23 or newer and OFF + # below that; said outright, an LLVM downgrade cannot quietly ship a + # sysroot that no wasm build links against. + - -DWASI_SDK_EXCEPTIONS=DUAL - -DCMAKE_INSTALL_PREFIX=/app post-install: # wasi-sdk installs into /share/wasi-sysroot; crafter-build # looks there (Crafter.Build:Toolchain), so nothing to move. - test -f /app/share/wasi-sysroot/share/libc++/v1/std.cppm + # The wasm tests skip when these are missing, as on a distro sysroot, + # so the Flatpak checks for them itself: without them its CI would + # pass while every wasm build failed. + - test -f /app/share/wasi-sysroot/lib/wasm32-wasip1/eh/libunwind.a + - test -f /app/share/wasi-sysroot/lib/wasm32-wasip1-threads/eh/libunwind.a sources: - type: git url: https://github.com/WebAssembly/wasi-sdk.git diff --git a/tests/WasiBrowserRuntime/main.cpp b/tests/WasiBrowserRuntime/main.cpp index 63a1c2a..9f0fe27 100644 --- a/tests/WasiBrowserRuntime/main.cpp +++ b/tests/WasiBrowserRuntime/main.cpp @@ -87,9 +87,10 @@ int main() { } // With no declared variants, variants.json is a single baseline entry - // pointing at outputName.wasm, with only the probe for what every wasm - // build needs: exception handling, so that runtime.js can turn an engine - // without it away with a message rather than a CompileError. + // pointing at outputName.wasm, with only the probes for what every wasm + // build needs, SIMD128 and exception handling, so that runtime.js can + // turn an engine without them away with a message rather than a + // CompileError. { std::ifstream vin(*variants); std::string vjson{std::istreambuf_iterator(vin), std::istreambuf_iterator()}; @@ -97,8 +98,8 @@ int main() { std::println(std::cerr, "variants.json missing baseline url: {}", vjson); return 1; } - if (vjson.find("\"probes\":[\"exception-handling\"]") == std::string::npos) { - std::println(std::cerr, "variants.json baseline does not probe for exception handling: {}", vjson); + if (vjson.find("\"probes\":[\"simd\",\"exception-handling\"]") == std::string::npos) { + std::println(std::cerr, "variants.json baseline does not probe for SIMD and exception handling: {}", vjson); return 1; } if (vjson.find("relaxed-simd") != std::string::npos) { diff --git a/tests/WasmVariants/main.cpp b/tests/WasmVariants/main.cpp index 27ab0a9..af26a9b 100644 --- a/tests/WasmVariants/main.cpp +++ b/tests/WasmVariants/main.cpp @@ -97,10 +97,11 @@ int main() { Check(j.find("\"label\":\"relaxed-simd\"") != std::string::npos, "manifest has relaxed-simd label"); Check(j.find("\"wvhelper.relaxed-simd.wasm\"") != std::string::npos, "manifest has labelled url"); Check(j.find("\"relaxed-simd\"") != std::string::npos, "manifest has relaxed-simd probe"); - // Every file is built with exceptions, so every entry needs them - // besides its own features; the baseline needs nothing else. - Check(j.find("\"probes\":[\"relaxed-simd\",\"exception-handling\"]") != std::string::npos, "relaxed-simd entry also probes for exception handling"); - Check(j.find("\"url\":\"wvhelper.wasm\",\"probes\":[\"exception-handling\"]") != std::string::npos, "baseline entry probes for exception handling only"); + // Every file is built with SIMD128 and exceptions, so every entry + // needs them besides its own features; the baseline needs + // nothing else. + Check(j.find("\"probes\":[\"relaxed-simd\",\"simd\",\"exception-handling\"]") != std::string::npos, "relaxed-simd entry also probes for the baseline's features"); + Check(j.find("\"url\":\"wvhelper.wasm\",\"probes\":[\"simd\",\"exception-handling\"]") != std::string::npos, "baseline entry probes for its own features only"); // baseline fallback entry. Check(j.find("\"wvhelper.wasm\"") != std::string::npos, "manifest has baseline url"); // The relaxed-simd variant must precede the baseline so the runtime diff --git a/wasi-runtime/runtime.js b/wasi-runtime/runtime.js index d8dbef1..75b9f04 100644 --- a/wasi-runtime/runtime.js +++ b/wasi-runtime/runtime.js @@ -876,15 +876,18 @@ class Wasi { // GoogleChromeLabs/wasm-feature-detect, inlined so we ship no dependency. A // variant in variants.json lists the probe names that must all pass for that // variant to be served; an unknown name is treated as unsupported (we fall -// through to a less demanding variant, ultimately the baseline). Keep the keys -// in sync with the `probes` a WasmVariant declares in Crafter.Build, and with -// WasmBaselineProbes, which every entry carries. +// through to a less demanding variant, ultimately the baseline, and to an +// error naming what is missing when not even the baseline fits). Keep the +// keys in sync with the `probes` a WasmVariant declares in Crafter.Build, and +// with WasmBaselineProbes, which every entry carries. const WASM_PROBES = { // relaxed SIMD: i8x16.relaxed_swizzle (Chrome 114+, Firefox 120+; Safari // still flag-gated as of mid-2026). "relaxed-simd": () => WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,10,15,1,13,0,65,1,253,15,65,2,253,15,253,128,2,11])), - // fixed-width SIMD128 (v128.const + i8x16.popcnt) — the current baseline, - // here for completeness / future baseline shifts. + // fixed-width SIMD128 (v128.const + i8x16.popcnt), which every build + // uses (-msimd128). V8 and SpiderMonkey switch it off on x86 CPUs without + // SSE4.1, so it is a baseline probe even though every engine with + // exception handling has it. "simd": () => WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,10,10,1,8,0,65,0,253,15,253,98,11])), // tail calls (return_call). "tail-call": () => WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,6,1,4,0,18,0,11])),