Skip to content

build: ship a genuine pure-Python wheel for Pyodide/JupyterLite - #632

Merged
petercorke merged 4 commits into
mainfrom
fix/pyodide-pure-python-wheel
Aug 25, 2026
Merged

build: ship a genuine pure-Python wheel for Pyodide/JupyterLite#632
petercorke merged 4 commits into
mainfrom
fix/pyodide-pure-python-wheel

Conversation

@petercorke

Copy link
Copy Markdown
Owner

Summary

Closes #627.

Replaces the cross-compiled wasm32 wheel (built via cibuildwheel's Pyodide platform, forced to cp312/Pyodide 0.27.x to stay JSPI-safe for Safari — see PR #626) with a genuine py3-none-any wheel built on a normal runner.

  • CMakeLists.txt: new RTB_BUILD_EXTENSIONS option (default ON) wraps both nanobind_add_module calls. Both _fknm_c and _frne_c already have complete, tested pure-Python fallbacks (fknm.py/frne.py, tests/test_fknm_fallback.py).
  • pyproject.toml: RTB_PURE_WHEEL=1 triggers a scikit-build-core override (wheel.platlib = false) that forces the wheel tag to py3-none-any — confirmed directly from scikit-build-core's own wheel_tag.py (root_is_purelibplats=["any"], abi="none", pyvers=["py3"]).
  • release.yml: build_pyodide job is now a plain python -m build --wheel, no Emscripten SDK, no cibuildwheel. Since the wheel has no ABI to be tag-rejected over, upload_pypi's wheel-exclusion step and the whole upload_pyodide_asset job (GitHub-Release-asset workaround) are gone — it uploads to PyPI like every other wheel.
  • ci.yml: docs-build now builds this wheel fresh on every push (fast, no compilation) instead of fetching the latest GitHub Release's wasm asset — fixes a staleness gap as a side effect ("Try it Now" now tracks main, not the last release).
  • docs/source/intro.rst: updated the now-inaccurate "available as a GitHub release resource" line.

spatialgeometry's own wheel is untouched here (still fetched cross-repo, still cp312-pyodide_wasm32-tagged) — SG hasn't adopted the same fix yet (jhavl/spatialgeometry#46 tracks it as a natural follow-up once this is proven out here).

Test plan

  • Local build with RTB_PURE_WHEEL=1: produces roboticstoolbox_python-1.4.0-py3-none-any.whl, "ninja: no work to do" confirms zero compiled targets.
  • Installed into a fresh, verified-isolated venv: _C_AVAILABLE is False for both fknm/frne; robot.fkine() and robot.jacob0() (pure-Python fallback paths) work correctly against a real Panda model; robot.ik_LM() correctly raises pointing at ikine_LM; robot.ikine_LM() (pure-Python IK) succeeds.
  • A normal build without RTB_PURE_WHEEL set is unaffected: still produces a compiled cp312-cp312-macosx_... wheel.
  • CI: this PR's own docs-build job will build the wheel and run a real jupyter lite build against it — watching for that to go green.

CMakeLists.txt: RTB_BUILD_EXTENSIONS option (default ON) wraps both
nanobind_add_module calls -- set -DRTB_BUILD_EXTENSIONS=OFF to skip them.
Both _fknm_c and _frne_c already have complete, tested pure-Python
fallbacks (fknm.py/frne.py, tests/test_fknm_fallback.py), so this is a
real working configuration.

pyproject.toml: RTB_PURE_WHEEL=1 env var triggers a scikit-build-core
override (wheel.platlib=false) that forces the wheel tag to py3-none-any,
confirmed by reading scikit-build-core's own wheel_tag.py. Combined with
the CMake option above (via cmake.define), this replaces cross-compiling
a wasm32 binary via cibuildwheel's Pyodide platform -- which coupled the
wheel to one specific Pyodide/Emscripten ABI version and to Pyodide's
JSPI-default-on cutover (Safari/Firefox lack JSPI) -- with an ordinary
wheel built on a normal runner. Verified locally: RTB_PURE_WHEEL=1 build
produces roboticstoolbox_python-1.4.0-py3-none-any.whl with zero compiled
targets ("ninja: no work to do"), installs and runs correctly (fkine,
jacob0, ikine_LM all verified against a fresh venv), and a normal build
without the env var is unaffected (still produces a compiled cp312 wheel).
release.yml's build_pyodide job no longer uses cibuildwheel's Pyodide
platform (no Emscripten SDK, no cross-compilation) -- just a plain
`python -m build --wheel` with RTB_PURE_WHEEL=1 on a normal ubuntu-latest
runner. Since the resulting py3-none-any wheel has no ABI to be rejected
over, upload_pypi's "exclude pyodide wheel" step and the whole
upload_pyodide_asset job (GitHub-Release-asset workaround) are gone --
it just uploads to PyPI like every other wheel.

ci.yml's docs-build job builds this wheel fresh from the checked-out
source on every push instead of fetching one from the latest GitHub
Release -- feasible now because there's no compilation involved, so it's
fast. This also fixes an existing staleness gap: "Try it Now" now tracks
bleeding-edge main instead of lagging behind the last release.

spatialgeometry's own wasm wheel is untouched here (still fetched via
cross-repo `gh release download`, still cp312-pyodide-tagged) -- SG
hasn't adopted the same fix yet (tracked: jhavl/spatialgeometry#46).
Updated that step's comment to reflect the new asymmetry accurately.

docs/source/intro.rst's "available as a GitHub release resource" line
was describing the now-removed upload_pyodide_asset mechanism; updated.
@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 0.00%. Comparing base (0ae645e) to head (f6c9a79).

Files with missing lines Patch % Lines
src/roboticstoolbox/models/URDF/URDFRobot.py 0.00% 2 Missing ⚠️
Additional details and impacted files
@@          Coverage Diff          @@
##            main    #632   +/-   ##
=====================================
  Coverage   0.00%   0.00%           
=====================================
  Files        143     143           
  Lines      14037   14035    -2     
=====================================
+ Misses     14037   14035    -2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

… path

Cherry-picked from the stale, never-merged feat/pyodide-pure-wheel-v2
branch (pushed 2026-07-23, 109 commits behind main as of this cherry-pick
-- too stale to merge as-is, but these two findings are real and still
live on main). That branch ran the full test suite inside a real Pyodide
environment with zero compiled extensions present; both bugs reproduce
identically with just the C extension absent, no real Pyodide required.

tests/test_ETS.py::test_insert: genuine off-by-one -- a 7-joint ETS
(jindex 0-6) tested with a 6-element q. The C extension has no bounds
checking, so q[6] silently read past the array end (undefined behaviour,
happened not to crash); the pure-Python fallback correctly raises
IndexError. Verified fail (IndexError against the pure wheel) -> fix
(q now has 7 elements) -> pass (both the pure-Python and compiled paths).

URDFRobot.py's _load_rd_module: the sys.platform == "emscripten" guard
only checked inside `except Exception`, not `except ImportError`.
GitPython's subprocess-spawn failure under Pyodide surfaces as a plain
ImportError, which the candidates loop's `except ImportError: continue`
treated as "try the next name" -- after exhausting every candidate this
fell through to a misleading "model renamed"/"not found" error instead of
the correct, actionable one. Fixed by checking the platform up front,
before the loop, since the outcome doesn't depend on which candidate name
is tried. Verified fail (misleading rename error) -> fix -> pass (correct
"browser sandbox" error), both by loading the pre/post-fix file content
directly (bypassing an unrelated editable-install path collision) and via
a proper pytest run against a real editable install of this worktree.
New regression test: tests/test_URDFRobot.py, same
patch.object(sys, "platform", "emscripten") pattern already used in
tests/test_collision.py.

Two other findings from that branch (test_ET.py's ungated C-only
assertions, a WASM-numpy-specific float tolerance in test_trajectory.py)
were not ported -- not live bugs, and the second can't be reproduced
without a real Pyodide/WASM numpy build. See #633 for the fuller
discussion, including why the branch's real-Pyodide CI job itself wasn't
ported (three of its four findings were actually pure-Python-path
coverage gaps, not WASM-specific -- catchable much more cheaply now that
build_pyodide produces a real installable pure-Python wheel).
@petercorke
petercorke merged commit 534f097 into main Aug 25, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JupyterLite wasm wheel likely still broken in Safari/Firefox: compiled extensions need an Emscripten pure-Python fallback build

1 participant