The OSX-KVM ESP ships Lilu 1.6.8 / VirtualSMC 1.3.3 / WhateverGreen 1.6.7,
which disable themselves on macOS 26; Apple's SMC driver then runs on QEMU's
isa-applesmc stub and the restart path panics (SMCWDT smcWriteKey
kSMCBadCommand → nested panic after MACH Reboot). Overlay pinned current
releases (upstream.json opencore.kexts) and drop isa-applesmc: with the stub
present VirtualSMC steps aside ("multiple devices present"); alone it carries
the OSK and reboots work (PE restart test: 10 s, clean). RestrictEvents with
revpatch=memtab silences MacPro7,1's "Memory Modules Misconfigured".
- makeOpenCore: kext overlay + Kernel.Add entries for overlaid kexts,
--memory-mb (4 DIMMs), bootArgs default revpatch=memtab
- qemu.nix: deviceArgsFor { appleSmc } (default false); cli: --applesmc for
images built before this change
- vm-driver: reboot-death detection (reset 60 s after a guest reboot request
that never comes back), panics wait for XNU's own auto-reboot, debug dir
works across nixbld users
- generalize: QEMU USB keyboard declared ANSI (no Keyboard Setup Assistant)
- README: architecture, reliability handling, debugging
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XsESshRCoBoUVWV9qKURUF
128 lines
5.5 KiB
Python
128 lines
5.5 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='{}')
|
|
p.add_argument('--memory-mb', type=int, default=8192, help='VM RAM, described as 4 DIMMs')
|
|
p.add_argument('--add-kexts', default='', help='comma-separated kext names (without .kext) that need a Kernel.Add entry')
|
|
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
|
|
# kexts overlaid into the ESP that the OSX-KVM config does not list yet
|
|
# (RestrictEvents: silences MacPro7,1's "Memory Modules Misconfigured", revpatch=memtab)
|
|
listed = {k['BundlePath'] for k in cfg['Kernel']['Add']}
|
|
for kext in [k + '.kext' for k in a.add_kexts.split(',') if k]:
|
|
if kext not in listed:
|
|
name = kext[:-5]
|
|
cfg['Kernel']['Add'].append({
|
|
'Arch': 'Any', 'BundlePath': kext, 'Comment': f'{name} (vmix)', 'Enabled': True,
|
|
'ExecutablePath': f'Contents/MacOS/{name}', 'MaxKernel': '', 'MinKernel': '',
|
|
'PlistPath': 'Contents/Info.plist'})
|
|
print('Kernel.Add +', kext)
|
|
|
|
# MacPro7,1 firmware expects DIMMs in pairs (>= 4); with QEMU's single SMBIOS
|
|
# module macOS shows "Memory Modules Misconfigured" at every login. Describe
|
|
# the VM's RAM as four DDR4 modules instead.
|
|
if a.model.startswith('MacPro7'):
|
|
size = max(1024, a.memory_mb // 4)
|
|
cfg['PlatformInfo']['CustomMemory'] = True
|
|
cfg['PlatformInfo']['Memory'] = {
|
|
'DataWidth': 64, 'ErrorCorrection': 3, 'FormFactor': 9, 'MaxCapacity': 1536 * 1024 * 1024 * 1024,
|
|
'TotalWidth': 64, 'Type': 26, 'TypeDetail': 128,
|
|
'Devices': [{
|
|
'AssetTag': '', 'BankLocator': f'BANK {i}', 'DeviceLocator': f'DIMM{i + 1}',
|
|
'Manufacturer': 'Apple', 'PartNumber': f'VMIX{size}', 'SerialNumber': f'VMIX{i:04d}',
|
|
'Size': size, 'Speed': 2666,
|
|
} for i in range(4)],
|
|
}
|
|
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()
|