From 6433c9b3ded7979c3b09a6ca05a5121c117ad64c Mon Sep 17 00:00:00 2001 From: kingchenc Date: Wed, 10 Jun 2026 01:44:48 +0200 Subject: [PATCH] bindings/r: build the C ABI from source for the WebAssembly target (#244) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem r-universe builds every package to WebAssembly (webR) in addition to the native platforms, calling `./configure --host=wasm32-unknown-emscripten`. `bindings/r/configure` only knew Linux/macOS/Windows and exited with `unsupported OS 'Emscripten'`, so the **WASM job was the single red check** on an otherwise fully-published r-universe build (all native platforms + deploy are green; the package installs fine everywhere). ## Fix The r-universe wasm build image ships **cargo** (`/usr/local/cargo/bin`) and **emscripten** (`EMSDK` on PATH). So instead of needing a prebuilt wasm lib (which would risk an emscripten ABI/version mismatch), `configure` now detects the Emscripten host and **builds the C ABI staticlib from source** for `wasm32-unknown-emscripten` in-place — compiled with the image's own emscripten, so the ABI always matches. The static `libwickra.a` is linked into the package object (no shared lib, no rpath). `wickra-core`'s rayon batch needs threads (absent on wasm), so the wasm build drops it via `--no-default-features`. `wickra-c` now takes `wickra-core` as a direct path dep with `default-features = false` plus a default `parallel` feature that re-enables it for native builds (a member-level `default-features = false` is ignored when inheriting a workspace dep — that was the trap). ## Validated locally - `cargo build -p wickra-c` (default) → rayon present, builds. - `cargo build -p wickra-c --no-default-features` (the wasm feature path) → rayon **gone**, builds. - `cargo build --workspace`, clippy, fmt all clean; `configure` passes `sh -n`. ## Cannot be validated locally No Rust/emscripten wasm toolchain here. The wasm build only runs in r-universe, and `configure` downloads the matching `v${version}` source tag — so this takes effect from the **first release that includes it** (the tagged source must contain the feature toggle). Open risks: whether the image has the `wasm32-unknown-emscripten` Rust target pre-installed (configure runs `rustup target add` best-effort) and the wasm build time. Worth one r-universe rebuild to confirm. Not merging — review first. --- bindings/c/Cargo.toml | 15 ++++++++++++++- bindings/r/configure | 30 ++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/bindings/c/Cargo.toml b/bindings/c/Cargo.toml index b6e8958d..43b508f1 100644 --- a/bindings/c/Cargo.toml +++ b/bindings/c/Cargo.toml @@ -42,5 +42,18 @@ cast_sign_loss = "allow" similar_names = "allow" float_cmp = "allow" +[features] +# `parallel` (rayon-backed batch in wickra-core) is on by default for native +# builds. The wasm32-unknown-emscripten target has no threads, so the R +# package's wasm build (r-universe / webR) compiles this crate with +# --no-default-features to drop rayon; wickra-core falls back to its serial +# batch path, which is cfg-gated behind the same feature. +default = ["parallel"] +parallel = ["wickra-core/parallel"] + [dependencies] -wickra-core = { workspace = true } +# Direct path dep rather than `workspace = true`: a member-level +# `default-features = false` is ignored when inheriting a workspace dep that +# does not set it, which would leave rayon in the wasm build. wickra-c is +# `publish = false`, so no version pin is needed (and none to keep in sync). +wickra-core = { path = "../../crates/wickra-core", default-features = false } diff --git a/bindings/r/configure b/bindings/r/configure index 76a8209d..7495e410 100755 --- a/bindings/r/configure +++ b/bindings/r/configure @@ -13,6 +13,36 @@ set -e inc="" lib="" + +# WebAssembly (r-universe / webR): there is no prebuilt wasm C ABI to download, +# but the build image ships cargo (/usr/local/cargo/bin) and emscripten +# (EMSDK on PATH), so build the C ABI staticlib from source for +# wasm32-unknown-emscripten right here. Building it in the same image with the +# same emscripten avoids any ABI/version mismatch a prebuilt lib would risk. +# rayon (threads) is dropped via --no-default-features; the indicators are pure +# computation, so the serial path is functionally identical. +if [ "$(uname -s)" = "Emscripten" ]; then + version=$(sed -n 's/^Version:[[:space:]]*//p' DESCRIPTION) + echo "wickra: building C ABI from source for wasm32-unknown-emscripten (v${version})" + build=$(mktemp -d) + url="https://github.com/wickra-lib/wickra/archive/refs/tags/v${version}.tar.gz" + "${R_HOME}/bin/Rscript" -e "download.file('${url}', '${build}/src.tar.gz', mode = 'wb', quiet = TRUE)" \ + || { echo "wickra: failed to download source ${url}"; exit 1; } + "${R_HOME}/bin/Rscript" -e "untar('${build}/src.tar.gz', exdir = '${build}')" + root="${build}/wickra-${version}" + rustup target add wasm32-unknown-emscripten 2>/dev/null || true + ( cd "${root}" && cargo build -p wickra-c --release \ + --target wasm32-unknown-emscripten --no-default-features ) \ + || { echo "wickra: cargo wasm build failed"; exit 1; } + cp "${root}/bindings/c/include/wickra.h" src/wickra.h + cp "${root}/target/wasm32-unknown-emscripten/release/libwickra.a" src/libwickra.a + rm -rf "${build}" + # The static archive is linked into the package object; no shared lib to + # bundle and no rpath needed. + sed "s|@WICKRA_RPATH@||" src/Makevars.in > src/Makevars + exit 0 +fi + if [ -n "${WICKRA_INCLUDE_DIR}" ] && [ -n "${WICKRA_LIB_DIR}" ]; then echo "wickra: using C ABI from WICKRA_INCLUDE_DIR / WICKRA_LIB_DIR (dev override)" inc="${WICKRA_INCLUDE_DIR}"