The arm64 render image had no pinned browser: chrome-for-testing publishes
no linux-arm64 build, so Dockerfile.render fell back to Debian bookworm's
rolling `chromium` package. Its current arm64 build (150.0.7871.46) SIGTRAPs
at startup (exit 133), breaking `render --docker` 100% on Apple Silicon.
Install a pinned, non-Debian chrome-headless-shell from Playwright on arm64
(Google's build, not Debian's repackage). The wrapper wires whichever binary
landed into PRODUCER_HEADLESS_SHELL_PATH and now fails the build loudly if
neither is present, instead of silently using the broken Debian chromium.
Bonus: arm64 gains BeginFrame deterministic capture it previously lacked.
amd64 path is unchanged.
Verified on Apple Silicon: same arm64 image, Debian chromium 150 -> exit 133,
Playwright arm64 headless-shell (Chromium 149) -> exit 0.
* fix(cli): support arm64 hosts for `--docker` render
The Docker render path pinned `--platform linux/amd64` for both build
and run, which on Apple Silicon / Graviton forced qemu emulation of
chrome-headless-shell. The emulated chrome process either SEGV'd or
hung on page navigation, producing the failures reported in #1193 /
#1194 / #1195.
Derive the platform from `process.arch` instead. On arm64 hosts:
- The image builds natively (no qemu).
- The Dockerfile skips the chrome-headless-shell install because
Chrome for Testing only publishes a `linux64` build (verified
against the known-good-versions manifest).
- The wrapper script leaves `PRODUCER_HEADLESS_SHELL_PATH` unset
when no headless-shell binary is present, so the engine falls
back to the system chromium that the Dockerfile already
installs from apt and points at via `PUPPETEER_EXECUTABLE_PATH`.
`TARGETARCH` is forwarded as an explicit `--build-arg` instead of
relying on BuildKit's automatic platform args — the legacy
builder (and some BuildKit configs, including colima on macOS)
leaves it unset, which would silently bypass the arch conditional
in the Dockerfile.
Image tags are now suffixed with `-arm64` on arm64 hosts so amd64
and arm64 images of the same hyperframes version can coexist in
the local cache.
The arm64 path renders correctly but loses byte-for-byte parity
with amd64 (system chromium uses screenshot capture, not
HeadlessExperimental.beginFrame). The CLI prints a one-line
warning so users comparing against amd64 baselines know.
Verified on macOS 26.5 / M4 Max:
- Before: `qemu: unknown option 'type=gpu-process'` followed by a
chrome-headless-shell SIGSEGV after ~4 minutes.
- After: 300/300 frames captured in ~18s of render time (1m18s
wallclock including a one-time image build), MP4 produced.
Closes#1193Closes#1194Closes#1195
* fix(cli): address review feedback on docker arm64 fix
Follow-up to 61880cdc. Addresses one substantive review comment from
@vanceingalls and three self-review gaps.
1. Restore loud build failure on amd64 when chrome-headless-shell is
missing (per @vanceingalls). The original Dockerfile used an `&&`
chain that crashed the build if `find` returned empty; the new
`if/else` wrapper silently fell through to system chromium even on
amd64, which would mask golden-baseline regressions from a future
@puppeteer/browsers cache layout change. The else branch now checks
`TARGETARCH = amd64` and exits 1 with an actionable error, while
arm64 still falls through to the system-chromium wrapper cleanly.
2. Add `HYPERFRAMES_DOCKER_PLATFORM` env override. The fix derives
platform from `process.arch`, which silently picks the wrong arch
in three real-world cases: x64 Node under Rosetta on Apple Silicon
(re-triggers issue #1193), parity-regen for amd64 golden baselines
on an arm64 host, and DOCKER_HOST pointing at a remote daemon with
a different arch. Empty/whitespace override is a no-op (falls back
to arch detection) so `export FOO=""` doesn't pin platform to "".
3. Fail fast when `--gpu` is requested on arm64. Docker Desktop on
Apple Silicon doesn't implement `--gpus` passthrough; the previous
code would crash at `docker run` with an opaque device-driver
error. We now short-circuit with errorBox pointing at the env
override as the workaround.
4. Close the test gap on the default-arch resolution. Every previous
test passed `arch` explicitly; a refactor that dropped the
`= process.arch` default would pass all tests but break every arm64
host at runtime. Added one assertion that calls
`resolveDockerPlatform()` with no args, plus coverage for the env
override.
The new arm64 platform-checking logic is extracted into
`resolveDockerHostPlatform()` so `renderDocker` itself stays focused
on the build/run wiring (and below the fallow complexity gate).
Test plan:
- `bunx vitest run packages/cli/src/utils/dockerRunArgs.test.ts` — 31 passed (was 27).
- `bunx vitest run packages/cli` — 647 passed (was 643).
- E2E on macOS 26.5 / M4 Max: deleted the cached arm64 image, ran
`--docker --quality draft --workers 1` against the blank scaffold —
300/300 frames in 1m1s wallclock, MP4 produced.
## Summary
- **The `--docker` flag was a no-op stub** — `renderDocker` called the same local `executeRenderJob` as `renderLocal`, no container was ever launched
- Now `renderDocker` generates a Dockerfile, builds a versioned `hyperframes-renderer:<version>` image with Chrome/FFmpeg/fonts/chrome-headless-shell, and runs the render inside a container
- Image is cached per CLI version — first render builds (~2 min), subsequent renders reuse it
- Forces `linux/amd64` platform since chrome-headless-shell has no ARM Linux binary
- Uses `execFileSync` (array form) throughout to prevent shell injection
- Forwards `--quiet`, `--gpu`, and render config flags into the container
- `Dockerfile.render` added as a reference for manual builds
## Test plan
- [x] `hyperframes render --docker` builds image and produces valid MP4
- [x] Second run reuses cached image (no rebuild)
- [x] `--quiet` suppresses container output while keeping stderr for errors
- [x] Typecheck, lint, format all pass
- [ ] Verify `--gpu` with `--docker` on a machine with GPU access
🤖 Generated with [Claude Code](https://claude.com/claude-code)