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
11 changes: 11 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,17 @@ jobs:
pip install build
python -m build --wheel --outdir docs/lite/pypi

- uses: actions/setup-node@v5
with:
node-version: "22"
cache: npm
cache-dependency-path: docs/lite/wasm_check/package-lock.json

- name: Check the JupyterLite install path actually works (real Pyodide, real WASM)
run: |
npm --prefix docs/lite/wasm_check ci
node docs/lite/wasm_check/check_pyodide_install.mjs docs/lite/pypi/*.whl

- name: Sync notebooks into JupyterLite content
run: |
mkdir -p docs/lite/files
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,6 @@ docs/source/stubs/*

# LLVM profiling data
*.profraw

### Node ###
node_modules/
75 changes: 75 additions & 0 deletions docs/lite/wasm_check/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Pyodide install check

A real-Pyodide, real-WASM check that the JupyterLite install path actually works —
not a mock, not a guess. Runs Pyodide under Node.js (the `pyodide` npm package),
which is the same WASM runtime a real browser uses, just without a browser or a
Web Worker around it.

## What this catches

machinevision-toolbox-python 2.2.0 shipped a broken JupyterLite demo: `tqdm` was
added as a real dependency, but nobody added it to the notebook's install cell, and
nothing tested it before release. This check exists so that class of bug fails CI
instead of shipping — it runs the real `ensure_installed()` from
`docs/notebooks/_mvtb_nb_bootstrap.py`, installs every seed package for real, and
confirms `import machinevisiontoolbox` actually succeeds afterward.

It reuses that function unmodified rather than re-listing the seed packages here —
a second copy of that list would just be a second place for it to drift.

## What this deliberately does NOT catch

Whether the *real* notebook's relative-URL wheel-install path (`pypi/<wheel>`)
resolves correctly inside a real browser's Pyodide Web Worker. Node has no
equivalent of a Worker's base URL, and faking one convincingly requires already
knowing the right answer — that question needs a real browser, and was already
answered that way (build the site locally with `jupyter lite build` +
`jupyter lite serve`, open it in an actual browser tab — see
`docs/notebooks/README.md`). This check sidesteps that entirely: it mounts the
wheel directly into Pyodide's own virtual filesystem and installs from there
(`emfs:/<wheel>`, a micropip scheme that reads straight from Pyodide's filesystem,
no HTTP involved) via a narrow, clearly-commented monkeypatch in
`check_pyodide_install.mjs`. Deliberate, not a shortcut — see that file's own
docstring for the reasoning.

If you ever suspect the relative-URL path itself is broken, this check will not
tell you — go build and serve the real site and check it in a real browser instead.

## Usage

```bash
python -m build --wheel --outdir docs/lite/pypi
npm --prefix docs/lite/wasm_check install # once per checkout, or after a version bump
node docs/lite/wasm_check/check_pyodide_install.mjs docs/lite/pypi/*.whl
```

Exits 0 and prints `PASS: Pyodide install check succeeded.` on success. On failure,
prints a diagnostic block explaining the likely cause (almost always: a new
dependency needs adding to `_mvtb_nb_bootstrap.py`'s seed list) and how to fix it,
followed by the real traceback.

## Known flakiness

Each run installs ~35 packages fresh from a CDN (jsdelivr). An occasional network
hiccup can produce a failure (e.g. `AbortError`) unrelated to any real regression —
if a CI run fails here, a straight re-run is a reasonable first move before
assuming something's actually broken. `pyodide`'s own local package cache
(`node_modules/.cache` after the first run) makes repeated local runs faster and
more reliable than the first one.

## Version pinning

`package.json` pins the `pyodide` npm package to the same version confirmed
working in real-browser testing (`314.0.3`, corresponding to the Pyodide runtime
that `jupyterlite-pyodide-kernel` currently bundles). If that drifts out of sync
with what the real deployed site actually uses, this check could pass while the
real site fails, or vice versa — see the toolbox-maintainer notes on JupyterLite
version pairing for why that pairing matters and isn't self-healing.

## Reusing this for another toolbox

This is meant to be a template — RTB, bdsim, etc. can copy this pattern for their
own JupyterLite setups (RTB's is compiled-extension-aware, per its own pyodide-wheel
notes, but the check-a-real-install-path idea transfers directly). Adjust the
`ensure_installed()` import path and the seed-package assumptions; the `emfs:`
redirect trick and the diagnostic-message design should carry over unchanged.
109 changes: 109 additions & 0 deletions docs/lite/wasm_check/check_pyodide_install.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#!/usr/bin/env node
/**
* Dependency-completeness check for the Pyodide/JupyterLite install path.
*
* Scope, deliberately narrow: this confirms that every package
* _mvtb_nb_bootstrap.py's Pyodide branch installs actually resolves, and
* that `import machinevisiontoolbox` succeeds afterward -- catching exactly
* the class of bug that shipped in machinevision-toolbox-python 2.2.0 (a
* new pyproject.toml dependency added but never added to the Pyodide
* seed-install list, breaking the JupyterLite demo silently).
*
* What this deliberately does NOT test: whether the real notebook's
* relative-URL wheel-install path resolves correctly inside a real
* browser's Pyodide Web Worker. Node has no equivalent of a Worker's base
* URL, and faking one convincingly requires already knowing the right
* answer -- that question needs a real browser, and was already answered
* that way (see docs/notebooks/README.md). This check mounts the wheel
* directly into Pyodide's own virtual filesystem instead (the `emfs:`
* micropip scheme) and installs from there, sidestepping the HTTP/URL
* layer entirely -- deliberately, not as a shortcut around it.
*
* Reuses the real, unmodified ensure_installed() from _mvtb_nb_bootstrap.py
* rather than re-listing the seed packages here -- that would just be a
* second, driftable copy of the exact list this check exists to keep
* honest.
*
* Usage:
* node check_pyodide_install.mjs <path-to-wheel>
*/

import { loadPyodide } from "pyodide";
import fs from "node:fs";
import path from "node:path";

const wheelPath = process.argv[2];
if (!wheelPath) {
console.error("Usage: node check_pyodide_install.mjs <path-to-wheel>");

Check warning on line 37 in docs/lite/wasm_check/check_pyodide_install.mjs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

docs/lite/wasm_check/check_pyodide_install.mjs#L37

HTML passed in to function 'console.error'
process.exit(2);
}

const wheelName = path.basename(wheelPath);
const bootstrapPath = new URL("../../notebooks/_mvtb_nb_bootstrap.py", import.meta.url);

const pyodide = await loadPyodide();
await pyodide.loadPackage("micropip");

pyodide.FS.writeFile("/" + wheelName, fs.readFileSync(wheelPath));

Check warning on line 47 in docs/lite/wasm_check/check_pyodide_install.mjs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

docs/lite/wasm_check/check_pyodide_install.mjs#L47

Found readFileSync from package "node:fs" with non literal argument at index 0

Check failure on line 47 in docs/lite/wasm_check/check_pyodide_install.mjs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

docs/lite/wasm_check/check_pyodide_install.mjs#L47

The application dynamically constructs file or path information.
pyodide.FS.writeFile("/_mvtb_nb_bootstrap.py", fs.readFileSync(bootstrapPath));

Check warning on line 48 in docs/lite/wasm_check/check_pyodide_install.mjs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

docs/lite/wasm_check/check_pyodide_install.mjs#L48

Found readFileSync from package "node:fs" with non literal argument at index 0

Check failure on line 48 in docs/lite/wasm_check/check_pyodide_install.mjs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

docs/lite/wasm_check/check_pyodide_install.mjs#L48

The application dynamically constructs file or path information.

const driver = `
import sys
sys.path.insert(0, "/")

import micropip
import _mvtb_nb_bootstrap as bootstrap

# Test-only scaffolding: redirect the one HTTP/relative-URL install call to
# the wheel already mounted in Pyodide's own virtual filesystem, so this
# check doesn't need to fake a browser Web Worker's base URL (see this
# file's module docstring for why that's out of scope here). Everything
# else -- the seed-package installs, which is what this check actually
# exists to catch drift in -- runs completely unmodified.
_real_install = micropip.install
async def _redirect_wheel_install(requirements, **kwargs):
if isinstance(requirements, str) and requirements.startswith("pypi/"):
requirements = "emfs:/${wheelName}"
return await _real_install(requirements, **kwargs)
micropip.install = _redirect_wheel_install

await bootstrap.ensure_installed()

from machinevisiontoolbox import Image # noqa: F401
print("import machinevisiontoolbox: OK")
`;

try {
await pyodide.runPythonAsync(driver);
console.log("\nPASS: Pyodide install check succeeded.");
} catch (err) {
console.error(`
============================================================
FAIL: the Pyodide/JupyterLite install check failed.

This means _mvtb_nb_bootstrap.py's Pyodide branch doesn't actually
install and import cleanly. Most likely cause: a new dependency was
added to pyproject.toml (or a notebook now needs a new package) without
being added to the seed-install list in _mvtb_nb_bootstrap.py -- the
same class of bug that broke the JupyterLite demo in 2.2.0.

To reproduce locally:
python -m build --wheel --outdir docs/lite/pypi
node docs/lite/wasm_check/check_pyodide_install.mjs docs/lite/pypi/*.whl

To fix a missing-dependency error specifically:
1. Add the missing package name to the micropip.install([...]) list
near the top of docs/notebooks/_mvtb_nb_bootstrap.py.
2. Run: python docs/notebooks/sync_bootstrap.py
3. Commit the regenerated notebooks along with the bootstrap file change.

If the error isn't a missing package, it may be a real Pyodide
compatibility problem with one of the dependencies -- not something this
script can fix for you, but the traceback below should say which package.

Original error below.
============================================================
`);
console.error(err);
process.exitCode = 1;
}
53 changes: 53 additions & 0 deletions docs/lite/wasm_check/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions docs/lite/wasm_check/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "mvtb-wasm-check",
"private": true,
"type": "module",
"description": "Real-Pyodide (Node.js) dependency-completeness check for the JupyterLite install path -- see check_pyodide_install.mjs and README.md.",
"dependencies": {
"pyodide": "314.0.3"
}
}