diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab83912a8..3bb6f5a97 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -188,34 +188,64 @@ jobs: # 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) and - # attaches it to each GitHub Release (PyPI rejects the pyodide_* - # platform tag), so reuse that rather than rebuilding here. 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. + # 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. # - # STOPGAP (see https://github.com/petercorke/robotics-toolbox-python/issues/579): - # only fetch the cp312 wheel, matching - # jupyterlite-pyodide-kernel==0.6.1 below. A release can carry wasm - # wheels for more than one CPython version (cibuildwheel's pyodide - # platform builds one per supported interpreter), and + # 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 - # fetching more than one wheel for the same package+version - # silently drops 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. Confirmed still present in - # jupyterlite_pyodide_kernel 0.7.1 (latest as of 2026-07-21), not - # just 0.6.1 -- an upstream bug to route around, not a version to - # bump past. Intended to be removed once the pure-Python wasm - # wheel work lands and this whole fetch step goes away. + # (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 }} run: | mkdir -p docs/lite/pypi gh release download --repo "$GITHUB_REPOSITORY" --pattern '*cp312*pyodide*' --dir docs/lite/pypi --clobber + - 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. + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release download --repo jhavl/spatialgeometry \ + --pattern '*cp312*pyodide*wasm32*' --dir docs/lite/pypi --clobber + - name: Sync notebooks into JupyterLite content # Copies *.ipynb plus any local asset directories (e.g. # docs/notebooks/figs/, referenced by relative path from markdown @@ -257,12 +287,23 @@ jobs: PY - name: Build JupyterLite site - # jupyterlite-pyodide-kernel is pinned deliberately: newer versions - # bundle a newer Pyodide runtime than the cp312 wasm wheel attached - # to releases targets, which manifests as a silent hang in the - # browser (no error, just a kernel that never responds) rather than - # a build-time failure. See toolbox-infrastructure.md for the full - # version-skew story before bumping this. + # jupyterlite-pyodide-kernel is pinned deliberately -- NOT just for + # wasm-ABI matching with the cp312 wheel fetched above, but because + # newer kernel releases bundle Pyodide >=0.27.7, which turned + # WebAssembly JSPI ("stack switching") on by default for + # run_until_complete(). Browsers without JSPI support (Safari has + # none as of 2026-08; Firefox only behind a flag) either crash the + # kernel outright or, with the enableRunUntilComplete: false + # workaround, turn run_until_complete into a fire-and-forget no-op -- + # letting a cell's package/import machinery run before an install has + # actually finished landing (confirmed live on bdsim: intermittent + # ModuleNotFoundError despite a successful-looking install cell). + # jupyterlite-pyodide-kernel==0.6.1 bundles Pyodide 0.27.6, genuinely + # predating this whole mechanism -- matches MVTB's and bdsim's + # existing pins. See the toolbox-maintainer skill's JupyterLite + # version-pinning section for the full story before bumping this. + # Once Safari (and Firefox out-of-flag) ship JSPI, revisit this pin + # across RTB/MVTB/bdsim together. run: | pip install jupyterlite-core "jupyterlite-pyodide-kernel==0.6.1" jupyter-server cd docs/lite && jupyter lite build --output-dir ../build/html/lite diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fe2b4b696..9e3b26bea 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -72,10 +72,32 @@ jobs: python3 --version pip3 --version pip3 show cibuildwheel 2>/dev/null || echo "cibuildwheel not installed locally (normal)" - echo "PYODIDE_VERSION=${{ vars.PYODIDE_VERSION || '0.27.5' }}" - name: Build Pyodide wheel - uses: pypa/cibuildwheel@v4.1.1 + # 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 @@ -139,14 +161,19 @@ jobs: name: sdist path: dist - # Exclude Pyodide wheels — PyPI rejects the pyodide_* platform tag. - # They are uploaded as release assets in the next step instead. + # 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 - uses: pypa/gh-action-pypi-publish@release/v1 with: - skip_existing: true + skip-existing: true upload_pyodide_asset: name: Attach Pyodide wheel to GitHub release diff --git a/pyproject.toml b/pyproject.toml index a232825eb..905dd5ecc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -178,6 +178,28 @@ archs = ["AMD64"] # 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. [tool.pytest.ini_options]