mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 14:50:02 +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:
@@ -49,7 +49,7 @@ import { maybePromptRenderFeedback } from "../telemetry/feedback.js";
|
|||||||
import { bytesToMb } from "../telemetry/system.js";
|
import { bytesToMb } from "../telemetry/system.js";
|
||||||
import { VERSION } from "../version.js";
|
import { VERSION } from "../version.js";
|
||||||
import { isDevMode } from "../utils/env.js";
|
import { isDevMode } from "../utils/env.js";
|
||||||
import { buildDockerRunArgs } from "../utils/dockerRunArgs.js";
|
import { buildDockerRunArgs, resolveDockerPlatform } from "../utils/dockerRunArgs.js";
|
||||||
import { normalizeErrorMessage } from "../utils/errorMessage.js";
|
import { normalizeErrorMessage } from "../utils/errorMessage.js";
|
||||||
import { findFFmpeg, getFFmpegInstallHint } from "../browser/ffmpeg.js";
|
import { findFFmpeg, getFFmpegInstallHint } from "../browser/ffmpeg.js";
|
||||||
import type { RenderJob } from "@hyperframes/producer";
|
import type { RenderJob } from "@hyperframes/producer";
|
||||||
@@ -632,15 +632,23 @@ function dockerImageExists(tag: string): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureDockerImage(version: string, quiet: boolean): string {
|
function dockerImageTagForPlatform(version: string, platform: string): string {
|
||||||
const tag = dockerImageTag(version);
|
// Suffix the tag with the arch so amd64 and arm64 images of the same
|
||||||
|
// hyperframes version coexist in the local cache (a developer who flips
|
||||||
|
// between hosts shouldn't have to rebuild).
|
||||||
|
const archSuffix = platform === "linux/arm64" ? "-arm64" : "";
|
||||||
|
return `${dockerImageTag(version)}${archSuffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureDockerImage(version: string, platform: string, quiet: boolean): string {
|
||||||
|
const tag = dockerImageTagForPlatform(version, platform);
|
||||||
|
|
||||||
if (dockerImageExists(tag)) {
|
if (dockerImageExists(tag)) {
|
||||||
if (!quiet) console.log(c.dim(` Docker image: ${tag} (cached)`));
|
if (!quiet) console.log(c.dim(` Docker image: ${tag} (cached)`));
|
||||||
return tag;
|
return tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!quiet) console.log(c.dim(` Building Docker image: ${tag}...`));
|
if (!quiet) console.log(c.dim(` Building Docker image: ${tag} (${platform})...`));
|
||||||
|
|
||||||
const dockerfilePath = resolveDockerfilePath();
|
const dockerfilePath = resolveDockerfilePath();
|
||||||
|
|
||||||
@@ -649,16 +657,27 @@ function ensureDockerImage(version: string, quiet: boolean): string {
|
|||||||
mkdirSync(tmpDir, { recursive: true });
|
mkdirSync(tmpDir, { recursive: true });
|
||||||
writeFileSync(join(tmpDir, "Dockerfile"), readFileSync(dockerfilePath));
|
writeFileSync(join(tmpDir, "Dockerfile"), readFileSync(dockerfilePath));
|
||||||
|
|
||||||
// linux/amd64 forced — chrome-headless-shell doesn't ship ARM Linux binaries
|
// Platform is now derived from the host arch (see resolveDockerPlatform).
|
||||||
|
// Apple Silicon and other arm64 hosts get a native linux/arm64 build; the
|
||||||
|
// Dockerfile skips chrome-headless-shell on arm64 and falls back to system
|
||||||
|
// chromium because chrome-headless-shell ships linux64 only.
|
||||||
|
//
|
||||||
|
// TARGETARCH is passed explicitly rather than relying on BuildKit's
|
||||||
|
// automatic platform args because the legacy builder (and some BuildKit
|
||||||
|
// configurations like colima 0.6.x) leaves it unset, which would defeat
|
||||||
|
// the arch conditional in the Dockerfile.
|
||||||
|
const targetArch = platform === "linux/arm64" ? "arm64" : "amd64";
|
||||||
try {
|
try {
|
||||||
execFileSync(
|
execFileSync(
|
||||||
"docker",
|
"docker",
|
||||||
[
|
[
|
||||||
"build",
|
"build",
|
||||||
"--platform",
|
"--platform",
|
||||||
"linux/amd64",
|
platform,
|
||||||
"--build-arg",
|
"--build-arg",
|
||||||
`HYPERFRAMES_VERSION=${version}`,
|
`HYPERFRAMES_VERSION=${version}`,
|
||||||
|
"--build-arg",
|
||||||
|
`TARGETARCH=${targetArch}`,
|
||||||
"-t",
|
"-t",
|
||||||
tag,
|
tag,
|
||||||
tmpDir,
|
tmpDir,
|
||||||
@@ -676,6 +695,47 @@ function ensureDockerImage(version: string, quiet: boolean): string {
|
|||||||
return tag;
|
return tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the Docker `--platform` for this host and enforces the constraints
|
||||||
|
* that come with it — keeping that policy out of `renderDocker` so the
|
||||||
|
* orchestrator stays focused on build/run wiring. May terminate the process
|
||||||
|
* via errorBox on unrecoverable mismatches (e.g. --gpu on arm64).
|
||||||
|
*/
|
||||||
|
function resolveDockerHostPlatform(options: RenderOptions): string {
|
||||||
|
const platform = resolveDockerPlatform();
|
||||||
|
|
||||||
|
// Docker Desktop on Apple Silicon (and colima with VZ) doesn't implement
|
||||||
|
// the `--gpus` host-passthrough flag, so requesting `--gpu` on a linux/arm64
|
||||||
|
// container fails at `docker run` with an opaque device-driver error. Catch
|
||||||
|
// it early with actionable guidance.
|
||||||
|
if (options.gpu && platform === "linux/arm64") {
|
||||||
|
errorBox(
|
||||||
|
"--gpu is not supported with --docker on arm64 hosts",
|
||||||
|
"Docker Desktop/colima on Apple Silicon doesn't expose --gpus host passthrough to linux/arm64 containers.",
|
||||||
|
"Drop --gpu, or run a native (non-Docker) render on this host, or set HYPERFRAMES_DOCKER_PLATFORM=linux/amd64 if you need GPU encoding (slow under qemu but works).",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.quiet && platform === "linux/arm64") {
|
||||||
|
// chrome-headless-shell doesn't publish a linux-arm64 build, so the arm64
|
||||||
|
// image falls back to system chromium. That loses byte-for-byte parity
|
||||||
|
// with amd64 renders — fine for end-user output, not fine if you're
|
||||||
|
// comparing against an amd64 golden baseline. Set
|
||||||
|
// HYPERFRAMES_DOCKER_PLATFORM=linux/amd64 to keep parity (qemu-emulated,
|
||||||
|
// slower).
|
||||||
|
console.log(
|
||||||
|
c.dim(
|
||||||
|
" Host is arm64 — using linux/arm64 image with system chromium " +
|
||||||
|
"(output won't be byte-identical to amd64 renders; " +
|
||||||
|
"set HYPERFRAMES_DOCKER_PLATFORM=linux/amd64 to force parity).",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return platform;
|
||||||
|
}
|
||||||
|
|
||||||
async function renderDocker(
|
async function renderDocker(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
outputPath: string,
|
outputPath: string,
|
||||||
@@ -689,9 +749,11 @@ async function renderDocker(
|
|||||||
console.log(c.dim(" Dev mode: using hyperframes@latest in Docker image"));
|
console.log(c.dim(" Dev mode: using hyperframes@latest in Docker image"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const platform = resolveDockerHostPlatform(options);
|
||||||
|
|
||||||
let imageTag: string;
|
let imageTag: string;
|
||||||
try {
|
try {
|
||||||
imageTag = ensureDockerImage(dockerVersion, options.quiet);
|
imageTag = ensureDockerImage(dockerVersion, platform, options.quiet);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
const isDockerMissing = /connect|not found|ENOENT/i.test(message);
|
const isDockerMissing = /connect|not found|ENOENT/i.test(message);
|
||||||
@@ -712,6 +774,7 @@ async function renderDocker(
|
|||||||
projectDir: resolve(projectDir),
|
projectDir: resolve(projectDir),
|
||||||
outputDir: resolve(outputDir),
|
outputDir: resolve(outputDir),
|
||||||
outputFilename,
|
outputFilename,
|
||||||
|
platform,
|
||||||
options: {
|
options: {
|
||||||
fps: options.fps,
|
fps: options.fps,
|
||||||
quality: options.quality,
|
quality: options.quality,
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
FROM node:22-bookworm-slim
|
FROM node:22-bookworm-slim
|
||||||
|
|
||||||
ARG HYPERFRAMES_VERSION=latest
|
ARG HYPERFRAMES_VERSION=latest
|
||||||
|
# Set automatically by `docker build --platform` (BuildKit); we use it to
|
||||||
|
# decide whether to install chrome-headless-shell, which only ships for
|
||||||
|
# linux64 (see https://googlechromelabs.github.io/chrome-for-testing/).
|
||||||
|
ARG TARGETARCH=amd64
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
ca-certificates curl unzip ffmpeg chromium \
|
ca-certificates curl unzip ffmpeg chromium \
|
||||||
@@ -16,16 +20,38 @@ ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
|||||||
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
||||||
ENV CONTAINER=true
|
ENV CONTAINER=true
|
||||||
|
|
||||||
RUN npx --yes @puppeteer/browsers install chrome-headless-shell@stable \
|
# chrome-headless-shell unlocks BeginFrame-based deterministic capture but the
|
||||||
--path /root/.cache/puppeteer
|
# project only publishes a linux64 binary. On arm64 we skip the install and
|
||||||
|
# let the engine fall back to system chromium (set via
|
||||||
|
# PUPPETEER_EXECUTABLE_PATH above). The wrapper script below only sets
|
||||||
|
# PRODUCER_HEADLESS_SHELL_PATH when the binary is actually present.
|
||||||
|
RUN if [ "$TARGETARCH" = "amd64" ]; then \
|
||||||
|
npx --yes @puppeteer/browsers install chrome-headless-shell@stable \
|
||||||
|
--path /root/.cache/puppeteer; \
|
||||||
|
else \
|
||||||
|
echo "Skipping chrome-headless-shell install on ${TARGETARCH} (linux64-only); using system chromium."; \
|
||||||
|
fi
|
||||||
|
|
||||||
RUN npm install -g hyperframes@${HYPERFRAMES_VERSION}
|
RUN npm install -g hyperframes@${HYPERFRAMES_VERSION}
|
||||||
|
|
||||||
# Wrapper script: resolves chrome-headless-shell path at build time,
|
# Wrapper script: resolves chrome-headless-shell path at build time when
|
||||||
# sets PRODUCER_HEADLESS_SHELL_PATH at runtime so the engine uses
|
# available so the engine uses BeginFrame rendering. On arm64 (no
|
||||||
# BeginFrame rendering instead of falling back to system Chromium.
|
# chrome-headless-shell) it leaves PRODUCER_HEADLESS_SHELL_PATH unset, so the
|
||||||
RUN SHELL_PATH=$(find /root/.cache/puppeteer/chrome-headless-shell -name "chrome-headless-shell" -type f | head -1) \
|
# engine falls back to PUPPETEER_EXECUTABLE_PATH (system chromium).
|
||||||
&& printf '#!/bin/sh\nexport PRODUCER_HEADLESS_SHELL_PATH=%s\nexec hyperframes render "$@"\n' "$SHELL_PATH" > /usr/local/bin/hf-render \
|
#
|
||||||
|
# If TARGETARCH=amd64 and the binary is missing, fail the build loudly — the
|
||||||
|
# previous (pre-PR) wrapper used an `&&` chain that crashed `docker build` in
|
||||||
|
# this case, and silently downgrading to system chromium on amd64 would mask
|
||||||
|
# golden-baseline regressions.
|
||||||
|
RUN SHELL_PATH=$(find /root/.cache/puppeteer/chrome-headless-shell -name "chrome-headless-shell" -type f 2>/dev/null | head -1); \
|
||||||
|
if [ -n "$SHELL_PATH" ]; then \
|
||||||
|
printf '#!/bin/sh\nexport PRODUCER_HEADLESS_SHELL_PATH=%s\nexec hyperframes render "$@"\n' "$SHELL_PATH" > /usr/local/bin/hf-render; \
|
||||||
|
elif [ "$TARGETARCH" = "amd64" ]; then \
|
||||||
|
echo "ERROR: chrome-headless-shell binary not found on amd64 — @puppeteer/browsers install must have failed or moved its cache layout." >&2; \
|
||||||
|
exit 1; \
|
||||||
|
else \
|
||||||
|
printf '#!/bin/sh\nexec hyperframes render "$@"\n' > /usr/local/bin/hf-render; \
|
||||||
|
fi \
|
||||||
&& chmod +x /usr/local/bin/hf-render
|
&& chmod +x /usr/local/bin/hf-render
|
||||||
|
|
||||||
WORKDIR /project
|
WORKDIR /project
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { buildDockerRunArgs, type DockerRenderOptions } from "./dockerRunArgs.js";
|
import {
|
||||||
|
buildDockerRunArgs,
|
||||||
|
resolveDockerPlatform,
|
||||||
|
type DockerRenderOptions,
|
||||||
|
} from "./dockerRunArgs.js";
|
||||||
|
|
||||||
const BASE: DockerRenderOptions = {
|
const BASE: DockerRenderOptions = {
|
||||||
fps: { num: 30, den: 1 },
|
fps: { num: 30, den: 1 },
|
||||||
@@ -18,6 +22,10 @@ const FIXED_INPUT = {
|
|||||||
projectDir: "/abs/proj",
|
projectDir: "/abs/proj",
|
||||||
outputDir: "/abs/out",
|
outputDir: "/abs/out",
|
||||||
outputFilename: "out.mp4",
|
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", () => {
|
describe("buildDockerRunArgs", () => {
|
||||||
@@ -290,4 +298,80 @@ describe("buildDockerRunArgs", () => {
|
|||||||
const args = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
|
const args = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
|
||||||
expect(args).not.toContain("--no-page-side-compositing");
|
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",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,6 +17,16 @@ export interface DockerRunArgsInput {
|
|||||||
outputDir: string;
|
outputDir: string;
|
||||||
/** Filename within `outputDir` (joined to /output inside the container). */
|
/** Filename within `outputDir` (joined to /output inside the container). */
|
||||||
outputFilename: string;
|
outputFilename: string;
|
||||||
|
/**
|
||||||
|
* Docker `--platform` value (`linux/amd64` or `linux/arm64`). When omitted,
|
||||||
|
* resolves to the host architecture via `resolveDockerPlatform()`. Pinning
|
||||||
|
* to `linux/amd64` on an arm64 host (the legacy default) forces qemu
|
||||||
|
* emulation of chrome-headless-shell, which segfaults or stalls on Apple
|
||||||
|
* Silicon — see issue #1193. Native `linux/arm64` falls back to the
|
||||||
|
* system chromium baked into the image at the cost of byte-for-byte
|
||||||
|
* parity with amd64 renders.
|
||||||
|
*/
|
||||||
|
platform?: string;
|
||||||
options: DockerRenderOptions;
|
options: DockerRenderOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,13 +54,39 @@ export interface DockerRenderOptions {
|
|||||||
pageSideCompositing?: boolean;
|
pageSideCompositing?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps Node's `process.arch` to a Docker `--platform` string. We only emit
|
||||||
|
* the two architectures the renderer actively supports — arm64 hosts (Apple
|
||||||
|
* Silicon, Graviton, Ampere) and everything else (treated as amd64).
|
||||||
|
*
|
||||||
|
* Honors `HYPERFRAMES_DOCKER_PLATFORM` as an escape hatch (typed loosely so
|
||||||
|
* the override can target future platforms without a CLI release):
|
||||||
|
*
|
||||||
|
* - Apple Silicon users running an x64 Node binary under Rosetta (where
|
||||||
|
* `process.arch === "x64"` despite the host being arm64) can set it to
|
||||||
|
* `linux/arm64` to avoid re-triggering issue #1193.
|
||||||
|
* - Maintainers regenerating amd64 golden baselines on an arm64 host can set
|
||||||
|
* it to `linux/amd64` to keep the byte-for-byte guarantee.
|
||||||
|
* - Users on remote daemons (`DOCKER_HOST=ssh://amd64-server`) can force the
|
||||||
|
* actual daemon arch instead of relying on local `process.arch`.
|
||||||
|
*/
|
||||||
|
export function resolveDockerPlatform(
|
||||||
|
arch: string = process.arch,
|
||||||
|
env: NodeJS.ProcessEnv = process.env,
|
||||||
|
): string {
|
||||||
|
const override = env.HYPERFRAMES_DOCKER_PLATFORM;
|
||||||
|
if (override && override.trim() !== "") return override.trim();
|
||||||
|
return arch === "arm64" ? "linux/arm64" : "linux/amd64";
|
||||||
|
}
|
||||||
|
|
||||||
export function buildDockerRunArgs(input: DockerRunArgsInput): string[] {
|
export function buildDockerRunArgs(input: DockerRunArgsInput): string[] {
|
||||||
const { imageTag, projectDir, outputDir, outputFilename, options } = input;
|
const { imageTag, projectDir, outputDir, outputFilename, options } = input;
|
||||||
|
const platform = input.platform ?? resolveDockerPlatform();
|
||||||
return [
|
return [
|
||||||
"run",
|
"run",
|
||||||
"--rm",
|
"--rm",
|
||||||
"--platform",
|
"--platform",
|
||||||
"linux/amd64",
|
platform,
|
||||||
"--shm-size=2g",
|
"--shm-size=2g",
|
||||||
// GPU encoding requires host GPU passthrough.
|
// GPU encoding requires host GPU passthrough.
|
||||||
...(options.gpu ? ["--gpus", "all"] : []),
|
...(options.gpu ? ["--gpus", "all"] : []),
|
||||||
|
|||||||
Reference in New Issue
Block a user