diff --git a/.emsdk-version b/.emsdk-version new file mode 100644 index 00000000..6b244dcd --- /dev/null +++ b/.emsdk-version @@ -0,0 +1 @@ +5.0.1 diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml index 58c6ff61..7f2d04c6 100644 --- a/.github/workflows/deploy-site.yml +++ b/.github/workflows/deploy-site.yml @@ -62,11 +62,14 @@ jobs: key: cargo-wasm-${{ hashFiles('Cargo.lock') }} restore-keys: cargo-wasm- + # Keyed on the pin, not a constant: a cache miss re-installs the same + # emscripten instead of silently picking up whatever `latest` resolves to + # that day, and bumping .emsdk-version invalidates the cache. - name: Cache emsdk uses: actions/cache@v4 with: path: local/emsdk - key: emsdk-latest + key: emsdk-${{ hashFiles('.emsdk-version') }} - name: Build wasm run: scripts/build-wasm.sh diff --git a/crates/toolpath-codex/src/io.rs b/crates/toolpath-codex/src/io.rs index 25441a9a..bc5340a4 100644 --- a/crates/toolpath-codex/src/io.rs +++ b/crates/toolpath-codex/src/io.rs @@ -173,7 +173,11 @@ mod tests { let (_t, io) = setup(); let day = io.resolver().sessions_root().unwrap().join("2026/04/21"); fs::create_dir_all(&day).unwrap(); - fs::write(day.join("rollout-2026-04-21T09-00-00-bbb.jsonl"), "not json").unwrap(); + fs::write( + day.join("rollout-2026-04-21T09-00-00-bbb.jsonl"), + "not json", + ) + .unwrap(); let ids = io.list_session_ids().unwrap(); assert_eq!(ids.len(), 2); diff --git a/crates/toolpath-codex/src/paths.rs b/crates/toolpath-codex/src/paths.rs index 610b2638..c047a0af 100644 --- a/crates/toolpath-codex/src/paths.rs +++ b/crates/toolpath-codex/src/paths.rs @@ -141,7 +141,10 @@ impl PathResolver { /// isn't stem-shaped or the file isn't at its dated path (the caller /// falls back to the tree walk). fn rollout_file_for_stem(&self, session_id: &str) -> Result> { - let Some(date) = session_id.strip_prefix("rollout-").and_then(|r| r.get(..10)) else { + let Some(date) = session_id + .strip_prefix("rollout-") + .and_then(|r| r.get(..10)) + else { return Ok(None); }; let parts: Vec<&str> = date.split('-').collect(); diff --git a/scripts/build-wasm.sh b/scripts/build-wasm.sh index 412a368a..1c9b9a4b 100755 --- a/scripts/build-wasm.sh +++ b/scripts/build-wasm.sh @@ -6,6 +6,13 @@ _wasm_js="${_root}/site/wasm/path.js" _wasm_bin="${_root}/site/wasm/path.wasm" _emsdk_dir="${_root}/local/emsdk" +# The single source of truth for the emscripten toolchain: this script installs +# it and the deploy workflow keys its emsdk cache on the file's hash, so a bump +# here invalidates the cache instead of silently reusing another compiler. +# `latest` is not usable as a pin — emscripten's wasm exception-handling ABI +# has broken this build across releases. +_emsdk_version="$(tr -d '[:space:]' < "${_root}/.emsdk-version")" + # --- Parse flags -------------------------------------------------------------- # --if-changed Skip build if outputs are newer than all Rust sources # --dev Use dev profile (fast incremental builds, no LTO/strip) @@ -48,9 +55,23 @@ fi # --- Ensure emsdk is available ------------------------------------------------ +# Warns rather than fails on a mismatch: developer machines carry their own +# emscripten, and refusing to build there would be worse than a link error the +# warning already explains. +warn_unless_pinned() { + local _active + _active="$(emcc -dumpversion 2>/dev/null || true)" + + if [ "${_active}" != "${_emsdk_version}" ]; then + echo "wasm: WARNING active emscripten is ${_active:-unknown}, pinned is ${_emsdk_version} (.emsdk-version)" >&2 + echo "wasm: WARNING link errors about __cpp_exception or _Unwind_* mean this skew" >&2 + fi +} + ensure_emsdk() { # Already on PATH? if command -v emcc &>/dev/null; then + warn_unless_pinned return 0 fi @@ -59,16 +80,18 @@ ensure_emsdk() { echo "wasm: Activating local emsdk..." # shellcheck source=/dev/null source "${_emsdk_dir}/emsdk_env.sh" 2>/dev/null + warn_unless_pinned return 0 fi # Bootstrap: clone + install + activate - echo "wasm: Installing emsdk to target/emsdk (one-time)..." + echo "wasm: Installing emsdk ${_emsdk_version} to local/emsdk (one-time)..." git clone --depth 1 https://github.com/emscripten-core/emsdk.git "${_emsdk_dir}" - "${_emsdk_dir}/emsdk" install latest - "${_emsdk_dir}/emsdk" activate latest + "${_emsdk_dir}/emsdk" install "${_emsdk_version}" + "${_emsdk_dir}/emsdk" activate "${_emsdk_version}" # shellcheck source=/dev/null source "${_emsdk_dir}/emsdk_env.sh" 2>/dev/null + warn_unless_pinned } ensure_emsdk diff --git a/scripts/quality_gates.sh b/scripts/quality_gates.sh index 0ad241d5..45a72012 100755 --- a/scripts/quality_gates.sh +++ b/scripts/quality_gates.sh @@ -47,11 +47,16 @@ _all_gates=(format shellcheck clippy test doc examples plugin site) # shellcheck disable=SC2329 gate_format() { + # Both checks always run so one pass reports every formatting problem, and + # either failing fails the gate — the gate's status must not be just the + # last command's. + local _failed=0 echo "--- cargo fmt ---" - cargo fmt --all --manifest-path "${_root}/Cargo.toml" --check 2>&1 + cargo fmt --all --manifest-path "${_root}/Cargo.toml" --check 2>&1 || _failed=1 echo "--- prettier ---" - cd "${_root}/site" || return 1 - npx --yes prettier --check --no-color "**/*.{md,css,json,js}" 2>&1 + # Subshell: gates run in this shell, so a bare `cd` would leak to the rest. + (cd "${_root}/site" && npx --yes prettier --check --no-color "**/*.{md,css,json,js}" 2>&1) || _failed=1 + return "${_failed}" } # shellcheck disable=SC2329 @@ -101,7 +106,7 @@ gate_site() { # (notably, every new worktree). Without it the build fails with # `sh: eleventy: command not found` and a stale "did you mean to install?" # warning that doesn't say which directory. - cd "${_root}/site" && pnpm install --frozen-lockfile 2>&1 && pnpm run build 2>&1 + (cd "${_root}/site" && pnpm install --frozen-lockfile 2>&1 && pnpm run build 2>&1) } # ── Runner ────────────────────────────────────────────────────────────────────