#!/usr/bin/env bash # Assemble the deployed single-file pages from source + shared partials + assets/. # # Every page (the Concepts landing, the editor app, and the device/form-factor # pages) is a source that shares code via markers: # /*@BUILD:include:src/@*/ inlines a shared partial (engine, seed lists, base CSS, header/footer/chrome) # @BUILD:favicon@ / @BUILD:logo-*@ inline base64 assets (voices are all synthesized — no samples) # This resolves them so each built page in dist/ is one self-contained file # (zero deps, works fully offline). deploy.sh runs this first. dist/ is generated — # don't edit or commit it. set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")" mkdir -p dist # Precompile the PM_K-1 CircuitPython firmware to .mpy. CircuitPython compiles a ~56KB .py at boot, # which fragments the heap and OOMs on the RP2040; a precompiled .mpy loads without compiling. Needs # Adafruit's mpy-cross matching the device's CircuitPython (10.2.1) -> emits CircuitPython mpy v6. MPYC="$PWD/tools/mpy-cross"; ROOT="$PWD" [[ -x "$MPYC" ]] || { echo "error: $MPYC missing (Adafruit mpy-cross for CircuitPython 10.2.1)" >&2; exit 1; } ( cd pico-cp && "$MPYC" app.py -o "$ROOT/dist/app.mpy" ) # compile from pico-cp/ so tracebacks read "app.py" echo "precompiled dist/app.mpy ($(stat -c%s dist/app.mpy) bytes <- $(stat -c%s pico-cp/app.py) source)" # PM_X-1 Explorer firmware uses the same mpy-cross. Output as dist/explorer-app.mpy so the Kit + Explorer # bundles each ship their own precompiled binary; the served URLs follow the same one-target-per-file rule. ( cd pico-explorer && "$MPYC" app.py -o "$ROOT/dist/explorer-app.mpy" ) echo "precompiled dist/explorer-app.mpy ($(stat -c%s dist/explorer-app.mpy) bytes <- $(stat -c%s pico-explorer/app.py) source)" # PM_G-1 "Grid" ships the native Rust firmware now (rust/pm-grid → pm-grid.uf2, built by # rust/pm-grid/build.sh and served by deploy.sh). The old CircuitPython build (pico-scroll/app.py) # is no longer bundled or served; the source stays in-repo as the reference port. python3 - <<'PY' import os, pathlib, re A = pathlib.Path("assets") def build(name): src = pathlib.Path(name).read_text() # 1) inline shared partials (function-replacement: no backslash/group interpretation) src = re.sub(r"/\*@BUILD:include:([^@]+)@\*/", lambda m: pathlib.Path(m.group(1)).read_text().rstrip("\n"), src) # 2) inline base64 assets (voices are all synthesized now — no samples) src = src.replace("@BUILD:favicon@", (A / "favicon.b64").read_text().strip()) src = src.replace("@BUILD:logo-dark@", (A / "logo-dark.b64").read_text().strip()) src = src.replace("@BUILD:logo-light@", (A / "logo-light.b64").read_text().strip()) src = src.replace("@BUILD:logo-side-dark@", (A / "logo-side-dark.b64").read_text().strip()) src = src.replace("@BUILD:logo-side-light@", (A / "logo-side-light.b64").read_text().strip()) src = src.replace("@BUILD:bravura@", (A / "bravura.woff2.b64").read_text().strip()) # SMuFL music font subset (PM_E-2 notation) assert "@BUILD:" not in src, f"unresolved build marker(s) remain in {name}" out = pathlib.Path("dist") / name out.write_text(src) return out.stat().st_size for name in ("index.html","editor.html","editor-beta.html","pm_e-2.html","player.html","mobile.html","mobile-sessions.html","teacher.html","stage.html","micro.html","showcase.html","kit.html","explorer.html","grid.html", "embed.html", "info-editor.html","info-pm_e-2.html","info-player.html","info-teacher.html","info-stage.html","info-micro.html","info-showcase.html","info-kit.html","info-explorer.html","info-grid.html"): print("built %s (%dKB)" % (name, build(name) // 1024)) pathlib.Path("dist/embed.js").write_text(pathlib.Path("embed.js").read_text()) # loader, served as-is print("copied embed.js") # PWA support files for mobile.html (the phone/tablet app): manifest, service worker, icons. for f in ("manifest.webmanifest", "mobile-sw.js"): pathlib.Path("dist/" + f).write_text(pathlib.Path(f).read_text()) for f in ("icon-192.png", "icon-512.png", "icon-180.png"): pathlib.Path("dist/" + f).write_bytes((A / f).read_bytes()) print("copied PWA files (manifest.webmanifest, mobile-sw.js, icon-{192,512,180}.png)") pathlib.Path("dist/pico-main.py").write_text(pathlib.Path("pico/main.py").read_text()) # PM_K-1 firmware, downloadable print("copied pico-main.py") _appsrc = pathlib.Path("pico-cp/app.py").read_text() # The A/B updater pushes app.py over USB-MIDI as 7-bit data, so it MUST be pure ASCII -- a stray # non-ASCII char (e.g. an em-dash in a comment) gets mangled to a NUL byte and bricks the device. _bad = [(i, c) for i, c in enumerate(_appsrc) if ord(c) > 0x7F] assert not _bad, "pico-cp/app.py has non-ASCII at %r -- keep it ASCII (version regex + clean source)" % (_bad[:5],) pathlib.Path("dist/pico-cp-app.py").write_text(_appsrc) # served for version reading + as readable reference # the editor pushes the PRECOMPILED .mpy (base64); serve it next to the source pathlib.Path("dist/pico-cp-app.mpy").write_bytes(pathlib.Path("dist/app.mpy").read_bytes()) print("copied pico-cp-app.py + pico-cp-app.mpy") _xsrc = pathlib.Path("pico-explorer/app.py").read_text() # PM_X-1 Explorer firmware (sibling to the Kit) _xbad = [(i, c) for i, c in enumerate(_xsrc) if ord(c) > 0x7F] assert not _xbad, "pico-explorer/app.py has non-ASCII at %r -- keep it ASCII (version regex + clean source)" % (_xbad[:5],) pathlib.Path("dist/pico-explorer-app.py").write_text(_xsrc) # editor reads APP_VERSION from here pathlib.Path("dist/pico-explorer-app.mpy").write_bytes(pathlib.Path("dist/explorer-app.mpy").read_bytes()) print("copied pico-explorer-app.py + pico-explorer-app.mpy") import zipfile # PM_K-1 CircuitPython drive bundle (download → unzip onto CIRCUITPY) with zipfile.ZipFile("dist/pm_k1_circuitpy.zip", "w", zipfile.ZIP_DEFLATED) as z: for f in ("code.py", "boot.py", "programs.json", "font_s.bin", "font_m.bin", "font_l.bin", "logo.bin", "midi.bin", "usb.bin", "README.md", "protect-firmware.sh"): z.write("pico-cp/" + f, f) z.write("dist/app.mpy", "app.mpy") # the precompiled firmware (NOT app.py - too big to compile on-device) z.write("dist/editor.html", "editor.html") # offline copy of the editor, on the drive print("zipped pm_k1_circuitpy.zip") # PM_X-1 Explorer drive bundle (download → unzip onto CIRCUITPY on the Pimoroni Explorer with CircuitPython for Pico 2) with zipfile.ZipFile("dist/pm_x1_circuitpy.zip", "w", zipfile.ZIP_DEFLATED) as z: for f in ("code.py", "boot.py", "programs.json", "README.md"): z.write("pico-explorer/" + f, f) for f in ("font_s.bin", "font_m.bin", "font_l.bin", "logo.bin", "midi.bin", "usb.bin"): z.write("pico-cp/" + f, f) # fonts + icons are resolution-agnostic; reuse the Kit's baked blobs z.write("dist/explorer-app.mpy", "app.mpy") z.write("dist/editor.html", "editor.html") print("zipped pm_x1_circuitpy.zip") # PM_G-1 Grid is the native Rust firmware now — no CircuitPython bundle (see rust/pm-grid). PY