macOS Tahoe: working fully-offline unattended install
The base image (macos.images.tahoe.upstream) now installs and boots end to end with no dependency on Apple's servers — proven on the KVM host: the finished qcow2 boots standalone (OpenCore from its own ESP) to the macOS 26.6.2 loginwindow. Install driving (vm-driver.py, screenshot + OCR over QMP): - map the whole InstallAssistant.pkg as a raw disk and dd it byte-exact into the app as SharedSupport.dmg (it is a pkgdmg: xar + koly footer — the bare xar member fails startosinstall with "pkgdmg is missing a footer") - offline install: no NIC + /etc/hosts blackhole of Apple install/verify endpoints so startosinstall's calls fail fast instead of hanging (SecureBootModel=Disabled allows the sealed-volume install offline) - keep the recovery display awake with a tiny mouse jiggle; coarse settle fingerprint so the cursor is not seen as a change - guest watchdog re-erases/retries a startosinstall attempt that stalls or runs too long (prepare is intermittently slow) - disk-aware boot watchdog: QMP system_reset only when the screen is dark AND the disk is idle (never interrupts a slow-but-working boot); recovery-restart if a post-prepare reboot lands back on recovery - detect the bright loginwindow and power the VM down (install complete); a black + disk-idle screen is treated as a completed halt - copy OpenCore into the image ESP so it boots standalone recovery.file pins a content-addressed local BaseSystem.dmg (Apple's CDN load-balances Sequoia/Tahoe during the rollout). fetchRecovery retries to the pinned hash when used instead. Not yet done: .generalize (user creation) — the first-boot agent LaunchDaemon is blocked by Ventura+ Background Task Management on headless boots; next step is offline user injection from the agent pkg postinstall. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XsESshRCoBoUVWV9qKURUF
This commit is contained in:
parent
242e48a5fc
commit
58a317f5d2
9 changed files with 415 additions and 128 deletions
|
|
@ -86,49 +86,53 @@ is used as for Windows.
|
|||
* Software updates inside the VM are disabled by the `noUpdates` template
|
||||
(OTA updates in a VM need the RestrictEvents kext).
|
||||
|
||||
## Current status (2026-09-08)
|
||||
## Current status (2026-09-09): working offline install
|
||||
|
||||
Everything up to and including the macOS Installer's *prepare* phase is working and
|
||||
proven end to end on the `root@daku.home` KVM host:
|
||||
`macos.images.tahoe.upstream` builds a bootable, installed macOS Tahoe 26.6.2
|
||||
qcow2 **fully offline** on the KVM host — no dependency on Apple's servers at build
|
||||
time, just the pinned local `InstallAssistant.pkg` and `BaseSystem.dmg`. The
|
||||
finished image boots standalone (OpenCore from its own ESP) to the macOS
|
||||
loginwindow. Serial/MLB/UUID/ROM are per-image for Apple ID / iMessage.
|
||||
|
||||
* OpenCore ESP per-image SMBIOS (serial/MLB via macserial, ROM=MAC, UUID), boot
|
||||
disk, `.contentVisibility` to hide the OC self-entry — **works** (`ocvalidate` clean).
|
||||
* Tahoe recovery boots via OpenCore; `vm-driver.py` drives it entirely by
|
||||
screenshots + OCR: handles the OpenCore picker, opens Terminal (Ctrl-F2 →
|
||||
Utilities → Terminal), types the bootstrap command — **works**.
|
||||
* HFS+ `VMIX` volume mounts in Recovery; the install script erases the disk as
|
||||
APFS "Macintosh HD", lays down the host-extracted `Install macOS Tahoe.app`
|
||||
skeleton and `dd`s a **byte-exact** `SharedSupport.dmg` from a raw disk mapped
|
||||
to that byte range of the pkg (verified `sha256` identical to Apple's) — **works**.
|
||||
* `startosinstall` runs, `osinstallersetupd` **mounts SharedSupport.dmg**, reads
|
||||
the MobileAsset bundle (`IA OS Version: 26.6.2, Build 25G83`), loads the
|
||||
641-product SU catalog from swscan.apple.com (so guest networking works), and
|
||||
logs `Machine is VM, will assume APFS is supported`.
|
||||
How the install is driven (`vm-driver.py`, all by screenshot + OCR over QMP):
|
||||
|
||||
### Blocker 1 — `OSISVerifyBaseSystemOperation: pkgdmg is missing a footer`
|
||||
* The whole `InstallAssistant.pkg` is mapped as a raw disk (it is a "pkgdmg":
|
||||
xar + koly footer) and `dd`'d byte-exact into the app as `SharedSupport.dmg` —
|
||||
extracting the bare xar member fails startosinstall with "pkgdmg missing a footer".
|
||||
* No NIC during install + `/etc/hosts` blackhole of Apple's install/verify
|
||||
endpoints, so `startosinstall`'s network calls fail fast instead of hanging —
|
||||
offline prepare, no external dependency. `SecureBootModel=Disabled` lets the
|
||||
sealed volume install without online personalization.
|
||||
* The recovery display is kept awake with a tiny mouse jiggle (a lone keypress
|
||||
does not reset display sleep, and the sleeping display swallows the menu-nav
|
||||
keystrokes); the settle detector uses a coarse fingerprint so the jiggling
|
||||
cursor is not seen as a screen change.
|
||||
* startosinstall prepare is intermittently slow/stalls; a guest watchdog kills and
|
||||
re-erases/retries an attempt that stalls or runs > 9 min.
|
||||
* First boot in QEMU intermittently hangs at the Apple logo; a disk-aware watchdog
|
||||
(`--progress-file`) issues a QMP `system_reset` only when the screen is dark AND
|
||||
the disk is idle, so a slow-but-working boot is never interrupted.
|
||||
* The install reaching the (bright) loginwindow is detected by brightness (the
|
||||
faint gray "password" text does not OCR) and the driver powers the VM down —
|
||||
the image is installed. macOS `shutdown -h now` halts to black without an ACPI
|
||||
power-off, so a black+disk-idle screen is also treated as a completed halt.
|
||||
* OpenCore is then copied into the image's own ESP so it boots standalone with OVMF.
|
||||
|
||||
After mounting the dmg, `osinstallersetupd` runs a verify step that treats
|
||||
`SharedSupport.dmg` as a *pkgdmg* (`Getting offset for dmg in pkg`) and fails
|
||||
`pkgdmg is missing a footer` → `Installation cannot proceed because the installer
|
||||
is damaged` (Code 255). But this InstallAssistant `SharedSupport.dmg` is a **plain
|
||||
UDIF** image (first bytes `eb 58 90 …`, not `xar!`), and it is **byte-identical to
|
||||
Apple's** — so this is not truncation or corruption (earlier truncation, from
|
||||
Recovery's `xar` mishandling an 18 GB member, was fixed by the byte-range `dd`).
|
||||
It is a macOS-internal verification that rejects the plain-UDIF SharedSupport
|
||||
when running Tahoe's `startosinstall` in this recovery/VM. Network is fine (the
|
||||
catalog loaded), so it is not the firewall case commonly cited for this error.
|
||||
### Recovery source
|
||||
|
||||
Leads not yet tried: driving the Tahoe recovery's **network "Reinstall macOS"**
|
||||
(GUI, downloads assets at install time — sidesteps the local-dmg verify); a
|
||||
different SMBIOS/board; or a newer OpenCore/kext combo. This is cutting-edge
|
||||
(Tahoe shipped 2025-09) and the hackintosh community is still working it out.
|
||||
`recovery.file` in `upstream.json` points at a content-addressed store path for the
|
||||
verified Tahoe `BaseSystem.dmg` (Apple's CDN load-balances Sequoia/Tahoe during the
|
||||
rollout, so a plain fetch is non-deterministic). Reproduce it on any host with
|
||||
`nix store add-path --name macos-tahoe-BaseSystem.dmg BaseSystem.dmg` (same path
|
||||
from the same bytes). Set `recovery.sha256` and remove `recovery.file` to fetch it
|
||||
from Apple instead (subject to the CDN rollout).
|
||||
|
||||
### Blocker 2 — Tahoe recovery availability on Apple's CDN
|
||||
### Not yet done: generalize / user creation
|
||||
|
||||
`osrecovery.apple.com` is load-balancing `latest` across CDN nodes during the
|
||||
Tahoe rollout: most requests return the **Sequoia** 15.4.1 BaseSystem
|
||||
(`082-33203`), some return **Tahoe** 26.6.2 (`140-93589`). `fetchRecovery`
|
||||
retries until it gets the pinned Tahoe hash, but that can exhaust its attempts
|
||||
when Tahoe is rare. The robust fix is to self-host the verified Tahoe
|
||||
`BaseSystem.dmg` (960530321 bytes, `sha256 edddd0d5…`, confirmed 26.6.2) the way
|
||||
the Win10 ISO is hosted on git.sagar.ch, and point `fetchRecovery` at it.
|
||||
The base image installs and boots to loginwindow. `.generalize` (user creation,
|
||||
auto-login, hostname) relies on the vmix agent LaunchDaemon running on first boot,
|
||||
but macOS Ventura+ Background Task Management does not auto-run a headless
|
||||
third-party daemon, and neither the pkg `launchctl bootstrap` (installer domain
|
||||
only) nor a cron `@reboot` reliably triggered it. The robust next step is to inject
|
||||
the user record + settings offline from the agent pkg's postinstall (which runs as
|
||||
root on the target during install), instead of a first-boot daemon.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue