# 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

- glibc version creep. Right now Linux binaries built on `ubuntu-latest` require the runner's current glibc on user machines. If users on older distros report "GLIBC_2.X not found", switch the Linux x64 job to `cargo-zigbuild` with a pinned floor (e.g. `.2.17` matches CentOS 7 / RHEL 7).
- macOS code signing. Unsigned macOS binaries trigger Gatekeeper warnings. The current setup leaves room to add codesigning + notarization steps in CI (would need Apple Developer ID secrets in repo settings) — local cross-compile can't notarize without uploading to Apple's service anyway.
- Windows arm64 maturity. Native MSVC arm64 is the right call (which current CI does); MinGW arm64 from `cross`/`zigbuild` is less battle-tested.

## 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.