Implements the Phase A A4 NIT-1 fix from codex Phase A gate review
(verdict GO, 2 NITs flagged): the local equivalent of the Step 1b §6
+ §7.1 CI gate, runnable by developers before submitting Phase A-E
PRs. The full CI workflow remains DEFERRED in
`.github/workflows/rust-release.yml` until brew emscripten install +
EMSDK + .wasm.a → .a symlink + wasm-bindgen + wasm-opt are
automated; until then this script is the authoritative gate.
What it enforces (matches spec §6 + §7.1):
- EMSDK env var present (build-time-only emsdk libcxx headers +
wasm-aware clang)
- cargo build → wasm-bindgen → wasm-opt -Oz pipeline succeeds
- 0 env.* imports in the post-bindgen bundle (any leak = LinkError
at load time = regression)
- gzip size ≤ STEP1B_SHELL_WASM_GZIP_LIMIT_BYTES (default 1 MiB)
Verified on the C-hard.2 bundle: 613 006 bytes gzip (58% of 1 MiB
ceiling), 0 env.* imports, exit 0.
Step 1b Phase A — completes A3 (local) + A4 (codex verdict GO).
88 lines
3.7 KiB
Bash
Executable file
88 lines
3.7 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# tools/check-wasm-bundle.sh — Step 1b §6 + §7.1 local bundle gate.
|
|
#
|
|
# What this script enforces (matches spec §6 ceilings and the §7.1
|
|
# env.* import count guard):
|
|
# 1. EMSDK is set so the build can resolve emsdk's libcxx headers
|
|
# + wasm-aware clang (build-time only; emscripten runtime is NOT
|
|
# linked into the bundle — see spec §2.2).
|
|
# 2. cargo build → wasm-bindgen → wasm-opt -Oz pipeline produces
|
|
# crates/openpencil-shell-web/pkg/openpencil_shell_web_bg.wasm.
|
|
# 3. Post-bindgen bundle has 0 env.* imports
|
|
# (i.e. all imports come from `./openpencil_shell_web_bg.js`,
|
|
# the wasm-bindgen JS shim). Any env.* import = LinkError at
|
|
# load time → regression → fail.
|
|
# 4. Post wasm-opt -Oz gzip size ≤ STEP1B_SHELL_WASM_GZIP_LIMIT_BYTES
|
|
# (default 1 048 576 bytes = 1 MiB; spec §6 ceiling for the
|
|
# shell-web wasm sub-component).
|
|
#
|
|
# Why this is a local script and not (yet) a CI workflow: the C-hard
|
|
# pipeline needs brew-emscripten + EMSDK + a .wasm.a → .a symlink hack
|
|
# in the skia-bindings out/ dir before the render bundle can be built
|
|
# in CI. That automation is tracked as DEFERRED in
|
|
# `.github/workflows/rust-release.yml`. Until it lands, this script
|
|
# is the source-of-truth gate for developers running the build
|
|
# locally before merging Phase A-E changes.
|
|
#
|
|
# Exit semantics:
|
|
# 0 all four checks PASS.
|
|
# 1 any check FAILED — message names which one.
|
|
# 2 prerequisite missing (EMSDK / wasm-bindgen / wasm-opt / node).
|
|
|
|
set -euo pipefail
|
|
|
|
CRATE_DIR="crates/openpencil-shell-web"
|
|
PKG_DIR="${CRATE_DIR}/pkg"
|
|
WASM_RAW="${PKG_DIR}/openpencil_shell_web_bg.wasm"
|
|
WASM_OPT="${PKG_DIR}/openpencil_shell_web_bg.opt.wasm"
|
|
TARGET_WASM="target/wasm32-unknown-unknown/release/openpencil_shell_web.wasm"
|
|
|
|
# Spec §6 row "Per-component ceiling — shell-web wasm (cdylib) gzip"
|
|
# = 1 MiB. Override via env for experiments only.
|
|
LIMIT="${STEP1B_SHELL_WASM_GZIP_LIMIT_BYTES:-1048576}"
|
|
|
|
step() { printf '\n[step %d/%d] %s\n' "$1" "$2" "$3"; }
|
|
fail() { printf 'FAIL: %s\n' "$1" >&2; exit 1; }
|
|
need() { command -v "$1" >/dev/null 2>&1 || { printf 'missing prerequisite: %s\n' "$1" >&2; exit 2; }; }
|
|
|
|
step 1 5 "Verify prerequisites"
|
|
need cargo
|
|
need wasm-bindgen
|
|
need wasm-opt
|
|
need node
|
|
need gzip
|
|
[ -n "${EMSDK:-}" ] || { printf 'EMSDK env var unset (needed for emsdk libcxx headers + wasm-aware clang)\n' >&2; exit 2; }
|
|
|
|
step 2 5 "Build shell-web wasm32-unknown-unknown with --features skia"
|
|
cargo build -p openpencil-shell-web \
|
|
--target wasm32-unknown-unknown --features skia --release >/dev/null
|
|
|
|
step 3 5 "wasm-bindgen --target web → ${PKG_DIR}/"
|
|
wasm-bindgen --target web --out-dir "${PKG_DIR}" "${TARGET_WASM}" >/dev/null
|
|
|
|
step 4 5 "Verify 0 env.* imports (spec §7.1 import guard)"
|
|
env_count="$(node -e '
|
|
const fs = require("fs");
|
|
const buf = fs.readFileSync(process.argv[1]);
|
|
WebAssembly.compile(buf).then(mod => {
|
|
const imps = WebAssembly.Module.imports(mod);
|
|
const env = imps.filter(i => i.module === "env");
|
|
console.log(env.length);
|
|
}).catch(e => { console.error("compile failed:", e); process.exit(1); });
|
|
' "${WASM_RAW}")"
|
|
if [ "${env_count}" != "0" ]; then
|
|
fail "env.* import count = ${env_count} (spec §7.1 requires 0); a new symbol leaked — add it to crates/wasm-libc-shim/src/imp.rs"
|
|
fi
|
|
printf ' ✓ 0 env.* imports\n'
|
|
|
|
step 5 5 "Verify gzip size ≤ ${LIMIT} bytes (spec §6 ceiling)"
|
|
wasm-opt -Oz "${WASM_RAW}" -o "${WASM_OPT}" >/dev/null
|
|
gz_bytes="$(gzip -c "${WASM_OPT}" | wc -c | tr -d ' ')"
|
|
if [ "${gz_bytes}" -gt "${LIMIT}" ]; then
|
|
fail "shell-web wasm gzip size ${gz_bytes} bytes > ceiling ${LIMIT} bytes"
|
|
fi
|
|
pct=$(( (gz_bytes * 100) / LIMIT ))
|
|
printf ' ✓ gzip size %s bytes (%d%% of %s ceiling)\n' "${gz_bytes}" "${pct}" "${LIMIT}"
|
|
|
|
printf '\nAll Step 1b §6 + §7.1 bundle gates PASS.\n'
|