Add a macOS image pipeline mirroring the Windows one: unattended install,
generalization and user creation, driven end to end in QEMU on a KVM host.
lib/images/macos:
- makeOpenCore: OSX-KVM OpenCore ESP with a config.plist rewritten per image —
SMBIOS model + serial/MLB (macserial) + UUID + ROM=en0 MAC (built-in NIC pinned
to PciRoot(0x0)/Pci(0x12,0x0)) for Apple ID / iMessage / App Store; boot disk;
OpenCore self-entry hidden. ident.nix derives MAC+UUID from a seed so the NixOS
module and CLI know the NIC MAC at eval time.
- makeImage: one QEMU session driven by vm-driver.py (QMP + screenshot settle
detection + OCR of the menu bar) — boots the recovery via OpenCore, opens
Terminal (Ctrl-F2 -> Utilities -> Terminal), types the bootstrap command;
vmix-install.sh erases the disk as APFS, lays down the host-extracted installer
app skeleton + a byte-exact SharedSupport.dmg (raw disk mapped to that byte range
of the pkg, dd'd in — Recovery's xar truncates an 18 GB member), runs
startosinstall with the vmix agent pkg. OpenCore is then copied into the image's
ESP so it boots standalone with OVMF.
- customizeImage / templates: boot the image with a FAT-then-HFS+ VMIX volume; the
vmix agent (LaunchDaemon) runs a script as root, records status and powers off —
the macOS counterpart of Windows Audit Mode. generalize creates the admin user +
auto-login (kcpassword), suppresses Setup Assistant, sets hostname/timezone,
grows APFS, and assigns a fresh SMBIOS identity. Templates: noUpdates,
performance, remoteAccess (ssh + screen sharing).
- fetchRecovery: Apple recovery BaseSystem, retried until the pinned Tahoe build
(osrecovery load-balances Sequoia/Tahoe during the rollout). makeAgentPkg builds
a distribution flat pkg on Linux (xar+bom+cpio) for startosinstall --installpackage.
CLI: vmix build/copy/run for macOS (run --macos --vnc, reads the image's MAC from
its ESP), and a `vmix macserial` helper. NixOS module: disks.os.file carrying
_vmixOsType="macos" auto-enables the macOS QEMU profile (AppleSMC+OSK, Skylake
CPU spoof, AHCI system disk, VMware SVGA, pinned NIC); macos.{enable,cpu,mac}.
Status: proven through the installer prepare phase (SharedSupport.dmg mounts,
version 26.6.2 read, SU catalog loads). Two blockers remain, documented in
lib/images/macos/README.md: (1) startosinstall's OSISVerifyBaseSystemOperation
rejects the byte-identical plain-UDIF SharedSupport as "pkgdmg missing a footer"
in this Tahoe recovery/VM; (2) Apple's CDN unreliably serves the Tahoe recovery
during rollout (self-hosting the verified BaseSystem is the robust fix).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XsESshRCoBoUVWV9qKURUF
99 lines
3.7 KiB
Python
99 lines
3.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Derive a VM config.plist from OSX-KVM's OpenCore config.
|
|
|
|
- keep only kexts/ACPI/drivers whose files exist in the ESP
|
|
- SMBIOS (model, serial, MLB, UUID, ROM=MAC) for Apple ID / iMessage
|
|
- mark the vmix NIC PCI path built-in, drop iMac19,1 GPU/audio properties
|
|
- boot-args, picker, resolution; optional JSON merged on top
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
import plistlib
|
|
|
|
APPLE_NVRAM = '7C436110-AB2A-4BBB-A880-FE41995C9F82'
|
|
MCE_KEXT = {
|
|
'Arch': 'Any', 'BundlePath': 'MCEReporterDisabler.kext',
|
|
'Comment': 'Fix kernel panic on MacPro/iMacPro SMBIOS (vmix)', 'Enabled': True,
|
|
'ExecutablePath': '', 'MaxKernel': '', 'MinKernel': '', 'PlistPath': 'Contents/Info.plist',
|
|
}
|
|
|
|
|
|
def merge(dst, src):
|
|
for k, v in src.items():
|
|
if isinstance(v, dict) and isinstance(dst.get(k), dict):
|
|
merge(dst[k], v)
|
|
else:
|
|
dst[k] = v
|
|
|
|
|
|
def main():
|
|
p = argparse.ArgumentParser()
|
|
p.add_argument('--base', required=True)
|
|
p.add_argument('--esp', required=True, help='directory containing EFI/OC')
|
|
p.add_argument('--out', required=True)
|
|
p.add_argument('--model', required=True)
|
|
p.add_argument('--serial', required=True)
|
|
p.add_argument('--mlb', required=True)
|
|
p.add_argument('--uuid', required=True)
|
|
p.add_argument('--mac', required=True)
|
|
p.add_argument('--nic-path', required=True)
|
|
p.add_argument('--boot-args', default='keepsyms=1')
|
|
p.add_argument('--resolution', default='1024x768')
|
|
p.add_argument('--show-picker', default='true')
|
|
p.add_argument('--timeout', type=int, default=2)
|
|
p.add_argument('--extra-json', default='{}')
|
|
a = p.parse_args()
|
|
|
|
with open(a.base, 'rb') as f:
|
|
cfg = plistlib.load(f)
|
|
oc = os.path.join(a.esp, 'EFI', 'OC')
|
|
|
|
def exists(sub, path):
|
|
return os.path.exists(os.path.join(oc, sub, path))
|
|
|
|
kexts = [k for k in cfg['Kernel']['Add'] if exists('Kexts', k['BundlePath'])]
|
|
if a.model.startswith(('MacPro', 'iMacPro')) and exists('Kexts', MCE_KEXT['BundlePath']) \
|
|
and not any(k['BundlePath'] == MCE_KEXT['BundlePath'] for k in kexts):
|
|
kexts.append(dict(MCE_KEXT))
|
|
cfg['Kernel']['Add'] = kexts
|
|
cfg['ACPI']['Add'] = [x for x in cfg['ACPI']['Add'] if exists('ACPI', x['Path'])]
|
|
cfg['UEFI']['Drivers'] = [d for d in cfg['UEFI']['Drivers'] if exists('Drivers', d['Path'])]
|
|
cfg['Misc']['Tools'] = [t for t in cfg['Misc'].get('Tools', []) if exists('Tools', t['Path'])]
|
|
cfg['Misc']['Entries'] = []
|
|
|
|
g = cfg['PlatformInfo']['Generic']
|
|
g['SystemProductName'] = a.model
|
|
g['SystemSerialNumber'] = a.serial
|
|
g['MLB'] = a.mlb
|
|
g['SystemUUID'] = a.uuid.upper()
|
|
g['ROM'] = bytes.fromhex(a.mac.replace(':', '').replace('-', ''))
|
|
g['SpoofVendor'] = True
|
|
g['AdviseFeatures'] = False
|
|
|
|
cfg['DeviceProperties']['Add'] = {a.nic_path: {'built-in': b'\x01'}}
|
|
cfg['DeviceProperties']['Delete'] = {}
|
|
|
|
nv = cfg['NVRAM']['Add'].setdefault(APPLE_NVRAM, {})
|
|
nv['boot-args'] = a.boot_args
|
|
nv['prev-lang:kbd'] = b'en-US:0'
|
|
nv['csr-active-config'] = b'\x00\x00\x00\x00'
|
|
|
|
cfg['UEFI']['Output']['Resolution'] = a.resolution
|
|
cfg['Misc']['Boot']['ShowPicker'] = a.show_picker.lower() == 'true'
|
|
cfg['Misc']['Boot']['Timeout'] = a.timeout
|
|
cfg['Misc']['Boot']['HideAuxiliary'] = True
|
|
cfg['Misc']['Security']['ScanPolicy'] = 0
|
|
cfg['Misc']['Security']['SecureBootModel'] = 'Disabled'
|
|
cfg['Misc']['Security']['AllowSetDefault'] = True
|
|
cfg['Misc']['Debug']['Target'] = 0
|
|
|
|
merge(cfg, json.loads(a.extra_json))
|
|
with open(a.out, 'wb') as f:
|
|
plistlib.dump(cfg, f, sort_keys=True)
|
|
print('kexts:', ', '.join(k['BundlePath'] for k in kexts))
|
|
print('acpi:', ', '.join(x['Path'] for x in cfg['ACPI']['Add']))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|