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:
| Object | Purpose |
|---|---|
cli/vVERSION/jerboa-darwin-arm64 | Native CLI (ad-hoc signed) |
daemon/vVERSION/jerboad-darwin-arm64 | Native daemon (ad-hoc signed) |
desktop/vVERSION/jerboa-desktop-VERSION-macos-arm64.dmg | Self-contained Desktop app with the complete runtime |
releases/vVERSION/manifest.json and .minisig | Signed metadata for pinned installations |
channels/stable.json and .minisig | Signed 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.
-
Freeze the sources once, before other work changes them. The snapshot contains tracked, modified and untracked non-ignored files of
jerboaandfirecracker-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.txtandtree-digest.txtidentify the exact content, not justHEAD:python3 scripts/build-macos-reproducible.py snapshot --output /path/to/snapshotExtraction 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.
-
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 byCargo.lock, and the native archives pinned bysources.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:
- C:
-ffile-prefix-map=<root>=/build;ZERO_AR_DATE=1;SOURCE_DATE_EPOCHis the frozen Jerboa commit time;TZ=UTC,LC_ALL=Cand a privateHOME. - Mach-O
LC_UUID: Apple ld hashes the output including the debug map’s object paths, and a laterstripdoes not recompute it. Host tools and Firecracker are therefore linked with-Wl,-S, then stripped withstrip -S. - Go:
-trimpath -buildvcs=false,GOTOOLCHAIN=local,GOENV=offand no root-specificCGO_*flags, because their values feed the Go build ID andLC_UUID. - Rust:
--remap-path-prefixfor the build root, the privateCARGO_HOMEand the source tree, one codegen unit and--locked --offline. - glib: installed with
DESTDIRunder the logical prefix/opt/jerboa-hvf-native. Only that constant appears in the libraries; the packager rewrites install names to@loader_path. - Kernel: the snapshot has no
.git, sogitversionreports the frozenHEADplus+snapshot.<tree digest>, and inert.git/index/.git/HEADplaceholders satisfykernel.mk’s prerequisites.
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.