Skip to content

build-zip stages host-platform ffmpeg, breaking Lambda when built from non-Linux #1057

@kiyeonjeon21

Description

@kiyeonjeon21

Describe the bug

scripts/build-zip.ts stages ffmpeg into the Lambda zip by copying node_modules/ffmpeg-static/ffmpeg verbatim:

// packages/aws-lambda/scripts/build-zip.ts:352-367
const ffmpegBinary = join(resolveModuleDir("ffmpeg-static"), "ffmpeg");
// ...
const ffmpegDest = join(binDir, "ffmpeg");
cpSync(ffmpegBinary, ffmpegDest);
chmodSync(ffmpegDest, 0o755);

ffmpeg-static materializes a single platform-selected binary at install time. By default that is the install host's platform/arch; it can be influenced with npm_config_platform / npm_config_arch, but build:zip currently does not assert that the installed binary matches Lambda's target. So a default install on macOS (Apple Silicon, in my case) can drop a Mach-O ARM64 ffmpeg into bin/ffmpeg, the zip uploads cleanly, and Lambda only blows up at the first ffmpeg invocation:

Error: Command failed: ffmpeg -version
/var/task/bin/ffmpeg: /var/task/bin/ffmpeg: cannot execute binary file

In the current distributed path, plan() calls ffmpeg -version after the browser probe and before writing the final plan result, so the observed failure can happen in the Plan state before any chunks are scheduled. Any later encode/assemble spawn would fail for the same binary-mismatch reason.

For contrast, ffprobe-static ships every platform under bin/<os>/<arch>/, and stageFfmpeg already picks the Linux variant explicitly (packages/aws-lambda/scripts/build-zip.ts:373), so the ffprobe side of the zip is correct regardless of host platform.

Steps to reproduce

  1. On a non-Linux-x64 host (macOS Apple Silicon, Linux arm64, etc.), run bun install from a fresh clone of hyperframes without target-platform overrides. ffmpeg-static's postinstall pulls the host-platform binary.
  2. bun run --cwd packages/aws-lambda build:zip.
  3. aws lambda update-function-code --zip-file fileb://packages/aws-lambda/dist/handler.zip to your render function (or hyperframes lambda deploy).
  4. Invoke a render whose Plan state reaches readFfmpegVersion() — the first ffmpeg -version call fails.

You can confirm the wrong arch landed in the zip without deploying:

unzip -p packages/aws-lambda/dist/handler.zip bin/ffmpeg | file -
# /dev/stdin: Mach-O 64-bit executable arm64

Expected behavior

build:zip either stages the linux/x64 ffmpeg regardless of host platform, or fails fast with a clear message when it can't.

Actual behavior

The zip builds and deploys silently. The failure only appears at runtime inside CloudWatch, with cannot execute binary file and no hint about the binary mismatch unless the operator happens to file bin/ffmpeg against the zip.

Root cause

ffmpeg-static@5.x installs one binary selected at install time. Its default selection follows the install host; npm_config_platform=linux npm_config_arch=x64 can force the target, but stageFfmpeg trusts whatever binary is present and does not verify it. The bug is therefore platform-of-install vs platform-of-Lambda-target mismatch.

Possible fixes

A few directions, ordered by blast radius — happy to defer to maintainer preference on which path makes sense for HyperFrames:

  1. Defensive ELF check inside stageFfmpeg (the route I took in fix(aws-lambda): fail build-zip when ffmpeg-static binary isn't Linux x86-64 #1058). Read the first 20 bytes, assert ELFCLASS64 + EM_X86_64 before cpSync, throw with the Docker / npm_config_platform workaround in the message when it fails. Zero dependency change, fails the build instead of shipping a broken zip.
  2. Swap ffmpeg-static for a package that ships every platform. @ffmpeg-installer/ffmpeg declares per-arch optionalDependencies (@ffmpeg-installer/linux-x64, etc.) — install all of them and read from @ffmpeg-installer/linux-x64/ffmpeg directly. Mirrors what ffprobe-static already does. Slightly larger node_modules, but the build script can pick the right binary on any host.
  3. Always re-fetch or rehydrate the linux/x64 binary at build time. Either via npm_config_platform=linux npm_config_arch=x64 install in an isolated build step, or by pulling a known-good static build into a cache directory the script reads from. More moving parts; would replace initial code #2's dependency story with a runtime-script story.

Option 1 is the smallest change that turns "silently broken zip" into "loud build failure," and I have it ready as a PR if it's directionally useful. Option 2 fixes the cross-platform story end-to-end; option 3 is overkill unless the dep change in #2 is a non-starter.

Environment

  • @hyperframes/aws-lambda@0.6.40
  • Build host: macOS / Apple Silicon arm64
  • Lambda runtime: nodejs22.x, x86_64
  • ffmpeg-static dependency range: ^5.2.0 (resolved 5.3.0 in my install)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions