mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-08 02:36:10 +00:00
fix(cli): support arm64 hosts for --docker render (#1196)
* 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 #1193
Closes #1194
Closes #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.
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildDockerRunArgs, type DockerRenderOptions } from "./dockerRunArgs.js";
|
||||
import {
|
||||
buildDockerRunArgs,
|
||||
resolveDockerPlatform,
|
||||
type DockerRenderOptions,
|
||||
} from "./dockerRunArgs.js";
|
||||
|
||||
const BASE: DockerRenderOptions = {
|
||||
fps: { num: 30, den: 1 },
|
||||
@@ -18,6 +22,10 @@ const FIXED_INPUT = {
|
||||
projectDir: "/abs/proj",
|
||||
outputDir: "/abs/out",
|
||||
outputFilename: "out.mp4",
|
||||
// Pin platform in tests so snapshots are arch-independent (otherwise they
|
||||
// flip between linux/amd64 and linux/arm64 depending on the host running
|
||||
// the test).
|
||||
platform: "linux/amd64",
|
||||
};
|
||||
|
||||
describe("buildDockerRunArgs", () => {
|
||||
@@ -290,4 +298,80 @@ describe("buildDockerRunArgs", () => {
|
||||
const args = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
|
||||
expect(args).not.toContain("--no-page-side-compositing");
|
||||
});
|
||||
|
||||
// Regression for #1193: an arm64 host (Apple Silicon) was being pinned to
|
||||
// linux/amd64, which forced qemu emulation of chrome-headless-shell and
|
||||
// produced either navigation timeouts or chrome SEGVs. Each host arch must
|
||||
// land in its native --platform value.
|
||||
it("emits linux/arm64 when host platform is arm64", () => {
|
||||
const args = buildDockerRunArgs({
|
||||
imageTag: "hyperframes-renderer:0.0.0-test",
|
||||
projectDir: "/abs/proj",
|
||||
outputDir: "/abs/out",
|
||||
outputFilename: "out.mp4",
|
||||
platform: "linux/arm64",
|
||||
options: BASE,
|
||||
});
|
||||
const idx = args.indexOf("--platform");
|
||||
expect(idx).toBeGreaterThanOrEqual(0);
|
||||
expect(args[idx + 1]).toBe("linux/arm64");
|
||||
});
|
||||
|
||||
it("emits linux/amd64 when platform is explicitly amd64", () => {
|
||||
const args = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
|
||||
const idx = args.indexOf("--platform");
|
||||
expect(idx).toBeGreaterThanOrEqual(0);
|
||||
expect(args[idx + 1]).toBe("linux/amd64");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveDockerPlatform", () => {
|
||||
it("maps arm64 hosts to linux/arm64", () => {
|
||||
expect(resolveDockerPlatform("arm64", {})).toBe("linux/arm64");
|
||||
});
|
||||
|
||||
it("maps x64 hosts to linux/amd64", () => {
|
||||
expect(resolveDockerPlatform("x64", {})).toBe("linux/amd64");
|
||||
});
|
||||
|
||||
it("treats unknown architectures as linux/amd64 (safe default)", () => {
|
||||
expect(resolveDockerPlatform("riscv64", {})).toBe("linux/amd64");
|
||||
});
|
||||
|
||||
// Regression guard: the production call site is `resolveDockerPlatform()`
|
||||
// with no args. If a refactor drops either default parameter, every other
|
||||
// arch-mapping test would still pass — this one fails loudly.
|
||||
it("uses process.arch and process.env when called with no arguments", () => {
|
||||
const result = resolveDockerPlatform();
|
||||
// Must equal the explicit-arg form (env override notwithstanding, which
|
||||
// wouldn't be set in the test runner unless deliberately stubbed).
|
||||
const expected = process.env.HYPERFRAMES_DOCKER_PLATFORM
|
||||
? process.env.HYPERFRAMES_DOCKER_PLATFORM
|
||||
: resolveDockerPlatform(process.arch, {});
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it("honors HYPERFRAMES_DOCKER_PLATFORM override on an arm64 host (Rosetta-Node / parity-regen escape hatch)", () => {
|
||||
expect(resolveDockerPlatform("arm64", { HYPERFRAMES_DOCKER_PLATFORM: "linux/amd64" })).toBe(
|
||||
"linux/amd64",
|
||||
);
|
||||
});
|
||||
|
||||
it("honors HYPERFRAMES_DOCKER_PLATFORM override on an amd64 host", () => {
|
||||
expect(resolveDockerPlatform("x64", { HYPERFRAMES_DOCKER_PLATFORM: "linux/arm64" })).toBe(
|
||||
"linux/arm64",
|
||||
);
|
||||
});
|
||||
|
||||
it("trims whitespace from HYPERFRAMES_DOCKER_PLATFORM and ignores empty override", () => {
|
||||
expect(resolveDockerPlatform("arm64", { HYPERFRAMES_DOCKER_PLATFORM: " linux/amd64 " })).toBe(
|
||||
"linux/amd64",
|
||||
);
|
||||
// Empty/whitespace-only override falls back to arch detection — important
|
||||
// for shells where `export FOO=""` would otherwise pin platform to "".
|
||||
expect(resolveDockerPlatform("arm64", { HYPERFRAMES_DOCKER_PLATFORM: "" })).toBe("linux/arm64");
|
||||
expect(resolveDockerPlatform("arm64", { HYPERFRAMES_DOCKER_PLATFORM: " " })).toBe(
|
||||
"linux/arm64",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user