mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-15 13:58:20 +00:00
feat(aws-lambda): arm64 video function image from the handler ZIP
The image is the release handler ZIP with its x86 ffmpeg and Chrome swapped for the native ffmpeg tree and the sha256-pinned Sparticuz arm64 Chrome pack, so the x86 and arm64 functions run the same handler code. The image stage only copies, so any host builds it. CI builds it and runs ffmpeg, a real Chrome render and the Lambda runtime on an ARM runner.
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import { afterEach, describe, expect, it } from "bun:test";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { CHROMIUM_ARM64_PACK_SHA256, stageArm64Image } from "./stage-arm64-image.js";
|
||||
|
||||
const X86_64 = 0x3e;
|
||||
const AARCH64 = 0xb7;
|
||||
const dirs: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function elf(machine: number): Buffer {
|
||||
const head = Buffer.alloc(64);
|
||||
head.writeUInt32BE(0x7f454c46, 0);
|
||||
head[4] = 2;
|
||||
head.writeUInt16LE(machine, 18);
|
||||
return head;
|
||||
}
|
||||
|
||||
function tree(files: Record<string, string | Buffer>): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), "stage-arm64-"));
|
||||
dirs.push(dir);
|
||||
for (const [path, body] of Object.entries(files)) {
|
||||
mkdirSync(dirname(join(dir, path)), { recursive: true });
|
||||
writeFileSync(join(dir, path), body);
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
function pack(cmd: string, args: string[], cwd: string): void {
|
||||
expect(spawnSync(cmd, args, { cwd }).status).toBe(0);
|
||||
}
|
||||
|
||||
function fixture(opts: { ffmpegMachine?: number; zipExtra?: Record<string, string> } = {}) {
|
||||
const zipDir = tree({
|
||||
"handler.mjs": "export const handler = () => {};",
|
||||
"bin/ffmpeg": elf(X86_64),
|
||||
"bin/ffprobe": elf(X86_64),
|
||||
"node_modules/@sparticuz/chromium/package.json": JSON.stringify({ version: "148.0.0" }),
|
||||
"node_modules/@sparticuz/chromium/bin/chromium.br": "x64 chrome",
|
||||
...opts.zipExtra,
|
||||
});
|
||||
pack("zip", ["-qr", "handler.zip", "."], zipDir);
|
||||
const machine = opts.ffmpegMachine ?? AARCH64;
|
||||
const mediaDir = tree({
|
||||
"hf-media/bin/ffmpeg": elf(machine),
|
||||
"hf-media/bin/ffprobe": elf(machine),
|
||||
"hf-media/lib/libgnutls.so.30": "gnutls",
|
||||
});
|
||||
pack("tar", ["-czf", "hf-media.tar.gz", "hf-media"], mediaDir);
|
||||
const packDir = tree({ "chromium.br": "arm64 chrome", "al2023.tar.br": "arm64 libs" });
|
||||
pack("tar", ["-cf", "pack.tar", "chromium.br", "al2023.tar.br"], packDir);
|
||||
const packBytes = readFileSync(join(packDir, "pack.tar"));
|
||||
return {
|
||||
handlerZip: join(zipDir, "handler.zip"),
|
||||
ffmpegTarball: join(mediaDir, "hf-media.tar.gz"),
|
||||
outDir: join(tree({}), "out"),
|
||||
fetchPack: async () => packBytes,
|
||||
packPins: { "148.0.0": createHash("sha256").update(packBytes).digest("hex") },
|
||||
};
|
||||
}
|
||||
|
||||
describe("stageArm64Image", () => {
|
||||
it("swaps the handler ZIP's x86 ffmpeg and Chrome for the arm64 ones", async () => {
|
||||
const task = await stageArm64Image(fixture());
|
||||
expect(readFileSync(join(task, "bin/ffmpeg")).readUInt16LE(18)).toBe(AARCH64);
|
||||
expect(readFileSync(join(task, "bin/ffprobe")).readUInt16LE(18)).toBe(AARCH64);
|
||||
expect(readFileSync(join(task, "lib/libgnutls.so.30"), "utf-8")).toBe("gnutls");
|
||||
const chromeBin = join(task, "node_modules/@sparticuz/chromium/bin");
|
||||
expect(readFileSync(join(chromeBin, "chromium.br"), "utf-8")).toBe("arm64 chrome");
|
||||
expect(readFileSync(join(chromeBin, "al2023.tar.br"), "utf-8")).toBe("arm64 libs");
|
||||
expect(readFileSync(join(task, "handler.mjs"), "utf-8")).toContain("handler");
|
||||
});
|
||||
|
||||
it("refuses an ffmpeg tarball that is not arm64", async () => {
|
||||
await expect(stageArm64Image(fixture({ ffmpegMachine: X86_64 }))).rejects.toThrow(
|
||||
"is not a 64-bit arm64 ELF binary",
|
||||
);
|
||||
});
|
||||
|
||||
it("refuses a Chrome pack whose sha256 is not the pinned one", async () => {
|
||||
const opts = { ...fixture(), packPins: CHROMIUM_ARM64_PACK_SHA256 };
|
||||
await expect(stageArm64Image(opts)).rejects.toThrow("pinned c18a5241");
|
||||
});
|
||||
|
||||
it("refuses a @sparticuz/chromium version with no pinned arm64 pack", async () => {
|
||||
await expect(stageArm64Image({ ...fixture(), packPins: {} })).rejects.toThrow(
|
||||
"no arm64 Chrome pack pinned for @sparticuz/chromium 148.0.0",
|
||||
);
|
||||
});
|
||||
|
||||
it("refuses a handler ZIP built with chrome-headless-shell", async () => {
|
||||
const opts = fixture({ zipExtra: { "bin/chrome-headless-shell": "x86 chrome" } });
|
||||
await expect(stageArm64Image(opts)).rejects.toThrow("sparticuz Chrome only");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env bun
|
||||
// Stages /var/task for the arm64 video function image: the handler ZIP with its x86 ffmpeg and Chrome
|
||||
// swapped for the native ffmpeg tree (image/Dockerfile's artifact) and Sparticuz's arm64 Chrome pack.
|
||||
// bun packages/aws-lambda/scripts/stage-arm64-image.ts --ffmpeg=<hf-media tarball> [--zip=..] [--out=..]
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
closeSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
openSync,
|
||||
readFileSync,
|
||||
readSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
// The npm package ships x64 Chrome only. Bumping @sparticuz/chromium fails here until its arm64 pack is pinned.
|
||||
export const CHROMIUM_ARM64_PACK_SHA256: Record<string, string> = {
|
||||
"148.0.0": "c18a5241b0a460cba233fff3abccd6678ebdcf855b8548bb7c58240a477f5f0e",
|
||||
};
|
||||
|
||||
export interface StageOptions {
|
||||
handlerZip: string;
|
||||
ffmpegTarball: string;
|
||||
outDir: string;
|
||||
fetchPack?: (url: string) => Promise<Uint8Array>;
|
||||
packPins?: Record<string, string>;
|
||||
}
|
||||
|
||||
async function fetchBytes(url: string): Promise<Uint8Array> {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error(`[stage-arm64-image] GET ${url} answered ${res.status}`);
|
||||
return new Uint8Array(await res.arrayBuffer());
|
||||
}
|
||||
|
||||
function run(cmd: string, args: string[]): void {
|
||||
const result = spawnSync(cmd, args, { stdio: "inherit" });
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`[stage-arm64-image] ${cmd} ${args.join(" ")} exited ${result.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function assertArm64Elf(path: string): void {
|
||||
const head = Buffer.alloc(20);
|
||||
const fd = openSync(path, "r");
|
||||
try {
|
||||
readSync(fd, head, 0, head.length, 0);
|
||||
} finally {
|
||||
closeSync(fd);
|
||||
}
|
||||
const elf64 = head.readUInt32BE(0) === 0x7f454c46 && head[4] === 2;
|
||||
if (!elf64 || head.readUInt16LE(18) !== 0xb7) {
|
||||
throw new Error(`[stage-arm64-image] ${path} is not a 64-bit arm64 ELF binary`);
|
||||
}
|
||||
}
|
||||
|
||||
async function stageChromiumArm64(chromiumDir: string, opts: StageOptions) {
|
||||
const { version } = JSON.parse(readFileSync(join(chromiumDir, "package.json"), "utf-8")) as {
|
||||
version: string;
|
||||
};
|
||||
const pinned = (opts.packPins ?? CHROMIUM_ARM64_PACK_SHA256)[version];
|
||||
if (!pinned) {
|
||||
throw new Error(
|
||||
`[stage-arm64-image] no arm64 Chrome pack pinned for @sparticuz/chromium ${version}; ` +
|
||||
"add its sha256 to CHROMIUM_ARM64_PACK_SHA256",
|
||||
);
|
||||
}
|
||||
const url = `https://github.com/Sparticuz/chromium/releases/download/v${version}/chromium-v${version}-pack.arm64.tar`;
|
||||
const pack = await (opts.fetchPack ?? fetchBytes)(url);
|
||||
const actual = createHash("sha256").update(pack).digest("hex");
|
||||
if (actual !== pinned) {
|
||||
throw new Error(`[stage-arm64-image] ${url} has sha256 ${actual}, pinned ${pinned}`);
|
||||
}
|
||||
const tar = join(chromiumDir, "pack.arm64.tar");
|
||||
writeFileSync(tar, pack);
|
||||
run("tar", ["-xf", tar, "-C", join(chromiumDir, "bin")]);
|
||||
rmSync(tar);
|
||||
}
|
||||
|
||||
export async function stageArm64Image(opts: StageOptions): Promise<string> {
|
||||
const task = join(opts.outDir, "task");
|
||||
rmSync(opts.outDir, { recursive: true, force: true });
|
||||
mkdirSync(task, { recursive: true });
|
||||
run("unzip", ["-q", opts.handlerZip, "-d", task]);
|
||||
if (existsSync(join(task, "bin", "chrome-headless-shell"))) {
|
||||
throw new Error("[stage-arm64-image] the arm64 image bundles sparticuz Chrome only");
|
||||
}
|
||||
rmSync(join(task, "bin", "ffmpeg"));
|
||||
rmSync(join(task, "bin", "ffprobe"));
|
||||
run("tar", ["-xzf", opts.ffmpegTarball, "-C", task, "--strip-components=1"]);
|
||||
for (const bin of ["ffmpeg", "ffprobe"]) assertArm64Elf(join(task, "bin", bin));
|
||||
await stageChromiumArm64(join(task, "node_modules", "@sparticuz", "chromium"), opts);
|
||||
return task;
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
const distDir = resolve(dirname(fileURLToPath(import.meta.url)), "..", "dist");
|
||||
const flag = (name: string) =>
|
||||
process.argv.find((arg) => arg.startsWith(`--${name}=`))?.slice(name.length + 3);
|
||||
const ffmpegTarball = flag("ffmpeg");
|
||||
if (!ffmpegTarball)
|
||||
throw new Error("[stage-arm64-image] --ffmpeg=<hf-media tarball> is required");
|
||||
const task = await stageArm64Image({
|
||||
handlerZip: flag("zip") ?? join(distDir, "handler.zip"),
|
||||
ffmpegTarball,
|
||||
outDir: flag("out") ?? join(distDir, "arm64-image"),
|
||||
});
|
||||
console.log(`[stage-arm64-image] staged ${task}`);
|
||||
}
|
||||
Reference in New Issue
Block a user