In WebKit, ending a worker whose WebGPU device onnxruntime-web's JSPI build still holds ends the page with it (found by the First letters work, 215f65a4; seen here with Playwright's WebKit, 2026-10-02). On an iPad that is the end of every listen: the listener lets go as listening ends (the models take turns), as the sentence helper closes, after a failed run, and when it sits unused. The worker now asks the adapter for a device of its own (every limit and feature it offers) and hands it to the runtime for all three sessions. Unloading closes any listening, releases the sessions and destroys the device, which gives back all of its graphics memory; a load that fails destroys it too. listen.js ends a worker only once it has answered that unload (retire, used by dropWorker, through which every path that lets the listener go passes); one that never answers is left rather than ended. Its bytes changed, so the runtime directory is onnxruntime-web-1.30.0-aac4, with the worker re-pinned in RUNTIME_FILES and the directory in vendor.sh. The bundle and the WebAssembly are unchanged. Checked in Playwright's WebKit with the app's own code loading three sessions on WebGPU: closing the row (twice) and a failed run leave the page alive; the same steps with the previous worker end the page. Co-Authored-By: Claude Opus 5.5 <[email protected]>
171 lines
9.2 KiB
Shell
Executable file
171 lines
9.2 KiB
Shell
Executable file
#!/usr/bin/env bash
|
|
# ============================================================================
|
|
# vendor.sh — put the listener's runtime (Microsoft's onnxruntime-web) on the
|
|
# app's own site.
|
|
#
|
|
# third_party/onnxruntime-web/vendor.sh <site-dir> [cache-dir]
|
|
#
|
|
# Writes <site-dir>/vendor/onnxruntime-web-<version>-<revision>/ (and removes
|
|
# any other vendor/onnxruntime-web-* there):
|
|
# asr-worker.js this directory's worker.js, unchanged
|
|
# ort.jspi.bundle.min.mjs and ort-wasm-simd-threaded.jspi.wasm
|
|
# the WebGPU runtime for browsers with JavaScript
|
|
# Promise Integration (WebAssembly.Suspending), from
|
|
# the npm package, unchanged
|
|
# LICENSE.txt, NOTICE.txt this directory's licence and notice
|
|
# ThirdPartyNotices.txt ONNX Runtime's notices for the code it includes,
|
|
# at the same release, fetched and pinned
|
|
#
|
|
# Only this build: the listener is offered only where JSPI is (it reads the
|
|
# model one tensor at a time; without JSPI the runtime would hold the whole
|
|
# model in memory first). The package's other builds (asyncify, the
|
|
# processor-only WebAssembly one, WebGL, JSEP) are not shipped.
|
|
#
|
|
# The app never imports these URLs directly. When someone downloads the
|
|
# listener, src/listen.rs stores the worker and the one build their browser
|
|
# runs in the model store (SHA-256 checked, kept across app updates), and
|
|
# src/bindings/listen.js runs them from there, in a worker, offline.
|
|
#
|
|
# The version and revision come from ORT_WEB_VERSION and ORT_WEB_VENDOR_REV
|
|
# in crates/aac-web/src/listen.rs, and each runtime file's size and SHA-256
|
|
# from RUNTIME_FILES there. This script fails loudly on any drift: another
|
|
# version asked for than pinned here, an npm tarball that is not the pinned
|
|
# one, a vendored file that differs from the Rust pin, or a directory whose
|
|
# bytes differ from VENDOR_DIGEST. Changed bytes need a new revision, because
|
|
# browsers and the model store treat the directory's URLs as immutable.
|
|
#
|
|
# Idempotent; the download is cached in cache-dir (default ~/.cache/aac-rs).
|
|
# Needs node (npm).
|
|
# ============================================================================
|
|
set -euo pipefail
|
|
|
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
AAC="$(cd "$HERE/../.." && pwd)"
|
|
SITE="${1:?usage: vendor.sh <site-dir> [cache-dir]}"
|
|
CACHE="${2:-${XDG_CACHE_HOME:-$HOME/.cache}/aac-rs}"
|
|
|
|
LS="$AAC/crates/aac-web/src/listen.rs"
|
|
ORT_VERSION="$(sed -n 's/^pub const ORT_WEB_VERSION: &str = "\([^"]*\)";$/\1/p' "$LS")"
|
|
ORT_REV="$(sed -n 's/^pub const ORT_WEB_VENDOR_REV: &str = "\([^"]*\)";$/\1/p' "$LS")"
|
|
[ -n "$ORT_VERSION" ] || { echo "✗ ORT_WEB_VERSION not found in $LS" >&2; exit 1; }
|
|
[ -n "$ORT_REV" ] || { echo "✗ ORT_WEB_VENDOR_REV not found in $LS" >&2; exit 1; }
|
|
DIR="onnxruntime-web-$ORT_VERSION-$ORT_REV"
|
|
|
|
# ─── Pins ────────────────────────────────────────────────────────────────────
|
|
# The npm release, by its registry integrity (npm view [email protected]
|
|
# dist.integrity, read 2026-10-01; 1.30.0 is the latest stable, only
|
|
# 1.31.0-dev builds are newer).
|
|
PINNED_VERSION="1.30.0"
|
|
NPM_INTEGRITY="sha512-q0y+JrrtukXSzsBWEMccVfqX25LRmosXHF+CaRJmg8pZClzcV7svNc4rKY3jL02Vb7QmRMDs1SigqR4CXAfKYQ=="
|
|
# ONNX Runtime's LICENSE at tag v1.30.0 (commit f2c39fe2f838cf35ce7da92824f5a5e3ee6e88a7).
|
|
LICENSE_SHA256="2f07c72751aed99790b8a4869cf2311df85a860b22ded05fa22803587a48922c"
|
|
# ONNX Runtime's ThirdPartyNotices.txt at the same commit.
|
|
NOTICES_URL="https://raw.githubusercontent.com/microsoft/onnxruntime/f2c39fe2f838cf35ce7da92824f5a5e3ee6e88a7/ThirdPartyNotices.txt"
|
|
NOTICES_SHA256="143764b952fdb1a7c69ce653bfba74a7744d6a8a573bfb73e235fba356c83de3"
|
|
# What vendor/$VENDOR_PINNED holds, as a sha256 over its sorted file list and
|
|
# each file's sha256. Re-pin only together with a new ORT_WEB_VENDOR_REV.
|
|
VENDOR_PINNED="onnxruntime-web-1.30.0-aac4"
|
|
VENDOR_DIGEST="29e86994b4eb7224033963e085a7cb15673142db9db8ad7ac8bfae66df6bcfca"
|
|
# The files shipped from the package's dist/.
|
|
DIST_FILES=(ort.jspi.bundle.min.mjs ort-wasm-simd-threaded.jspi.wasm)
|
|
# The one link in the bundle: a page cited in an error message about
|
|
# cross-origin isolation, never fetched.
|
|
DOC_LINK="https://web.dev/cross-origin-isolation-guide/"
|
|
|
|
[ "$ORT_VERSION" = "$PINNED_VERSION" ] || {
|
|
echo "✗ listen.rs asks for onnxruntime-web $ORT_VERSION, but vendor.sh is pinned to $PINNED_VERSION —" >&2
|
|
echo " check the new release (licence, no network use, the graphs still run on WebGPU only), then re-pin PINNED_VERSION and NPM_INTEGRITY" >&2
|
|
exit 1
|
|
}
|
|
[ "$DIR" = "$VENDOR_PINNED" ] || {
|
|
echo "✗ listen.rs asks for $DIR, but vendor.sh is pinned to $VENDOR_PINNED — vendor it, check it, and re-pin VENDOR_PINNED and VENDOR_DIGEST" >&2
|
|
exit 1
|
|
}
|
|
|
|
sha256() { shasum -a 256 "$1" | cut -c1-64; }
|
|
fsize() { wc -c < "$1" | tr -d ' '; }
|
|
|
|
echo "▸ vendoring onnxruntime-web $ORT_VERSION ($DIR)"
|
|
mkdir -p "$CACHE"
|
|
|
|
# Tell (but do not fail) when npm has a newer stable release to review.
|
|
LATEST="$(npm view onnxruntime-web dist-tags.latest 2>/dev/null || true)"
|
|
if [ -n "$LATEST" ] && [ "$LATEST" != "$ORT_VERSION" ]; then
|
|
echo " note: npm's latest onnxruntime-web is $LATEST (pinned: $ORT_VERSION)"
|
|
fi
|
|
|
|
# The package, fetched and checked against its pinned integrity beside the
|
|
# cache entry, then moved in whole, so an interrupted run never leaves half a
|
|
# package that looks complete.
|
|
ORT_CACHE="$CACHE/onnxruntime-web-$ORT_VERSION"
|
|
if [ ! -f "$ORT_CACHE/package/package.json" ]; then
|
|
tmp="$(mktemp -d "$CACHE/npm.XXXXXX")"
|
|
(cd "$tmp" && npm pack --silent "onnxruntime-web@$ORT_VERSION" >/dev/null)
|
|
tgz="$(ls "$tmp"/*.tgz)"
|
|
got="sha512-$(openssl dgst -sha512 -binary "$tgz" | base64 | tr -d '\n')"
|
|
[ "$got" = "$NPM_INTEGRITY" ] || {
|
|
rm -rf "$tmp"
|
|
echo "✗ onnxruntime-web@$ORT_VERSION from npm is not the pinned tarball (got $got)" >&2
|
|
exit 1
|
|
}
|
|
(cd "$tmp" && tar xzf "$tgz")
|
|
rm -f "$tgz"
|
|
rm -rf "$ORT_CACHE"
|
|
mv "$tmp" "$ORT_CACHE"
|
|
fi
|
|
grep -q "\"version\": \"$ORT_VERSION\"" "$ORT_CACHE/package/package.json" || { echo "✗ cached onnxruntime-web is not $ORT_VERSION" >&2; exit 1; }
|
|
grep -q '"license": "MIT"' "$ORT_CACHE/package/package.json" || { echo "✗ onnxruntime-web $ORT_VERSION no longer declares MIT — review it" >&2; exit 1; }
|
|
[ "$(sha256 "$HERE/LICENSE.txt")" = "$LICENSE_SHA256" ] || { echo "✗ $HERE/LICENSE.txt is not ONNX Runtime's LICENSE at v1.30.0" >&2; exit 1; }
|
|
|
|
# ONNX Runtime's third-party notices: fetched to a temporary name and moved in
|
|
# only once their hash matches.
|
|
NOTICES="$CACHE/onnxruntime-$ORT_VERSION-ThirdPartyNotices.txt"
|
|
if [ ! -f "$NOTICES" ] || [ "$(sha256 "$NOTICES")" != "$NOTICES_SHA256" ]; then
|
|
rm -f "$NOTICES"
|
|
curl -fsSL "$NOTICES_URL" -o "$NOTICES.tmp"
|
|
[ "$(sha256 "$NOTICES.tmp")" = "$NOTICES_SHA256" ] || { rm -f "$NOTICES.tmp"; echo "✗ ThirdPartyNotices.txt ($NOTICES_URL) does not match its pinned sha256" >&2; exit 1; }
|
|
mv "$NOTICES.tmp" "$NOTICES"
|
|
fi
|
|
|
|
rm -rf "$SITE"/vendor/onnxruntime-web-*
|
|
OUT="$SITE/vendor/$DIR"
|
|
mkdir -p "$OUT"
|
|
cp "$HERE/worker.js" "$OUT/asr-worker.js"
|
|
cp "$HERE/LICENSE.txt" "$HERE/NOTICE.txt" "$OUT/"
|
|
cp "$NOTICES" "$OUT/ThirdPartyNotices.txt"
|
|
for f in "${DIST_FILES[@]}"; do cp "$ORT_CACHE/package/dist/$f" "$OUT/"; done
|
|
chmod 644 "$OUT"/*
|
|
|
|
# The runtime must never reach another site: no URL in the worker at all, and
|
|
# in the bundle none but the documentation link in one error message.
|
|
if grep -nE 'https?://' "$OUT/asr-worker.js"; then
|
|
echo "✗ the listener's worker names another site — review it" >&2; exit 1
|
|
fi
|
|
other="$(grep -oE "https?://[^\"'\` )]*" "$OUT/ort.jspi.bundle.min.mjs" | grep -vxF "$DOC_LINK" || true)"
|
|
[ -z "$other" ] || { echo "✗ ort.jspi.bundle.min.mjs names another site: $other — review it" >&2; exit 1; }
|
|
|
|
# Each runtime file must be exactly what listen.rs pins (RUNTIME_FILES): the
|
|
# app downloads it by that size and SHA-256.
|
|
PINS="$(sed -n 's/^ *RuntimeFile { name: "\([^"]*\)", bytes: \([0-9_]*\), sha256: "\([0-9a-f]*\)" },$/\1 \2 \3/p' "$LS" | awk '{ gsub(/_/, "", $2); print }')"
|
|
[ "$(printf '%s\n' "$PINS" | grep -c .)" = 3 ] || { echo "✗ RUNTIME_FILES in $LS should pin 3 files" >&2; exit 1; }
|
|
bad=0
|
|
while read -r name bytes want; do
|
|
f="$OUT/$name"
|
|
[ -s "$f" ] || { echo "✗ vendored runtime is missing $name" >&2; exit 1; }
|
|
got_bytes="$(fsize "$f")"; got="$(sha256 "$f")"
|
|
if [ "$got_bytes" != "$bytes" ] || [ "$got" != "$want" ]; then
|
|
echo "✗ vendor/$DIR/$name is $got_bytes bytes, sha256 $got;" >&2
|
|
echo " listen.rs RUNTIME_FILES pins $bytes bytes, sha256 $want." >&2
|
|
bad=1
|
|
fi
|
|
done <<< "$PINS"
|
|
[ "$bad" = 0 ] || { echo " Changed bytes need a new ORT_WEB_VENDOR_REV (a new URL), then new pins there and here." >&2; exit 1; }
|
|
|
|
DIGEST="$(cd "$OUT" && find . -type f | LC_ALL=C sort | while IFS= read -r f; do printf '%s %s\n' "$f" "$(sha256 "$f")"; done | shasum -a 256 | cut -c1-64)"
|
|
[ "$DIGEST" = "$VENDOR_DIGEST" ] || {
|
|
echo "✗ vendor/$DIR differs from what was pinned (got $DIGEST)." >&2
|
|
echo " Its URLs are cached as immutable: bump ORT_WEB_VENDOR_REV in listen.rs," >&2
|
|
echo " then set VENDOR_PINNED and VENDOR_DIGEST in $HERE/vendor.sh." >&2
|
|
exit 1
|
|
}
|
|
echo "✓ vendor/$DIR ($(du -sh "$OUT" | cut -f1))"
|