macOS Firecracker distribution

The distribution is one relocatable ARM64 bundle containing Jerboa, its native tools and the complete Firecracker/HVF package. It targets Apple Silicon and macOS 26 or newer. Build it only after the release-candidate binaries from all workstreams are final; the example input paths below are review artifacts, not immutable release inputs.

CI and the public R2 release channel

The release.yml workflow builds a complete product candidate on explicit request. build-macos runs on the GitHub-hosted macos-26 Apple Silicon runner and is required before candidate assembly. It installs ARM64 ELF binutils with Homebrew, Go 1.27.2 and Rust 1.97.0. The Desktop and Firecracker commits are explicit immutable inputs recorded in the signed inventory. The default Firecracker revision is 6750fc374d4758a8181d75e5299e7137a110dd41; validate the fork API before selecting a different revision. Native dependencies are built from the fork’s SHA-256-locked sources and bundled with their licenses.

The job builds the kernel, native tools and Firecracker, runs the Go tests and installer/package regressions, then packages the relocatable bundle and verifies it in ad-hoc mode. It stages the same native build and Firecracker package into JerboaDesktop (checked out with the DESKTOP_REPO_TOKEN secret), builds the self-contained DMG and checks the packaged app: a valid seal, Firecracker’s intact Hypervisor entitlement, the bundled engine version, and --version of the CLI, daemon and Firecracker run from inside Contents/Resources.

GitHub-hosted macOS runners expose no Hypervisor.framework, so CI boots no VMs. Before promoting a candidate, run the Firecracker functional suites (test-firecracker-macos.py, test-firecracker-network.py, test-firecracker-stats.py and test-jerboa-snapshots.py) locally on macOS 26 against the actual candidate app, as described under Verification. scripts/release/native-macos.sh records the candidate run and app archive hash. The separate promote.yml workflow requires that evidence and a release review, verifies all platforms and only then updates stable and the website. See release operations.

Releases are not Developer ID signed or notarized yet. Every Mach-O keeps its ad-hoc signature and the DMG is unsigned, so Gatekeeper blocks the first launch of a downloaded copy until it is allowed in System Settings → Privacy & Security. No installer package is published, so the macOS path of install.sh rejects these releases. R2 receives:

ObjectPurpose
cli/vVERSION/jerboa-darwin-arm64Native CLI (ad-hoc signed)
daemon/vVERSION/jerboad-darwin-arm64Native daemon (ad-hoc signed)
desktop/vVERSION/jerboa-desktop-VERSION-macos-arm64.dmgSelf-contained Desktop app with the complete runtime
releases/vVERSION/manifest.json and .minisigSigned metadata for pinned installations
channels/stable.json and .minisigSigned current release metadata

The standalone CLI and daemon assets do not include Firecracker, its libraries or the kernel toolset; the DMG’s app does. The Windows installer is mirrored into the desktop component only when its channel already points at the release version. All versioned artifacts are uploaded before the stable channel is updated, and the post-publication check downloads and hashes every published artifact. A successful PR check alone does not publish to R2.

Development bundle

The packaging command stages bytes but never builds, signs, installs, submits or publishes them. It first verifies the Firecracker package’s own checksums, writes a complete bundle checksum inventory and creates a deterministic tar.gz:

epoch=$(git show -s --format=%ct HEAD)
python3 scripts/package-macos-distribution.py \
  --jerboa-dir dist/astra-pkg-arm64/review-fixes \
  --tools-dir dist/macos-arm64/tools \
  --firecracker-package ../firecracker-macos/experiments/hvf/build/astra-shared-network/review-fixes/package \
  --output-dir dist/macos-distribution/v0.0.0-dev \
  --version 0.0.0-dev \
  --source-date-epoch "$epoch"
python3 scripts/verify-macos-distribution.py \
  dist/macos-distribution/v0.0.0-dev/jerboa-0.0.0-dev-macos-arm64.tar.gz \
  --mode adhoc

The layout puts jerboad, Firecracker and tools/ together under bin/, which matches Jerboa’s relative lookup. Firecracker’s @loader_path/../lib references resolve to the bundled libraries. Its source archives, lockfile, manifest, licenses and upstream checksums remain under share/firecracker/.

Given identical input bytes, version, Git commit, script and SOURCE_DATE_EPOCH, staging and archive bytes are deterministic. This claim is limited to packaging: the example reuses prebuilt Jerboa/tools/Firecracker inputs. To claim that the compiled bytes are reproducible, use the source build below and keep its comparison output with the release report.

Reproducible source build

scripts/build-macos-reproducible.py compiles every ARM64 component from source instead of staging prebuilt bytes: jerboa/jerboad (Go), the native tools and kernel/boot images (kernel Makefiles), glib/libintl (Meson), and Firecracker/HVF (Cargo). It never signs with an identity, notarizes, installs or publishes.

  1. Freeze the sources once, before other work changes them. The snapshot contains tracked, modified and untracked non-ignored files of jerboa and firecracker-macos, the vendored kernel sources, the SHA-256-locked native source archives and the few ignored generated headers the build needs. Dirty changes are included on purpose; content-manifest.txt and tree-digest.txt identify the exact content, not just HEAD:

    python3 scripts/build-macos-reproducible.py snapshot --output /path/to/snapshot

    Extraction validates the complete symlink graph before writing, rejects duplicate paths and files below symlink parents, and requires an empty destination. Relative links confined to the extracted tree remain supported.

  2. Build twice, in two new directories outside any Git checkout. Each build extracts the snapshot, refuses an existing root and uses fresh Go, Cargo and Meson caches and target directories. Only authenticated dependency archives are read from shared locations: Go module zips (checked against go.sum), crate archives and the Git database pinned by Cargo.lock, and the native archives pinned by sources.json. No network is used:

    python3 scripts/build-macos-reproducible.py build --snapshot /path/to/snapshot \
      --work /private/tmp/jerboa-repro/a --version 0.0.0-dev
    python3 scripts/build-macos-reproducible.py build --snapshot /path/to/snapshot \
      --work /private/tmp/jerboa-repro/second-b --version 0.0.0-dev
    python3 scripts/build-macos-reproducible.py compare \
      /private/tmp/jerboa-repro/a /private/tmp/jerboa-repro/second-b --output comparison.json

scripts/macos-reproducible-lock.json pins the environment: Go 1.26.0, Rust 1.97.0, Apple clang/ld/strip/codesign from Command Line Tools, macOS SDK 26.5, aarch64-elf binutils 2.47, make/xxd/awk, the macOS build and SHA-256 hashes of those executables. build refuses a mismatch unless you pass --allow-toolchain-drift; compare never reports such a build as reproducible. Regenerate the lock only as a reviewed maintainer change (lock --output).

The recipe removes build-root and time dependence in these ways:

compare checks every file of the Jerboa, tools and Firecracker outputs and of the final bundle twice: the ad-hoc signed bytes (sha256) and, for Mach-O files, the bytes with the signature removed (unsigned_sha256). It also compares the archive hash. Pre-signature equality is what a later Developer ID signature would be applied to. Signed equality is only true of ad-hoc signatures made without a timestamp. A Developer ID signature with a secure timestamp will differ between runs by design.

bin/tools/x86 is not rebuilt: no x86_64 guest cross toolchain is pinned. Its three files are copied from the signed release artifacts fetched by scripts/fetch-x86-tools.go, and the lock verifies their hashes.

The claim is bit-for-bit reproducibility on this pinned macOS ARM64 environment only. Other Macs, other Command Line Tools or SDK versions, and full Xcode are unverified until the comparison is repeated there.

Verification

The ad-hoc verifier checks every checksum, Mach-O ARM64 architecture, strict code signature validity, exact Firecracker Hypervisor entitlement, absence of unexpected Jerboa entitlements, and all dynamic-library paths. It extracts to a new temporary directory and runs only --version for Jerboa, the daemon and Firecracker. It does not start or replace the managed daemon, boot a VM, install under /usr/local, or contact Apple.

Run the functional smoke against the relocated bundle as a separate acceptance step. The test creates its own short-lived HOME, daemon socket, image store, ports and VMs, then builds an ARM64 fixture and proves HTTP from the guest:

export PATH="$PWD/../firecracker-macos/experiments/hvf/build/guest-tools/go/bin:$PATH"
bundle="$PWD/dist/macos-distribution/v0.0.0-dev/jerboa-0.0.0-dev-macos-arm64"
JERBOA_TEST_BIN="$bundle/bin" \
JERBOA_FIRECRACKER_BIN="$bundle/bin/firecracker" \
python3 scripts/test-firecracker-macos.py

Developer ID and notarized release

Ad-hoc signing is suitable for local development evidence, not public release. A distributable release needs a trusted Developer ID Application identity for every Mach-O, a Developer ID Installer identity for the installer, Hardened Runtime, the Firecracker Hypervisor entitlement, successful Apple notarization, and a stapled ticket. Use a copy of the reviewed bundle:

scripts/sign-notarize-macos.sh \
  --input dist/macos-distribution/v1.2.3/jerboa-1.2.3-macos-arm64 \
  --output dist/macos-distribution/v1.2.3/jerboa-1.2.3-macos-arm64-signed \
  --application-identity 'Developer ID Application: Example Corp (TEAMID)' \
  --pkg dist/macos-distribution/v1.2.3/jerboa-1.2.3-macos-arm64.pkg \
  --installer-identity 'Developer ID Installer: Example Corp (TEAMID)'

That command signs locally and creates a signed installer, but does not contact Apple. Only an explicitly authorized run may add:

  --notarize --notary-profile jerboa-notary

This invokes notarytool submit --wait, staples and validates the ticket, then asks Gatekeeper to assess the installer. Never put Apple credentials in command arguments or the repository; create the named Keychain profile separately. The signing helper regenerates the bundle checksum inventory after changing the Mach-O bytes. It renames the embedded Firecracker manifest.json and SHA256SUMS with an upstream-adhoc suffix and adds SIGNING-NOTICE.json, so metadata for pre-signing bytes cannot be mistaken for the signed inventory. After notarization, verify both the signed bundle and the stapled installer (this deliberately fails for an unstapled or ad-hoc artifact):

python3 scripts/verify-macos-distribution.py \
  dist/macos-distribution/v1.2.3/jerboa-1.2.3-macos-arm64-signed \
  --mode release \
  --installer-pkg dist/macos-distribution/v1.2.3/jerboa-1.2.3-macos-arm64.pkg

Release verification expands the installer without installing it and compares every payload path, byte hash and file mode with the already verified signed bundle. Installer scripts and payload files outside /usr/local/libexec/jerboa are rejected, so an unrelated validly notarized package cannot satisfy release verification.

Also run the isolated Firecracker functional suites documented in Firecracker on macOS.

Before publishing, review the embedded Firecracker README and manifest. A notarized wrapper cannot turn an experimental dependency with deferred network or stability acceptance into a production-supported release. Preserve all license/source material and publish the final checksum file through an authenticated release channel.