Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions packages/aws-lambda/scripts/build-zip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,14 @@
import { spawnSync } from "node:child_process";
import {
chmodSync,
closeSync,
cpSync,
existsSync,
mkdirSync,
openSync,
readdirSync,
readFileSync,
readSync,
rmSync,
statSync,
writeFileSync,
Expand Down Expand Up @@ -349,19 +352,75 @@ function stageHyperframeRuntime(stagingDir: string): void {
console.log(`[build-zip] staged hyperframe.manifest.json + hyperframe.runtime.iife.js`);
}

// ELF header constants used by `assertLinuxX86_64Elf`. Header layout:
// bytes 0..3 magic `0x7F 'E' 'L' 'F'`, byte 4 EI_CLASS (`2` = ELFCLASS64),
// bytes 18..19 e_machine little-endian (`0x3E` = EM_X86_64).
const ELF_MAGIC = Buffer.from([0x7f, 0x45, 0x4c, 0x46]);
const ELF_CLASS_64 = 2;
const ELF_MACHINE_X86_64 = 0x3e;
const ELF_HEADER_BYTES = 20;

/**
* Verify the binary at `path` is a Linux x86-64 ELF executable, the only
* shape the Lambda runtime can exec. Throws with the canonical workaround
* (Docker `--platform=linux/amd64` or `npm_config_platform` /
* `npm_config_arch` overrides) so the build doesn't ship a silently
* broken zip when a non-Linux host's postinstall fetched the wrong arch.
*/
function assertLinuxX86_64Elf(path: string, label: string): void {
const head = readFileHead(path, ELF_HEADER_BYTES, label);
const isElf = head.subarray(0, 4).equals(ELF_MAGIC);
const isElf64 = head[4] === ELF_CLASS_64;
const machine = head.readUInt16LE(18);
if (isElf && isElf64 && machine === ELF_MACHINE_X86_64) return;

const magicHex = head.subarray(0, 4).toString("hex");
throw new Error(
`[build-zip] ${label} at ${path} is not a Linux x86-64 ELF executable ` +
`(magic=0x${magicHex}, ei_class=${head[4]}, e_machine=0x${machine.toString(16)}). ` +
`This usually means the deploy host's postinstall fetched a host-platform binary ` +
`(e.g. macOS arm64 ffmpeg) instead of the linux/x64 binary Lambda needs. ` +
`Re-run the build inside a linux/amd64 container, or pre-install with ` +
`\`npm_config_platform=linux npm_config_arch=x64\` so the package fetches the right binary.`,
);
}

function readFileHead(path: string, byteCount: number, label: string): Buffer {
const fd = openSync(path, "r");
try {
const buf = Buffer.alloc(byteCount);
const bytesRead = readSync(fd, buf, 0, byteCount, 0);
if (bytesRead < byteCount) {
throw new Error(
`[build-zip] ${label} at ${path} is too short — read ${bytesRead} of ${byteCount} bytes.`,
);
}
return buf;
} finally {
closeSync(fd);
}
}

function stageFfmpeg(stagingDir: string): void {
const binDir = join(stagingDir, "bin");
mkdirSync(binDir, { recursive: true });

// ffmpeg from `ffmpeg-static`. The package only ships the encoder
// binary; the audio pad/trim path also needs ffprobe, which comes
// from `ffprobe-static`.
//
// `ffmpeg-static`'s postinstall fetches a binary for the host platform
// (e.g. arm64 Mach-O on Apple Silicon macOS). Lambda runs Linux x86-64,
// so a build from a non-Linux host silently produces a zip that boots
// but fails at first ffmpeg invocation with `cannot execute binary file`.
// Verify the ELF header up front and bail with a clear message instead.
const ffmpegBinary = join(resolveModuleDir("ffmpeg-static"), "ffmpeg");
if (!existsSync(ffmpegBinary)) {
throw new Error(
`[build-zip] ffmpeg-static binary missing at ${ffmpegBinary}. Did postinstall run?`,
);
}
assertLinuxX86_64Elf(ffmpegBinary, "ffmpeg-static binary");
const ffmpegDest = join(binDir, "ffmpeg");
cpSync(ffmpegBinary, ffmpegDest);
chmodSync(ffmpegDest, 0o755);
Expand Down
Loading