Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 27 additions & 47 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -184,62 +184,42 @@ jobs:
cd docs && make SPHINXOPTS="--keep-going" html
touch build/html/.nojekyll

- name: Fetch pyodide wheel for JupyterLite
# A plain `pip wheel`/`build` here would produce a native
# macOS/Linux wheel with the compiled _fknm_c/_frne_c extensions --
# useless in Pyodide (WASM). release.yml already cross-compiles a
# genuine wasm32 wheel via cibuildwheel (CIBW_PLATFORM=pyodide),
# forced to cp312 (see release.yml's build_pyodide job and
# pyproject.toml's [tool.cibuildwheel.pyodide] comment) and
# attaches it to each GitHub Release, since PyPI rejects this old,
# pre-standard `pyodide_*` platform tag. This means the "Try it
# Now" experience tracks the latest *release*, not bleeding-edge
# main -- deliberate, since a full Pyodide/Emscripten rebuild on
# every docs push would be slow.
#
# Deliberately staying on cp312/the old tag rather than the newer
# PEP-783 `pyemscripten_*` tag PyPI now accepts: Pyodide only
# supports cp313+ from 0.28.0a1 onward, by which point WebAssembly
# JSPI ("stack switching") was already on by default (since
# 0.27.7) -- and Safari has no JSPI support, Firefox only behind a
# flag. cp312/0.27.x is the newest JSPI-safe line. See the
# toolbox-maintainer skill's JupyterLite version-pinning section
# before "modernizing" this away.
#
# The `--pattern '*cp312*pyodide*'` filter also guards against a
# real upstream bug (https://github.com/petercorke/robotics-toolbox-python/issues/579):
# jupyterlite-pyodide-kernel's all.json generator keys entries by
# (name, version) and *overwrites* rather than accumulates, so if a
# release ever carried wasm wheels for more than one CPython
# version, fetching more than one would silently drop all but the
# alphabetically-last from the piplite index -- no build error,
# just a runtime "can't find a pure Python 3 wheel" in the browser.
# release.yml's CIBW_BUILD restriction means only one wasm wheel
# (cp312) is ever built now, so this shouldn't trigger in practice,
# but the filter costs nothing to keep as a defence-in-depth guard.
env:
GH_TOKEN: ${{ github.token }}
- name: Build pure-Python wheel for JupyterLite
# RTB_PURE_WHEEL=1 (see pyproject.toml's scikit-build-core override)
# skips _fknm_c/_frne_c and forces a py3-none-any tag -- both have
# complete, tested pure-Python fallbacks, so nothing is missing at
# runtime. This used to require fetching a wasm32 wheel
# cross-compiled by release.yml's build_pyodide job (via
# cibuildwheel's Pyodide platform) from the latest GitHub Release,
# since building that wheel here on every docs push would've been
# slow, and PyPI rejected its old-format tag outright. A pure wheel
# needs neither: it's fast enough to build fresh on every push (no
# compilation at all, see build_pyodide's comment in release.yml),
# so "Try it Now" now tracks bleeding-edge main instead of lagging
# behind the last release, and py3-none-any needs no cross-repo
# GitHub Release fetch at all.
run: |
mkdir -p docs/lite/pypi
gh release download --repo "$GITHUB_REPOSITORY" --pattern '*cp312*pyodide*' --dir docs/lite/pypi --clobber
pip install build
RTB_PURE_WHEEL=1 python3 -m build --wheel --outdir docs/lite/pypi

- name: Fetch spatialgeometry pyodide wheel for JupyterLite
# spatialgeometry is an unconditional runtime dependency of
# roboticstoolbox-python (since it stopped vendoring a pure-Python
# copy internally at 1.4.0), so `piplite.install` needs a wasm
# wheel for it too, staged in the same local index. Fetched from
# spatialgeometry's own GitHub Release (jhavl/spatialgeometry) --
# it doesn't publish this to PyPI at all (tech debt raised
# upstream: jhavl/spatialgeometry#46, though note that issue's
# "publish via the new PEP-783 tag" suggestion has the same
# JSPI/Safari problem as RTB's own wheel above if it means moving
# off cp312). Matching RTB's own wheel above, this fetches the
# cp312 build specifically. Low-risk despite the cross-repo fetch:
# spatialgeometry's own pyproject.toml notes its CMake build skips
# the compiled nanobind extension entirely under Emscripten, so
# this wheel is genuinely pure Python -- no Pyodide ABI-version
# coupling to track here, unlike the JSPI/cp312 discipline the
# RTB wheel above needs.
# it doesn't publish this to PyPI at all. SG's own CMake build
# already skips its compiled nanobind extension entirely under
# Emscripten (same idea as RTB_PURE_WHEEL above), so this wheel's
# *content* is genuinely pure Python -- but SG hasn't adopted the
# scikit-build-core override that makes the wheel *itself*
# py3-none-any yet, so it's still tagged cp312-cp312-pyodide_wasm32
# and still needs this cross-repo GitHub Release fetch rather than
# a plain PyPI install. Tech debt raised upstream:
# jhavl/spatialgeometry#46 -- once SG applies the same fix RTB just
# did, this step (and the cp312 filter) can go away in favour of a
# plain `pip download`/PyPI dependency resolution.
env:
GH_TOKEN: ${{ github.token }}
run: |
Expand Down
96 changes: 32 additions & 64 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,49 +60,43 @@ jobs:
path: ./wheelhouse/*.whl

build_pyodide:
name: Build Pyodide (wasm32) wheel
name: Build pure-Python wheel (for Pyodide/JupyterLite)
needs: verify_version
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v7

- name: Diagnostics
- uses: actions/setup-python@v7
with:
python-version: "3.12"

# Not a cross-compile: RTB_PURE_WHEEL=1 (see pyproject.toml's
# scikit-build-core override) skips both _fknm_c/_frne_c (they have
# complete, tested pure-Python fallbacks -- fknm.py/frne.py,
# tests/test_fknm_fallback.py) and forces the wheel tag to
# py3-none-any. No Emscripten SDK, no cibuildwheel Pyodide platform,
# no Pyodide-version coupling at all -- a plain wheel built on a
# normal runner. This replaced an earlier approach that cross-compiled
# a real wasm32 binary via cibuildwheel's Pyodide platform, which tied
# the wheel to one specific Pyodide/Emscripten ABI version and, worse,
# to a JSPI-safe-vs-unsafe split (Safari/Firefox lack WebAssembly JSPI,
# which Pyodide >=0.27.7 needs) -- see git history on this job and on
# pyproject.toml for that saga. A pure-Python wheel has no ABI to be
# wrong about, so none of that applies here.
- name: Build pure-Python wheel
run: |
python3 --version
pip3 --version
pip3 show cibuildwheel 2>/dev/null || echo "cibuildwheel not installed locally (normal)"

- name: Build Pyodide wheel
# Pinned to cibuildwheel 3.4.1 deliberately -- NOT the same version
# as the native wheel builds below (4.1.1) -- because the tag
# *format* pyodide-build emits turns out to be a property of the
# pyodide-build tool version, not of which Pyodide version it
# targets. Confirmed directly: forcing cibuildwheel 4.1.1 to build
# cp312/Pyodide 0.27.7 (via CIBW_BUILD + CIBW_ENABLE overrides)
# still produced a wheel tagged `pyemscripten_2024_0_wasm32` (the
# new PEP-783 tag), which Pyodide 0.27.6's micropip -- like every
# pre-PEP-783 Pyodide release -- hard-rejects with "Wheel was built
# with Emscripten vpyemscripten.2024.0 but Pyodide was built with
# Emscripten v3.1.58". There is no `pyodide-version` override that
# fixes this: cibuildwheel 4.1.1 cannot produce an old-tag wheel at
# all. cibuildwheel 3.4.1 predates the tag-format change and
# defaults to cp312/Pyodide 0.27.7 unprompted (matches
# spatialgeometry's own pyodide build, same version, same reason).
#
# This is why cp312/Pyodide 0.27.x matters at all: Pyodide only
# ships cp313+ support from 0.28.0a1 onward, by which point
# WebAssembly JSPI ("stack switching") was already on by default
# (since 0.27.7) -- and Safari has no JSPI support, Firefox only
# behind a flag. See pyproject.toml's [tool.cibuildwheel.pyodide]
# comment and the toolbox-maintainer skill's JupyterLite
# version-pinning section for the full story before changing this.
uses: pypa/cibuildwheel@v3.4.1
env:
CIBW_PLATFORM: pyodide
pip install build
RTB_PURE_WHEEL=1 python3 -m build --wheel --outdir wheelhouse

- name: Verify wheel is genuinely pure Python
run: |
ls wheelhouse/
case "$(ls wheelhouse/)" in
*-py3-none-any.whl) echo "OK: py3-none-any" ;;
*) echo "ERROR: expected a py3-none-any wheel"; exit 1 ;;
esac

- uses: actions/upload-artifact@v7
if: success()
with:
name: wheels-pyodide
path: ./wheelhouse/*.whl
Expand Down Expand Up @@ -161,35 +155,9 @@ jobs:
name: sdist
path: dist

# Exclude the Pyodide wheel -- PyPI rejects the old, pre-standard
# `pyodide_*` platform tag this cp312 build produces. Deliberately
# staying on this tag (not the newer PEP-783 `pyemscripten_*` one) to
# keep the wasm build JSPI-safe for Safari/Firefox -- see the
# `build_pyodide` job's comment and the toolbox-maintainer skill's
# JupyterLite version-pinning section. It's attached as a GitHub
# Release asset in the next job instead.
- name: Remove Pyodide wheels from PyPI upload set
run: rm -f dist/*pyodide*.whl

# The pyodide wheel (py3-none-any) uploads to PyPI like any other
# wheel now -- no tag rejection, no GitHub-Release-asset workaround.
# See build_pyodide's comment for why.
- uses: pypa/gh-action-pypi-publish@release/v1
with:
skip-existing: true

upload_pyodide_asset:
name: Attach Pyodide wheel to GitHub release
needs: build_pyodide
if: github.event_name == 'release' && needs.build_pyodide.result == 'success'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v8
with:
name: wheels-pyodide
path: pyodide-dist

- name: Upload Pyodide wheel as release asset
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ github.ref_name }}
files: pyodide-dist/*.whl
69 changes: 42 additions & 27 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,47 @@ execute_process(
)
find_package(nanobind CONFIG REQUIRED HINTS "${NB_DIR}")

# ---------------------------------------------------------------------------
# _frne_c — Newton-Euler inverse dynamics
# ne.c / vmath.c are pure C maths; frne_nb.cpp is the nanobind glue.
# ---------------------------------------------------------------------------
nanobind_add_module(_frne_c
src/roboticstoolbox/robot/cpp-extensions/vmath.c
src/roboticstoolbox/robot/cpp-extensions/ne.c
src/roboticstoolbox/robot/cpp-extensions/frne_nb.cpp
)
target_include_directories(_frne_c PRIVATE
src/roboticstoolbox/robot/cpp-extensions
)
# Set -DRTB_BUILD_EXTENSIONS=OFF to skip both compiled extensions and build a
# genuinely pure-Python wheel instead -- both _fknm_c and _frne_c have
# complete, tested pure-Python fallbacks (see fknm.py/frne.py and
# tests/test_fknm_fallback.py), so this is a real, working configuration,
# not just a stub. Used for the Pyodide/JupyterLite wheel (see release.yml's
# build_pyodide job and pyproject.toml's scikit-build-core override): rather
# than cross-compiling for wasm32 via cibuildwheel's Pyodide platform (which
# ties the wheel to a specific Pyodide/Emscripten ABI version -- see git
# history on this file/release.yml for the JSPI/tag saga that caused),
# building on a normal runner with extensions off produces an ordinary
# py3-none-any wheel with no such coupling at all.
option(RTB_BUILD_EXTENSIONS "Build the compiled _fknm_c/_frne_c nanobind extensions" ON)

# ---------------------------------------------------------------------------
# _fknm_c — forward kinematics, Jacobian, Hessian, IK
# Eigen is vendored as header-only in src/roboticstoolbox/ets/cpp-extensions/Eigen/
# fknm_nb.cpp is the nanobind glue; maths lives in methods/ik/linalg.
# ---------------------------------------------------------------------------
nanobind_add_module(_fknm_c
src/roboticstoolbox/ets/cpp-extensions/methods.cpp
src/roboticstoolbox/ets/cpp-extensions/ik.cpp
src/roboticstoolbox/ets/cpp-extensions/linalg.cpp
src/roboticstoolbox/ets/cpp-extensions/fknm_nb.cpp
)
target_include_directories(_fknm_c PRIVATE
src/roboticstoolbox/ets/cpp-extensions
)
if(RTB_BUILD_EXTENSIONS)
# -----------------------------------------------------------------------
# _frne_c — Newton-Euler inverse dynamics
# ne.c / vmath.c are pure C maths; frne_nb.cpp is the nanobind glue.
# -----------------------------------------------------------------------
nanobind_add_module(_frne_c
src/roboticstoolbox/robot/cpp-extensions/vmath.c
src/roboticstoolbox/robot/cpp-extensions/ne.c
src/roboticstoolbox/robot/cpp-extensions/frne_nb.cpp
)
target_include_directories(_frne_c PRIVATE
src/roboticstoolbox/robot/cpp-extensions
)

# -----------------------------------------------------------------------
# _fknm_c — forward kinematics, Jacobian, Hessian, IK
# Eigen is vendored as header-only in src/roboticstoolbox/ets/cpp-extensions/Eigen/
# fknm_nb.cpp is the nanobind glue; maths lives in methods/ik/linalg.
# -----------------------------------------------------------------------
nanobind_add_module(_fknm_c
src/roboticstoolbox/ets/cpp-extensions/methods.cpp
src/roboticstoolbox/ets/cpp-extensions/ik.cpp
src/roboticstoolbox/ets/cpp-extensions/linalg.cpp
src/roboticstoolbox/ets/cpp-extensions/fknm_nb.cpp
)
target_include_directories(_fknm_c PRIVATE
src/roboticstoolbox/ets/cpp-extensions
)

install(TARGETS _frne_c _fknm_c DESTINATION roboticstoolbox)
install(TARGETS _frne_c _fknm_c DESTINATION roboticstoolbox)
endif()
2 changes: 1 addition & 1 deletion docs/source/intro.rst
Original file line number Diff line number Diff line change
Expand Up @@ -658,7 +658,7 @@ to import them if the user attempts to exploit a functionality that requires it.
If a dependency is not installed, a warning provides instructions on how to install it using ``pip``.

C/C++ extensions are provided for recursive Newton-Euler dynamics and optimized forward and inverse kinematics for ETS defined robots. These
wheels are built by the GitHub CI actions. A pyodide wheel is also built for use in the browser and is available as a GitHub release resource.
wheels are built by the GitHub CI actions. A pure-Python wheel (using tested pure-Python fallbacks for the C/C++ functionality) is also built for use in the browser via Pyodide/JupyterLite, and is published to PyPI alongside the compiled wheels.


Spatial math layer
Expand Down
49 changes: 23 additions & 26 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,23 @@ sdist.include = [
]
sdist.exclude = ["rtb-data/**"]

# Set RTB_PURE_WHEEL=1 to build a genuinely pure-Python wheel: skips both
# compiled extensions (CMakeLists.txt's RTB_BUILD_EXTENSIONS option) and
# forces the wheel tag to py3-none-any (wheel.platlib=false — confirmed by
# reading scikit-build-core's own wheel_tag.py: root_is_purelib forces
# plats=["any"], abi="none", pyvers=["py3"]). Used for the Pyodide/
# JupyterLite build (see release.yml's build_pyodide job) -- both _fknm_c
# and _frne_c have complete, tested pure-Python fallbacks (fknm.py/frne.py,
# tests/test_fknm_fallback.py), so there's nothing missing at runtime.
# pip always prefers a platform-specific compiled wheel over py3-none-any
# when both are published (packaging.tags ranks specific tags higher) --
# this wheel is only ever selected where no compiled wheel exists, i.e.
# Pyodide today.
[[tool.scikit-build.overrides]]
if.env.RTB_PURE_WHEEL = true
wheel.platlib = false
cmake.define.RTB_BUILD_EXTENSIONS = "OFF"


[tool.ruff]
line-length = 88
Expand Down Expand Up @@ -174,32 +191,12 @@ archs = [
[tool.cibuildwheel.windows]
archs = ["AMD64"]

[tool.cibuildwheel.pyodide]
# Pyodide (wasm32-emscripten) — used by JupyterLite
# cibuildwheel manages the Emscripten SDK; scikit-build-core picks up
# the toolchain file automatically from the PYODIDE environment.
#
# Deliberately NO explicit `pyodide-version` override here -- the
# `build_pyodide` job in release.yml is pinned to cibuildwheel 3.4.1
# specifically (not the 4.1.1 used for native wheels) so it self-resolves
# its own default (cp312/Pyodide 0.27.7), matching spatialgeometry's own
# config exactly. An explicit `pyodide-version = "0.27.7"` override was
# tried and failed here: cibuildwheel 3.4.1's bundled pyodide-build
# (0.33.0) rejected it outright as "not compatible" even though it's
# that same tool's own documented default -- the override and the tool's
# internal default-resolution logic aren't equivalent, and only the
# latter actually works. Don't add one back without testing against a
# real build first.
#
# Why cp312/0.27.x specifically: Pyodide only ships cp313+ support from
# 0.28.0a1 onward, by which point WebAssembly JSPI ("stack switching")
# was already on by default (since 0.27.7) -- and Safari has no JSPI
# support, Firefox only behind a flag. cp312/0.27.x is the newest
# JSPI-safe line. jupyterlite-pyodide-kernel's pin in ci.yml must match
# (jupyterlite-pyodide-kernel==0.6.1, bundling Pyodide 0.27.6) -- see
# that file's "Fetch pyodide wheel for JupyterLite" step, and the
# toolbox-maintainer skill's JupyterLite version-pinning section for the
# full JSPI/Safari story before changing any of this.
# No [tool.cibuildwheel.pyodide] section -- the wasm/Pyodide wheel is no
# longer built via cibuildwheel's Pyodide platform (cross-compiling for a
# specific Emscripten/Pyodide ABI version was the whole source of the
# JSPI/tag-matching saga, see git history on this file and release.yml).
# It's built as an ordinary pure-Python wheel on a normal runner instead --
# see the RTB_PURE_WHEEL override above and release.yml's build_pyodide job.


[tool.pytest.ini_options]
Expand Down
Loading
Loading