DESIGN
Draft 1, 2026-07-23. The "ultimate challenge": a good macOS-based installer for bootc Asahi images (all TunaOS variants + Dakota + Bluefin).
The core idea: install a bootstrap, not an OSβ
Every existing asahi-installer distro (Fedora Asahi, Ubuntu Asahi, NixOS) ships one zip payload per OS variant β extracted raw onto the partitions from macOS. For a family with 10+ variants Γ desktops Γ streams, that's a combinatorial artifact explosion, always stale relative to the registries.
Instead: one bootstrap payload per architecture, ever.
macOS app βββΆ asahi-installer backend βββΆ writes:
β’ stub macOS + per-install ESP (m1n1 boot.bin, U-Boot, systemd-boot/GRUB)
β’ "bootsahi" bootstrap root (~1.5 GB): 16K asahi kernel, dracut-asahi,
NetworkManager, podman/bootc, fisherman + minimal UI
β’ install-config.json (chosen image ref, user, locale, LUKS choice, Wi-Fi)
β reboot (after the one unavoidable recoveryOS blessing step)
bootstrap boots βββΆ first-boot agent reads install-config.json
βββΆ bootc install to-filesystem / bootc switch --apply ghcr.io/tuna-os/<variant>:<tag>-arm64
βββΆ reboots into the real OS; bootstrap root is reclaimed
Why this wins:
- Catalog = an allowlist of harness-verified refs. The macOS app lists
variants from a small
catalog.json(generated in CI from registry-map.yaml, filtered through harness results per tuna-os/tunaOS#910). Every entry carries averifiedfield; the app only offers entries where it istrue. New variants/tags appear only after passing the Asahi golden-manifest harness β no unverified image is ever offered, because a user picking one would complete the whole recoveryOS blessing flow and end up with a Mac that does not boot (#41). - The heavy download happens in Linux, with bootc's resumable, layered, signed pulls β not as a giant zip over asahi-installer's plain HTTP.
- One artifact to maintain, test, and sign. The bootstrap is small enough to boot-test in QEMU on every build (it's just an aarch64 UEFI image).
- Serves TunaOS, Dakota, Bluefin, Bazzite β anyone with a bootc aarch64 image ref. This is the shared piece the whole ecosystem lacks.
Componentsβ
1. macOS app ("Bootsahi")β
- Wraps, never reimplements, the asahi-installer Python backend β APFS
live-resize, stub macOS creation, per-install ESP, machine-signed m1n1
stage-1 install, and Apple-firmware extraction to
<ESP>/vendorfware battle-tested and Apple-fragile. Fork it only to add a--jsonmachine interface (progress events + answers over stdio) for the GUI to drive. - SwiftUI shell (native disk pickers, notarization, no runtime deps; the audience is by definition on a Mac). Tauri acceptable fallback if team prefers web-stack.
- Flow: welcome β catalog (variant/desktop/stream picker, rendered from
catalog.json) β disk-space slider (APFS resize) β user + Wi-Fi + LUKS
options (written to install-config.json) β run backend with progress β
guided recoveryOS walkthrough (the one step no software can do: shut
down, hold power, select the new OS,
bputil-bless via the terminal dialog β illustrated, with a phone-scannable QR to continue instructions off-device while the Mac is rebooting). - Distribution: notarized DMG +
curl | shfallback that runs the TUI backend directly (keeps CLI parity for servers/CI).
2. Bootstrap image ("bootsahi-boot")β
- Built in CI from the leanest asahi-capable base we have β bonito-asahi minimal (near-term) or Dakota-asahi minimal (long-term, from-source pride).
- Contents: 16K asahi kernel + Apple DTBs, dracut-asahi (ESP firmware flow), NetworkManager + iwd, bootc + podman, greetd + a fisherman-driven first-boot UI (tuna-installer-* frontends already exist for the ISO path β reuse the contract in INSTALLER-FRONTENDS.md), speakersafetyd (safety even in bootstrap), sshd togglable for headless installs.
- First-boot agent: reads install-config.json from the ESP; if Wi-Fi creds
present, connects;
bootc install to-filesystemthe chosen ref into the prepared root partition (or switch-in-place); on failure drops to the fisherman UI instead of a black screen. Verifies cosign signatures before deploying. - The asahi-installer payload zip wraps: ESP tree (m1n1 boot.bin + U-Boot +
bootloader) + the bootstrap root filesystem image + installer metadata.
Produced by CI (adapt
make-asahi-installer-package.sh), uploaded to R2 (download.tunaos.org), referenced by ourinstaller_data.json.
3. Boot.bin lifecycle (shared engineering, already scoped)β
- The installed OS needs the update-m1n1-on-change systemd unit (the piece travier/Bazzite also need). Ship it in every asahi image; upstream to fedora-asahi-remix-atomic-desktops#2 / bootupd.
Constraints to design aroundβ
- M1/M2 only until Asahi supports M3+; the app must detect the SoC
generation from macOS (
sysctl hw.model/ IORegistry) and refuse politely with a link, not fail late. - macOS β₯ 13.5 host requirement (asahi-installer backend requirement).
- No external boot, no live ISO β this flow is the only path onto the hardware; polish is not optional.
- The recoveryOS blessing step cannot be automated β invest UX effort there; it's where every first-time Asahi user gets lost.
- Never redistribute Apple firmware: extraction happens on-device (backend handles it; the bootstrap's dracut module consumes it).
Milestonesβ
- D0 β (payload pipeline + QEMU boot-gate proven; R2 upload still
blocked on bucket credentials being added as repo secrets) CI job builds
bootsahi-boot(bonito-asahi minimal) + payload zip + installer_data.json to R2; manual install with stock asahi-installer TUI pointed at our URL; QEMU boot-gate the bootstrap on every build. - D1 β (done, #7) first-boot agent: install-config.json β unattended
bootc installof a chosen ref; headless/ssh path proven (this is also how the M1 Air test loop gets provisioned). Implemented ascomponents/bootsahi-agent, which hands fisherman the asahi-installer backend's already-partitioned disk viacustomMountsand runsbootc install to-filesystemβto-diskremains debug/QEMU-only (D0's own payload harness). - D2 β (implemented, unverified on real hardware)
--jsonmachine mode for asahi-installer: hanthor/asahi-installer@json-machine-mode (protocol doc). Structured stdio events/answers routed through the two chokepoints every TTY prompt already funnels through, so the ~80 existing call sites needed no changes and the default TTY path is provably byte-for-byte unchanged. 18 protocol-layer tests pass in CI; the full interactive flow (diskutil/bless/bputil) still needs a real Mac to verify. Not opened as an upstream PR β awaiting James's go-ahead per the standing "no upstream outreach" rule. - D3 β (skeleton scaffolded;
swift build/swift testCI-green on a GitHub macos-14 runner, not yet run on real hardware) SwiftUI app (macos-app/Bootsahi) driving the backend; notarized DMG; catalog.json generation in CI..github/workflows/ bootsahi-app-build.ymlgives real compiler feedback without needing a physical Mac β dispatch manually against any branch. Still needs actual hardware to prove the app launches, drives a real backend subprocess, and partitions a real disk. Biggest open design gap, documented in the app's README: no defined contract yet for how install-config.json gets written onto the ESP after the backend finishes partitioning. - D4 β polish: recoveryOS walkthrough UX, LUKS via systemd-repart options, Wi-Fi handoff from macOS (SystemConfiguration read of current SSID; never the password β user re-enters). Depends on D3's app shell; real UX/hardware verification still needs a Mac, though the CI build workflow removes that constraint for pure compile-correctness.
Open questions for Jamesβ
- Naming: "Bootsahi" is a placeholder; fisherman-adjacent naming preferred?
- Repo home: tuna-os/bootsahi (app + payload CI together, or split)?
- Does the bootstrap adopt fisherman as the first-boot agent directly, or a thin dedicated agent that calls bootc? (fisherman reuse keeps one installer brain across ISO and Asahi paths β my recommendation.)
Adopted RFC #6 Host-Driven Install Patternsβ
See issue #6 and UNIFIED-INSTALL-CONTRACT.md for full technical rationale.
-
Unified Install Contract (
install-config.json):- Single JSON contract shared between macOS host app and first-boot agent.
- Hashed account passwords ($6$ crypt format) supported via
chpasswd -e. - Wi-Fi credentials and LUKS passphrases are never stored in plaintext on the ESP.
- Partition device paths are resolved at runtime via PARTUUID/role (per ADR 0001) rather than hardcoded by the host app.
-
Drive-Mode E2E Testing (
TUNADIVE_E2E_DRIVE=1):- Framework-agnostic UI automation seam for the SwiftUI app without accessibility bus dependencies.
- Directive polling (
/tmp/tuna-dive-e2e-drive.json) and state snapshot reporting (/tmp/tuna-dive-e2e-drive-state.json).
-
macOS User Data Bridge (Architecture Direction):
- Reverses wootc's Linux-driven bind mount pattern due to Apple Silicon hardware encryption and lack of APFS driver in Linux.
- Extraction occurs natively on the macOS host app before rebooting.
- Reuses wootc's catalog, consent-tiering (
migration-selection.json), and non-secret identity extraction (wootc-identity).
-
Evidence & Quality Discipline:
- Green-only evidence requirement for published walkthroughs and screenshots.
- CI-enforced regression tests across payload, agent, and backend contracts.