macOS Tahoe VM images (OpenCore/QEMU), Apple-ID compatible, VNC
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
This commit is contained in:
parent
6a62a649bd
commit
242e48a5fc
35 changed files with 1842 additions and 36 deletions
15
lib/images/macos/templates/default.nix
Normal file
15
lib/images/macos/templates/default.nix
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{ pkgs, lib, ... }:
|
||||
rec {
|
||||
generalize = import ./generalize.nix { inherit pkgs lib; };
|
||||
|
||||
essentials = {
|
||||
remoteAccess = import ./essentials/remote-access.nix { };
|
||||
noUpdates = import ./essentials/no-updates.nix { };
|
||||
performance = import ./essentials/performance.nix { };
|
||||
};
|
||||
|
||||
bundles = {
|
||||
basic = with essentials; [ noUpdates performance ];
|
||||
remote = with essentials; [ noUpdates performance remoteAccess ];
|
||||
};
|
||||
}
|
||||
15
lib/images/macos/templates/essentials/no-updates.nix
Normal file
15
lib/images/macos/templates/essentials/no-updates.nix
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Disable automatic macOS / App Store updates (an OTA update would also need
|
||||
# the RestrictEvents kext to work in a VM)
|
||||
{ ... }:
|
||||
{
|
||||
name = "no-updates";
|
||||
script = ''
|
||||
softwareupdate --schedule off || true
|
||||
defaults write /Library/Preferences/com.apple.SoftwareUpdate AutomaticCheckEnabled -bool false
|
||||
defaults write /Library/Preferences/com.apple.SoftwareUpdate AutomaticDownload -bool false
|
||||
defaults write /Library/Preferences/com.apple.SoftwareUpdate AutomaticallyInstallMacOSUpdates -bool false
|
||||
defaults write /Library/Preferences/com.apple.SoftwareUpdate ConfigDataInstall -bool false
|
||||
defaults write /Library/Preferences/com.apple.SoftwareUpdate CriticalUpdateInstall -bool false
|
||||
defaults write /Library/Preferences/com.apple.commerce AutoUpdate -bool false
|
||||
'';
|
||||
}
|
||||
11
lib/images/macos/templates/essentials/performance.nix
Normal file
11
lib/images/macos/templates/essentials/performance.nix
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Less background work in a VM: no Spotlight indexing, no Time Machine, no sleep
|
||||
{ ... }:
|
||||
{
|
||||
name = "performance";
|
||||
script = ''
|
||||
mdutil -a -i off || true
|
||||
tmutil disable || true
|
||||
pmset -a sleep 0 displaysleep 0 disksleep 0 hibernatemode 0 womp 0 || true
|
||||
defaults write /Library/Preferences/com.apple.loginwindow DisableScreenLockImmediate -bool true
|
||||
'';
|
||||
}
|
||||
11
lib/images/macos/templates/essentials/remote-access.nix
Normal file
11
lib/images/macos/templates/essentials/remote-access.nix
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Enable SSH (Remote Login) and Screen Sharing (VNC on 5900 inside the guest)
|
||||
{ ... }:
|
||||
{
|
||||
name = "remote-access";
|
||||
script = ''
|
||||
systemsetup -setremotelogin on >/dev/null 2>&1 || launchctl load -w /System/Library/LaunchDaemons/ssh.plist
|
||||
launchctl load -w /System/Library/LaunchDaemons/com.apple.screensharing.plist
|
||||
# allow all local users to screen share
|
||||
defaults write /var/db/launchd.db/com.apple.launchd/overrides.plist com.apple.screensharing -dict Disabled -bool false 2>/dev/null || true
|
||||
'';
|
||||
}
|
||||
103
lib/images/macos/templates/generalize.nix
Normal file
103
lib/images/macos/templates/generalize.nix
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
# Generalize a macOS image: create the user, auto-login, hostname, timezone,
|
||||
# suppress Setup Assistant prompts, then remove the vmix agent. Also gives the
|
||||
# image a fresh SMBIOS identity (serial/MLB from macserial, MAC + UUID from
|
||||
# `seed`) so every generalized VM looks like a distinct Mac to Apple ID/iMessage.
|
||||
# Usage: (templates.generalize { username = "User"; password = ""; hostname = "MAC"; })
|
||||
# delayOobeRun = true: no user, Setup Assistant runs on first real boot (like Windows OOBE)
|
||||
{ pkgs, lib, ... }:
|
||||
{
|
||||
username ? "User",
|
||||
password ? "",
|
||||
fullName ? username,
|
||||
autoLogon ? true,
|
||||
hostname ? "MAC-VM",
|
||||
locale ? "en-US",
|
||||
timezone ? "UTC",
|
||||
delayOobeRun ? false,
|
||||
# SMBIOS identity; anything unset is generated
|
||||
model ? null,
|
||||
serial ? null,
|
||||
mlb ? null,
|
||||
uuid ? null,
|
||||
mac ? null,
|
||||
seed ? "${hostname}-${username}",
|
||||
# accepted for CLI parity with Windows, not supported on macOS
|
||||
bgColor ? null,
|
||||
}:
|
||||
let
|
||||
kcpasswordFile = pkgs.runCommand "kcpassword" { nativeBuildInputs = [ pkgs.python3 ]; } ''
|
||||
python3 ${../guest/kcpassword.py} ${lib.escapeShellArg password} > $out
|
||||
'';
|
||||
macLocale = builtins.replaceStrings [ "-" ] [ "_" ] locale;
|
||||
tempPassword = "vmix-temp-password";
|
||||
setupKeys = [
|
||||
"DidSeeCloudSetup" "DidSeeSiriSetup" "DidSeePrivacy" "DidSeeTouchIDSetup" "DidSeeAppearanceSetup"
|
||||
"DidSeeScreenTime" "DidSeeAccessibility" "DidSeeTrueTonePrivacy" "DidSeeActivationLock"
|
||||
"DidSeeiCloudLoginForStorageServices" "DidSeeSyncSetup" "DidSeeSyncSetup2" "DidSeeAppleIDSyncSetup"
|
||||
"DidSeeApplePaySetup" "DidSeeIntelligence" "DidSeeLockdownMode" "DidSeeAppStore" "SkipFirstLoginOptimization"
|
||||
];
|
||||
in
|
||||
{
|
||||
name = if delayOobeRun then "generalize-delay-oobe" else "generalize";
|
||||
files = [ { source = kcpasswordFile; name = "kcpassword"; } ];
|
||||
smbios = { inherit seed; } // lib.filterAttrs (_: v: v != null) { inherit model serial mlb uuid mac; };
|
||||
script = ''
|
||||
set -x
|
||||
${lib.optionalString (bgColor != null) ''echo "vmix: bgColor is not supported on macOS, ignoring"''}
|
||||
|
||||
${lib.optionalString (!delayOobeRun) ''
|
||||
# --- user account (admin)
|
||||
if ! id "${username}" >/dev/null 2>&1; then
|
||||
sysadminctl -addUser "${username}" -fullName ${lib.escapeShellArg fullName} \
|
||||
-password ${lib.escapeShellArg (if password == "" then tempPassword else password)} \
|
||||
-admin -home "/Users/${username}" || exit 1
|
||||
${lib.optionalString (password == "") ''
|
||||
dscl . -passwd "/Users/${username}" "${tempPassword}" "" || echo "vmix: WARNING: could not set an empty password, password is '${tempPassword}'"
|
||||
''}
|
||||
fi
|
||||
${lib.optionalString autoLogon ''
|
||||
defaults write /Library/Preferences/com.apple.loginwindow autoLoginUser "${username}"
|
||||
cp /Volumes/VMIX/kcpassword /etc/kcpassword
|
||||
chmod 600 /etc/kcpassword
|
||||
chown root:wheel /etc/kcpassword
|
||||
''}
|
||||
# --- no Setup Assistant / "What's new" prompts at first login
|
||||
P="/Users/${username}/Library/Preferences/com.apple.SetupAssistant"
|
||||
VER=$(sw_vers -productVersion)
|
||||
BUILD=$(sw_vers -buildVersion)
|
||||
for k in ${lib.concatStringsSep " " setupKeys}; do
|
||||
defaults write "$P" "$k" -bool true
|
||||
done
|
||||
defaults write "$P" GestureMovieSeen none
|
||||
defaults write "$P" LastSeenCloudProductVersion "$VER"
|
||||
defaults write "$P" LastSeenBuddyBuildVersion "$BUILD"
|
||||
defaults write "$P" LastSeenSiriProductVersion "$VER"
|
||||
defaults write "$P" LastPreLoginTasksPerformedVersion "$VER"
|
||||
defaults write "/Users/${username}/Library/Preferences/.GlobalPreferences" AppleLocale "${macLocale}"
|
||||
chown -R "${username}" "/Users/${username}/Library/Preferences"
|
||||
''}
|
||||
|
||||
# --- machine identity
|
||||
scutil --set ComputerName "${hostname}"
|
||||
scutil --set HostName "${hostname}"
|
||||
scutil --set LocalHostName "${hostname}"
|
||||
defaults write /Library/Preferences/.GlobalPreferences AppleLocale "${macLocale}"
|
||||
systemsetup -settimezone "${timezone}" >/dev/null 2>&1 || ln -sfn "/usr/share/zoneinfo/${timezone}" /etc/localtime
|
||||
|
||||
# --- never sleep (VM)
|
||||
pmset -a sleep 0 displaysleep 0 disksleep 0 hibernatemode 0 || true
|
||||
|
||||
# --- use the whole (possibly grown) disk
|
||||
STORE=$(diskutil info / | awk '/APFS Physical Store/ {print $NF}')
|
||||
[ -n "$STORE" ] && diskutil apfs resizeContainer "$STORE" 0 || true
|
||||
|
||||
${lib.optionalString delayOobeRun ''
|
||||
# Setup Assistant will run on the next boot
|
||||
rm -f /var/db/.AppleSetupDone
|
||||
''}
|
||||
|
||||
# --- the agent's job is done: remove it (this is the last vmix step)
|
||||
rm -f /Library/LaunchDaemons/ch.vmix.agent.plist
|
||||
rm -rf /Library/vmix
|
||||
'';
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue