fbi-proxy: cross-compile tradeoffs

End-user perspective vs. infra cost · prepared 2026-05-18

TL;DR — Status quo (CI matrix on native runners) is already correct for fbi-proxy. Every push produces 6 binaries in ~5 min parallel, at $0 (public repo). End users get natively-built binaries with no glibc/macOS-version surprises. Local cross-compile is only worth setting up if you want sub-minute iteration on a non-host target — and even then, gh run download against the latest CI run is usually faster than re-running locally.

What end users get

Regardless of build method, the user runs bunx fbi-proxy which downloads the right binary for their host. The interesting question is: does the binary just work?

Concern Native-on-CI (current) cargo-zigbuild cross
All 6 targets shipped Yes Yes No — macOS missing1
Linux glibc compatibility floor ubuntu-latest libc (~2.39, May 2026) Pick any glibc version explicitly
--target x86_64-unknown-linux-gnu.2.17
Image-dependent (~2.31 by default)
macOS minimum version macos-latest SDK (macOS 14+) Configurable via MACOSX_DEPLOYMENT_TARGET N/A
Windows MSVC subtleties Native MSVC — zero surprises MinGW-style, not MSVC MinGW-style, not MSVC
Binary signing path Can codesign in CI (Apple/Windows) Local — manual signing Local — manual signing
Trust signal (provenance) Built on GitHub-hosted runners; SLSA-friendly Built on your machine Built on your machine

1 Apple's macOS SDK isn't redistributable, so Docker-based cross can't legally ship a macOS toolchain. cargo-zigbuild sidesteps this with Zig's Mach-O linker but you still need the macOS SDK headers on disk.

Infra cost

Cost Native-on-CI (current) cargo-zigbuild local cross local
Wall-clock per build (6 targets) ~5 min (parallel matrix) ~3-6 min (sequential, one host) ~6-10 min (Docker pull then build)
First-run setup $0 — already configured cargo install cargo-zigbuild
+ brew install zig or pip install ziglang (~50MB)
cargo install cross
+ Docker images (500MB-1GB total)
Per-build runner $ (private repo)2 ~$1.04 (2× Linux + 2× Windows 2× + 2× macOS 10×) $0 (local) $0 (local)
Per-build runner $ (public repo — this repo) $0 — unlimited minutes $0 $0
Iteration loop (incremental edit → binary) 5 min push → wait → download ~30s incremental ~1-2 min (Docker overhead)
Maintenance — toolchain drift GitHub-managed runners auto-update Zig releases ~monthly; pin via lockfile Docker images updated upstream
Disk footprint $0 local ~150 MB ~1.5 GB

2 Rough estimate using GitHub's per-minute pricing (May 2026): Linux $0.008/min, Windows $0.016/min, macOS $0.08/min × ~5 min each.

When each one makes sense

Stick with CI matrix (current) RECOMMENDED

Public repo · cuts no corners · provenance + signing path · zero local toolchain. The only downside is iteration time, and gh run download <run-id> closes that gap to one command.

Add cargo-zigbuild if…

You want to test a specific non-host target quickly without pushing to main — e.g., debugging a macOS-only segfault from a Linux dev box, or producing a glibc-2.17-compatible Linux binary for legacy servers. Only setup that lets a Linux host produce macOS binaries.

Add cross if…

You want a clean Docker-per-target sandbox, you don't need macOS locally, and you prefer one tool that handles linux+windows. Roughly equivalent in capability to direct cargo build --target=X with system toolchains installed, but no host-pollution.

Switch away from native CI runners if…

You move this repo to a private org and macOS minutes start showing up on the invoice. At that point, build linux + windows with cargo-zigbuild on a single Linux runner (saves ~75%), and keep one macOS runner for the two Apple targets. Don't optimize this prematurely.

Concrete recipes

gh run download — the no-install path

gh run list --workflow="Build and Release" --branch=main --limit=1 --json databaseId -q '.[0].databaseId' \
  | xargs -I{} gh run download {} --dir ./release

Drops all 6 binaries into release/. ~2 seconds of your time, zero local toolchain.

cargo-zigbuild — one-time setup

# Install
cargo install cargo-zigbuild
pip install ziglang   # or: brew install zig

# Add targets
rustup target add aarch64-unknown-linux-gnu
rustup target add x86_64-apple-darwin aarch64-apple-darwin
rustup target add x86_64-pc-windows-gnu

# Build (any target, no extra config)
cargo zigbuild --release --target aarch64-apple-darwin
cargo zigbuild --release --target x86_64-unknown-linux-gnu.2.17  # pin glibc

cross — one-time setup

# Install (Docker must be running)
cargo install cross --git https://github.com/cross-rs/cross

# Build
cross build --release --target aarch64-unknown-linux-gnu
cross build --release --target x86_64-pc-windows-gnu

Risk register

Recommendation

Don't change anything. Add this two-liner to package.json scripts so "download the latest CI build for my host" is one keystroke:

"scripts": {
  "dl-artifacts": "gh run list --workflow='Build and Release' --branch=main --limit=1 --json databaseId -q '.[0].databaseId' | xargs -I{} gh run download {} --dir release"
}

Revisit if (a) the repo goes private, (b) users hit glibc-floor issues, or (c) you want sub-minute iteration on a target you don't run natively.