Conversation
Runs rwasm in the prebuilt webR container, which already carries Emscripten, a wasm-targeting LLVM flang for the Fortran a large part of CRAN still contains, a host R and rwasm itself. Reassembling that would mean owning a toolchain nobody here wants to maintain. The image tag comes from the recipe's webrVersion, as the emscripten strategy takes its version from emscriptenVersion. An R operation has no compiled binary, so the per-operation copying differs from the C strategies. What gets published is the package library as an Emscripten filesystem image, which every operation in the recipe shares, plus the script named in `bin`, which ships with the recipe rather than being produced by the build. A script named in `bin` but absent from the recipe directory is reported against the operation that asked for it, rather than surfacing as a bare copy error. Two rwasm behaviours are worked around in the build script, both of which otherwise yield a build that reports success and ships nothing usable. add_pkg() resolves every reference in its built-in webr-remotes list on each call whatever was requested, and that resolution currently fails in this container, so remotes is passed as NULL. It also reduces a failed package build to a warning and continues, exiting 0 having produced no binary, so the requested packages are checked against what actually landed. The package list reaches the container as an environment variable rather than interpolated into the R source. The container path itself needs Docker and is exercised in CI. What is verified here is the surrounding logic, against a stubbed build: the registry tree and bundle for an R recipe carry the library, metadata and script digests and no wasm block; a missing script fails with a message naming the operation; and an emscripten recipe still produces exactly its .js and .wasm with an unchanged wasm block.
Five defects, none of which would have shown up in a passing build. An operation's `bin` was typed as a bare string. Under the R strategy it names a script inside the contributed recipe directory and is used to build a path, so a recipe could reach outside its own directory with "../.." and have whatever it found copied into the plugin folder, which publish then pushes to the registry wholesale. It is now constrained to a filename, matching how `id` is already constrained. The check that every requested package produced a binary only recovered a package name from plain, cran:: and @Version references. A GitHub or url reference reduced to something that could never match its own artefact, so a build that succeeded was reported as having produced nothing. remotes = NULL skips rwasm's webR-patched forks as well as the failing resolution it was added for. That cost is now recorded where the decision is made, and a package rwasm keeps a patched fork of is refused outright rather than quietly built from unpatched CRAN source and left to misbehave in a browser. A recipe could declare an R wasm build and a native build together. The two give `bin` incompatible meanings, so it validated and then failed in the native copy step looking for a compiled artifact under a script's name. The webR version a library was built against is now recorded in the bundle. These binaries are only loadable by that release, and without it a routine dependency bump in the frontend would break every published R bundle with nothing to attribute the failure to. Verified: eight schema cases including four path-traversal forms, and all 32 recipes still validate unchanged.
The check catches packages the recipe names. It does not catch one reached transitively, which dependencies: NA makes possible, since skipping prefer_remotes() gives up the patched forks throughout the resolved tree rather than only at its root. Recorded rather than left for someone to discover. A missing webr-remotes file also degraded to an empty list, quietly removing the guard altogether. That is now an error.
A recipe with an empty build.wasm block raised an AttributeError out of the validator instead of reporting a bad recipe, because the key parses as None. The bin pattern used ^ and $, and Python's $ also matches before a trailing newline, so "summary.R\n" passed a check whose whole purpose is to require a bare filename. It is anchored with \A and \Z now, and bounded in length. The two reference checks in the R builder normalised names differently: the clash check kept the version suffix while the missing-artifact check stripped it, so a url:: reference reduced to igraph_2.0.3.tar.gz in one and igraph in the other, and a patched package could be built from unpatched CRAN source without tripping the guard meant to refuse it. Both now share one normaliser. Verified: six schema cases including the trailing newline, an over-long name and a leading dash; the normaliser across six reference forms; and all 32 recipes unchanged.
Cerberus drops type and schema checking for a None value but still runs check_with, so an empty mapping in a recipe reaches these functions as None and raises out of the validator instead of being reported as a bad recipe. The previous commit fixed validate_wasm_strategy; the same fault was in validate_build_combination and, from before this work, validate_output_mode, where a bare list entry under outputs triggers it. All three are guarded, and the null forms are covered: a null build, a null build.wasm, a null entry in inputs, outputs or parameters, and a null operation all now report a validation failure. The 32 existing recipes are unaffected.
This was referenced Aug 5, 2026
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What it does
Runs
rwasmin the prebuilt webR container, which already carries Emscripten, a wasm-targeting LLVM flang for the Fortran a large part of CRAN still contains, a host R, and rwasm itself. Reassembling that here would mean owning a toolchain nobody wants to maintain. The image tag comes from the recipe'swebrVersion, exactly as the emscripten strategy takes its version fromemscriptenVersion.Why the per-operation copying differs
An R operation has no compiled binary. What gets published is:
library.data.gz+library.js.metadata), shared by every operation in the recipe, andbin, which ships with the recipe rather than being produced by the build.So the bundle carries an
rblock instead of awasmone:A script named in
binbut missing from the recipe directory is reported against the operation that asked for it, rather than surfacing as a barecopyfileerror naming only a path.Two rwasm behaviours worked around
Both otherwise produce a build that reports success and ships nothing usable — and both were observed in #27, not guessed at:
add_pkg()resolves its entire built-inwebr-remoteslist on every call, whatever was actually requested, and that resolution currently fails in this container (res_one_row_df: nrow(out) must equal 1).remotes = NULLskips it.add_pkg()reduces a failed package build to awarning()and continues, exiting 0 having produced no binary. The requested packages are checked against what actually landed.The package list reaches the container as an environment variable rather than interpolated into the R source, so nothing from a recipe is parsed as code.
Verification, and its limits
The container path needs Docker and is only exercised in CI — I could not run it locally, so treat the actual
docker runas unverified until it runs here. What is verified, against a stubbed build:wasmblock.jsand.wasm, with an unchangedwasmblock — the C path is untouchedNote the repo already requires Python 3.12+:
builder.pyuses PEP 701 nested quotes in f-strings, which is why the CI pins 3.12. Worth knowing if anyone runs the hub on an older interpreter.Known cost
The library image is copied per operation, so a recipe with several operations duplicates it in each plugin directory. That matches how the C strategies already give each operation its own binary, and the images are small (~340 KB for two packages), but a shared layer would be the better end state once more than one R recipe exists.
Merge order
Stacked on #28 — base it on
add-r-build-strategy, notmaster, and merge #28 first.sbom-implementationis the priority branch and rewrites parts of the builders. This PR conflicts with it onhub/builders/builder.pyandhub/validate/validate.py. Rebase after it lands.