From d6db7689822209a73907074234ca4bb90049491d Mon Sep 17 00:00:00 2001 From: Lumi Date: Sat, 10 Oct 2026 01:19:02 -0700 Subject: [PATCH] Make the WebAssembly rules build reproducible across checkouts Rust embedded absolute checkout, registry and toolchain paths in panic locations, and symbol names depended on the package path, so one commit built different module bytes from a symlinked checkout and shipped build-machine paths. Remap those paths and strip symbols: builds from two checkout paths are now identical, and the module is 454,612 bytes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BmRXrH3X8VnAPbuCgo6PgW --- core/README.md | 11 ++++++++--- core/wasm/Cargo.toml | 2 +- core/wasm/build.sh | 17 +++++++++++++++-- docs/deployment.md | 2 +- docs/gotchas.md | 7 +++++++ docs/testing.md | 4 ++++ 6 files changed, 36 insertions(+), 7 deletions(-) diff --git a/core/README.md b/core/README.md index cae2c2c..8ac0ddf 100644 --- a/core/README.md +++ b/core/README.md @@ -68,8 +68,13 @@ daylight-saving change, and requires identical engine JSON after every step. ## Size and speed (2026-10-10) Measured on one desktop with Node.js 24; not phone or download measurements. -The release module is 589,730 bytes (186,689 with gzip). Node instantiated it +The release module is 454,612 bytes (167,650 with gzip). Node instantiated it in about 2–3 ms. Reading a deliberately large state (240 chat lines, 60 -memories, 365 diary pages) took about 1.1–1.4 ms; the body-only patch read per +memories, 365 diary pages) took about 1.0–1.4 ms; the body-only patch read per tick about 0.01 ms, and one body advance about 0.01 ms (averages of 50 runs, -two runs). +three runs). + +The build is reproducible across checkouts: `core/wasm/build.sh` remaps the +checkout, Cargo registry and toolchain paths that panic locations embed, and +the release profile strips symbols. Builds of one commit from two different +checkout paths produced identical module, glue and declaration files. diff --git a/core/wasm/Cargo.toml b/core/wasm/Cargo.toml index 35b07dd..eb96839 100644 --- a/core/wasm/Cargo.toml +++ b/core/wasm/Cargo.toml @@ -24,4 +24,4 @@ opt-level = "s" lto = true codegen-units = 1 panic = "abort" -strip = "debuginfo" +strip = true diff --git a/core/wasm/build.sh b/core/wasm/build.sh index cdd6142..541614d 100755 --- a/core/wasm/build.sh +++ b/core/wasm/build.sh @@ -6,7 +6,8 @@ # # Requires Rust 1.88+ with the wasm32-unknown-unknown target and # wasm-bindgen-cli at exactly the version Cargo.lock pins for wasm-bindgen. -# Reads no other environment. OUT_DIR is replaced only after both steps pass. +# Reads CARGO_HOME (default ~/.cargo) only to remap its paths; it replaces +# any RUSTFLAGS for this build. OUT_DIR is replaced only after both steps pass. set -euo pipefail [ $# -le 1 ] || { sed -n '2,/^set -e/p' "$0" | sed '$d;s/^# \{0,1\}//' >&2; exit 2; } HERE=$(cd "$(dirname "$0")" && pwd) @@ -26,7 +27,19 @@ rustup target list --installed 2>/dev/null | grep -qx wasm32-unknown-unknown || echo "core/wasm/build.sh: run: rustup target add wasm32-unknown-unknown" >&2 exit 2 } -cargo build --release --locked --quiet --manifest-path "$HERE/Cargo.toml" --target wasm32-unknown-unknown +# Remap the checkout, Cargo registry and toolchain paths that panic locations +# would embed, so the same commit builds the same bytes on any machine and no +# build-machine path ships in the app. Symbols are stripped (Cargo.toml). +ROOT=$(dirname "$(dirname "$HERE")") +REGISTRY=${CARGO_HOME:-$HOME/.cargo}/registry/src +SYSROOT=$(rustc --print sysroot) +FLAGS=() +for map in "$ROOT=/tokenling" "$(realpath -m "$ROOT")=/tokenling" "$REGISTRY=/cargo" \ + "$(realpath -m "$REGISTRY")=/cargo" "$SYSROOT=/rust" "$(realpath -m "$SYSROOT")=/rust"; do + FLAGS+=("--remap-path-prefix=$map") +done +CARGO_ENCODED_RUSTFLAGS=$(IFS=$'\x1f'; echo "${FLAGS[*]}") \ + cargo build --release --locked --quiet --manifest-path "$HERE/Cargo.toml" --target wasm32-unknown-unknown STAGE=$(mktemp -d) trap 'rm -rf "$STAGE"' EXIT wasm-bindgen "$HERE/target/wasm32-unknown-unknown/release/tokenling_core.wasm" \ diff --git a/docs/deployment.md b/docs/deployment.md index a4b085f..6d35d53 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -128,7 +128,7 @@ Any static file server or static host works; nothing runs on the server. because those directories never change in place. Serve `sw.js` with `Cache-Control: no-cache` (or a short max-age) so worker updates arrive. - **Compression** is optional; the `.onnx` and runtime `.wasm` files barely - compress. The app's rules module (about 0.6 MB) shrinks to about a third + compress. The app's rules module (about 0.45 MB) shrinks to about a third with gzip. - Allow for sizes: about 97 MB per model and about 85 MB of runtime (a browser fetches only one of the `.wasm` builds; the one transformers.js loads is diff --git a/docs/gotchas.md b/docs/gotchas.md index 9b80e29..dfdc966 100644 --- a/docs/gotchas.md +++ b/docs/gotchas.md @@ -339,6 +339,13 @@ generated glue only works with the CLI of exactly the crate's version. Install the version `core/wasm/Cargo.lock` pins (`cargo install wasm-bindgen-cli --version X --locked`) rather than unpinning the crate. +**The same commit builds a different app hash on another machine or path.** +Rust embeds absolute source paths in panic locations and its symbol names +depend on the package path, so an unremapped WebAssembly build differs by +checkout (even through a symlinked path) and ships the build machine's +paths. `core/wasm/build.sh` remaps those paths and the release profile strips +symbols; keep both when changing the build. + **A Node test throws `document is not defined`.** Some browser modules read the page at import (`web/app/config.ts`). Modules the Node tests import (`web/core.ts`, `web/app/engine.ts`, `storage.ts`, `save-format.ts`) must not diff --git a/docs/testing.md b/docs/testing.md index be8cff1..c253cca 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -310,6 +310,10 @@ speed measurements. progress, both transfer directions preserving identity, offline recovery, a second (loopback alias) host and eventual stop, and 1280/768/360 px. The only console errors were the flow's injected network failures. +- Builds of the same commit from two checkout paths (one through a symlink) + first produced different modules: Rust embedded absolute source paths. + After remapping paths and stripping symbols, both produced identical + files, and the checks above passed again on that build. - Not re-tested for this change: physical phones, Firefox, install prompts, background alert delivery, upgrades between two releases, the 90-second one-thread fallback and CUDA serving. -- knotmirror 0.0.1