From 235a0e483a9b6bca0e91b9cee5b7e5203e5a3667 Mon Sep 17 00:00:00 2001 From: marcocaggioni Date: Sat, 26 Sep 2026 16:22:07 -0400 Subject: [PATCH] Portable offline WASM export for the fit app and docs explorers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Export both the browser fit app and all nine documentation explorers with `marimo export html-wasm --offline`: the Python runtime, Pyodide, and every dependency wheel (including rheofit for the fit app) now ship with the page, so both boot with no network access and never touch a package index at runtime. - deploy-app.yml: install headless Chromium via Playwright, export app/fit_app.py with --offline - docs/export_interactive.py: export all explorers with --offline, then share the identical runtime (assets/pyodide/packages/lockfile) across all nine via docs/_static/interactive/_shared/ (one ~59 MB runtime instead of nine) - docs/requirements.txt: marimo>=0.25, add playwright - docs.yml CI build job: install Chromium for the offline export - .readthedocs.yaml: apt packages for headless Chromium + `playwright install chromium` in pre_build Verified: fit app reaches its upload screen and the TC explorer its full working screen (sliders live, plot renders) with all external requests blocked — zero external requests, zero page errors. --- .github/workflows/deploy-app.yml | 15 ++++++--- .github/workflows/docs.yml | 3 ++ .readthedocs.yaml | 29 ++++++++++++++++- docs/export_interactive.py | 53 +++++++++++++++++++++++++++++++- docs/requirements.txt | 6 ++-- 5 files changed, 97 insertions(+), 9 deletions(-) diff --git a/.github/workflows/deploy-app.yml b/.github/workflows/deploy-app.yml index 2885d59..0b9f957 100644 --- a/.github/workflows/deploy-app.yml +++ b/.github/workflows/deploy-app.yml @@ -25,11 +25,16 @@ jobs: - uses: astral-sh/setup-uv@v5 - - name: Install marimo - run: pip install marimo - - - name: Export app to WebAssembly - run: marimo export html-wasm app/fit_app.py -o dist --mode run + - name: Install marimo and Playwright + run: | + pip install "marimo>=0.25" playwright + python -m playwright install chromium --with-deps + + - name: Export app to WebAssembly (offline bundle) + # --offline bundles the Python runtime and all dependency wheels + # (incl. rheofit) with the page: the app boots with no network + # access and never touches a package index at runtime. + run: marimo export html-wasm app/fit_app.py -o dist --mode run --offline - name: Upload Pages artifact uses: actions/upload-pages-artifact@v3 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 7b4cf6f..4d13bec 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -21,6 +21,9 @@ jobs: pip install -r docs/requirements.txt pip install -e . + - name: Install Chromium (offline WASM export) + run: python -m playwright install chromium --with-deps + - name: Export interactive notebooks run: python docs/export_interactive.py diff --git a/.readthedocs.yaml b/.readthedocs.yaml index ffc7dd0..1db8b80 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -7,9 +7,36 @@ build: os: ubuntu-24.04 tools: python: "3.12" + # System libraries for the headless Chromium that the offline WASM + # export (marimo --offline) needs; list from Playwright's own dependency + # data for ubuntu-24.04/chromium. + apt_packages: + - libasound2t64 + - libatk-bridge2.0-0t64 + - libatk1.0-0t64 + - libatspi2.0-0t64 + - libcairo2 + - libcups2t64 + - libdbus-1-3 + - libdrm2 + - libgbm1 + - libglib2.0-0t64 + - libnspr4 + - libnss3 + - libpango-1.0-0 + - libx11-6 + - libxcb1 + - libxcomposite1 + - libxdamage1 + - libxext6 + - libxfixes3 + - libxkbcommon0 + - libxrandr2 jobs: pre_build: - # export marimo wasm explorers before Sphinx runs + # headless browser for the offline WASM export, then export the + # marimo explorers before Sphinx runs + - python -m playwright install chromium - python docs/export_interactive.py sphinx: diff --git a/docs/export_interactive.py b/docs/export_interactive.py index c4b323d..dfd7f80 100644 --- a/docs/export_interactive.py +++ b/docs/export_interactive.py @@ -6,6 +6,14 @@ Generated HTML goes to ``docs/_static/interactive//`` (gitignored); static preview PNGs are committed next to the model pages. +Each explorer is exported as an *offline* bundle (``--offline``): the Python +runtime, Pyodide, and every dependency wheel ship with the page, so the +explorers boot with no network access and never touch a package index at +runtime. The identical runtime directories (``assets/``, ``pyodide/``, +``packages/``, ``lockfile/``) are shared: the first explorer's copy is moved +to ``docs/_static/interactive/_shared/`` and every ``index.html`` is +rewritten to reference it relatively, so nine explorers cost one runtime. + To add a model: write ``docs/interactive/_explorer.py`` (marimo notebook, same layout as the others), add an entry to ``MODELS`` below, and add the iframe section to the model page. @@ -103,6 +111,8 @@ def export_wasm(name: str, notebook: Path) -> None: # marimo's wasm export shells out to `uv`; make sure it's on PATH. + # `--offline` bundles the Python runtime and all dependency wheels with + # the page (needs a headless Chromium: `playwright install chromium`). bindir = Path(sys.prefix) / ("Scripts" if os.name == "nt" else "bin") os.environ["PATH"] = str(bindir) + os.pathsep + os.environ["PATH"] out = STATIC_OUT / name @@ -110,12 +120,52 @@ def export_wasm(name: str, notebook: Path) -> None: shutil.rmtree(out) subprocess.run( [sys.executable, "-m", "marimo", "export", "html-wasm", - str(notebook), "-o", str(out), "--mode", "run"], + str(notebook), "-o", str(out), "--mode", "run", "--offline"], check=True, ) print(f"exported {notebook.name} -> {out}") +# Runtime directories every --offline export produces. Identical across +# explorers (same marimo version, same dependency set), so they are shared. +OFFLINE_RUNTIME_DIRS = ("assets", "pyodide", "packages", "lockfile") +SHARED = STATIC_OUT / "_shared" + + +def share_offline_runtime(names: list[str]) -> None: + """Share one offline runtime across all explorer exports. + + Moves the first explorer's runtime directories into ``_shared/`` and + rewrites every explorer's ``index.html`` to reference them via relative + URLs, deleting the per-explorer copies. The model pages' iframes keep + working unchanged: ``/index.html`` still exists, it just loads the + runtime from ``../_shared/``. + """ + first = STATIC_OUT / names[0] + SHARED.mkdir(parents=True, exist_ok=True) + for dirname in OFFLINE_RUNTIME_DIRS: + src, dest = first / dirname, SHARED / dirname + if not src.is_dir(): + raise RuntimeError(f"offline runtime missing from export: {src}") + if dest.exists(): + shutil.rmtree(dest) + shutil.move(str(src), str(dest)) + for name in names: + out = STATIC_OUT / name + html = out / "index.html" + text = html.read_text(encoding="utf-8") + for dirname in OFFLINE_RUNTIME_DIRS: + text = text.replace(f'"./{dirname}/', f'"../_shared/{dirname}/') + if '"./pyodide/' in text or '"./packages/' in text: + raise RuntimeError(f"unrewritten runtime URLs left in {html}") + html.write_text(text, encoding="utf-8") + for dirname in OFFLINE_RUNTIME_DIRS: + d = out / dirname + if d.is_dir(): + shutil.rmtree(d) + print(f"shared offline runtime -> {SHARED}") + + def preview_png(spec: dict) -> None: import matplotlib matplotlib.use("Agg") @@ -150,6 +200,7 @@ def main() -> None: for name, spec in MODELS.items(): export_wasm(name, spec["notebook"]) preview_png(spec) + share_offline_runtime(list(MODELS)) if __name__ == "__main__": diff --git a/docs/requirements.txt b/docs/requirements.txt index 0a35fd7..cf241cf 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -5,6 +5,8 @@ sphinx-rtd-theme>=2 # sphinxcontrib-mermaid: renders ```mermaid fences as diagrams on the HTML site sphinxcontrib-mermaid>=0.9 # marimo: exports the interactive model explorers to WebAssembly at build time -# (docs/export_interactive.py). uv is needed on PATH for the export. -marimo>=0.24 +# (docs/export_interactive.py). uv is needed on PATH for the export; the +# --offline flag needs a headless Chromium (playwright) plus marimo>=0.25. +marimo>=0.25 +playwright uv>=0.7