Apple's built-in QEMU guest agent (AppleQEMUGuestAgent, launched by launchd when a virtio console port org.qemu.guest_agent.0 appears; guest-exec as root) is attached by vmix run --macos and the NixOS module. AppleVirtIO.kext on x86 Tahoe drives virtio-fs, block, console, input, net — verified in QEMU. - customizeImage: `bootScript` — online step through the guest agent (driver mode qga): boot the image, run the script as root with the VMIX volume, shut down through the agent. `as_user` runs commands in the logged-in session. - templates.software: pkg/app (offline in the PE), script/homebrew (online). - templates.profile.settings: widgets, wallpaper (pinned desktoppr — Apple Events need TCC consent that a headless session cannot give), dock apps, autohide, dark mode, hidden files. - generalize: persistHome (fstab LABEL=vmix-home /Users), hideWidgets offline. - formatVolume: formats a blank disk image as APFS by booting the PE (~35 s); idempotent. - NixOS module: macos.guestAgent (/run/vmix/qga-<name>.sock), shares via virtiofsd + vhost-user-fs (Apple automount tag for the first share, others mounted through the agent), macos.homeDisk (created + formatted on first start, virtio-blk), SPICE keeps -vga vmware for macOS. - CLI: vmix run --macos --share DIR --home FILE --qga PATH. - qemu.nix helpers; README section. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XsESshRCoBoUVWV9qKURUF
146 lines
7.3 KiB
Nix
146 lines
7.3 KiB
Nix
# Generalize a macOS image, offline from the PE: create the (admin) user on the
|
|
# image's Data volume with dscl, auto-login, suppress the first-login Setup
|
|
# Assistant, hostname, locale, timezone, use the whole disk, and give 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}",
|
|
# mount an APFS volume labelled vmix-home (a virtio-blk/AHCI disk the host
|
|
# provides, formatted by the PE on first start) at /Users: ephemeral OS disk,
|
|
# persistent home directories
|
|
persistHome ? false,
|
|
homeVolumeLabel ? "vmix-home",
|
|
# no desktop widgets for the created user (Sonoma+)
|
|
hideWidgets ? true,
|
|
# 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" "DidSeeUpdateMacAutomatically"
|
|
"DidSeeSoftwareUpdate" "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"''}
|
|
VER=$(pe_target_version); BUILD=$(pe_target_build)
|
|
echo "vmix: target macOS $VER ($BUILD)"
|
|
N="$DATA/private/var/db/dslocal/nodes/Default"
|
|
D() { dscl -f "$N" localhost "$@"; }
|
|
|
|
${lib.optionalString (!delayOobeRun) ''
|
|
# --- user account (admin), created directly in the local directory node
|
|
U="${username}"; HOME_DIR="$DATA/Users/$U"
|
|
if ! D -read "/Local/Default/Users/$U" >/dev/null 2>&1; then
|
|
UID_NEW=$(D -list /Local/Default/Users UniqueID | awk '$2 >= 501 && $2 < 1000 && $2 > m {m = $2} END {print (m ? m + 1 : 501)}')
|
|
D -create "/Local/Default/Users/$U" || pe_fail "dscl create user"
|
|
D -create "/Local/Default/Users/$U" UserShell /bin/zsh
|
|
D -create "/Local/Default/Users/$U" RealName ${lib.escapeShellArg fullName}
|
|
D -create "/Local/Default/Users/$U" UniqueID "$UID_NEW"
|
|
D -create "/Local/Default/Users/$U" PrimaryGroupID 20
|
|
D -create "/Local/Default/Users/$U" NFSHomeDirectory "/Users/$U"
|
|
if ! D -passwd "/Local/Default/Users/$U" ${lib.escapeShellArg password}; then
|
|
echo "vmix: WARNING: could not set the requested password, using '${tempPassword}'"
|
|
D -passwd "/Local/Default/Users/$U" "${tempPassword}" || pe_fail "dscl passwd"
|
|
fi
|
|
for g in admin _appserverusr _appserveradm _lpadmin; do
|
|
D -append "/Local/Default/Groups/$g" GroupMembership "$U" 2>/dev/null || true
|
|
done
|
|
mkdir -p "$HOME_DIR"
|
|
T="$SYS/System/Library/User Template/Non_localized"; [ -d "$T" ] || T="/System/Library/User Template/Non_localized"
|
|
ditto "$T" "$HOME_DIR" 2>/dev/null || true
|
|
L="$SYS/System/Library/User Template/English.lproj"; [ -d "$L" ] && ditto "$L" "$HOME_DIR" 2>/dev/null || true
|
|
else
|
|
UID_NEW=$(D -read "/Local/Default/Users/$U" UniqueID | awk '{print $2}')
|
|
fi
|
|
${lib.optionalString autoLogon ''
|
|
pe_plist_set "$DATA/Library/Preferences/com.apple.loginwindow.plist" autoLoginUser string "$U"
|
|
cp "$V/kcpassword" "$DATA/private/etc/kcpassword"
|
|
chmod 600 "$DATA/private/etc/kcpassword"; chown 0:0 "$DATA/private/etc/kcpassword"
|
|
''}
|
|
# --- no Setup Assistant / "What's new" prompts at first login
|
|
mkdir -p "$HOME_DIR/Library/Preferences"
|
|
P="$HOME_DIR/Library/Preferences/com.apple.SetupAssistant.plist"
|
|
for k in ${lib.concatStringsSep " " setupKeys}; do pe_plist_set "$P" "$k" bool true; done
|
|
pe_plist_set "$P" GestureMovieSeen string none
|
|
pe_plist_set "$P" LastSeenCloudProductVersion string "$VER"
|
|
pe_plist_set "$P" LastSeenBuddyBuildVersion string "$BUILD"
|
|
pe_plist_set "$P" LastSeenSiriProductVersion string "$VER"
|
|
pe_plist_set "$P" LastPreLoginTasksPerformedVersion string "$VER"
|
|
pe_plist_set "$P" LastPreLoginTasksPerformedBuild string "$BUILD"
|
|
pe_plist_set "$HOME_DIR/Library/Preferences/.GlobalPreferences.plist" AppleLocale string "${macLocale}"
|
|
${lib.optionalString hideWidgets ''
|
|
WM="$HOME_DIR/Library/Preferences/com.apple.WindowManager.plist"
|
|
pe_plist_set "$WM" StandardHideWidgets integer 1
|
|
pe_plist_set "$WM" StageManagerHideWidgets integer 1
|
|
''}
|
|
chown -R "$UID_NEW:20" "$HOME_DIR"
|
|
touch "$DATA/private/var/db/.AppleSetupDone"
|
|
''}
|
|
${lib.optionalString delayOobeRun ''
|
|
rm -f "$DATA/private/var/db/.AppleSetupDone"
|
|
''}
|
|
|
|
# --- machine identity
|
|
PF="$DATA/Library/Preferences/SystemConfiguration/preferences.plist"
|
|
mkdir -p "$(dirname "$PF")"
|
|
pe_plist_dict "$PF" System
|
|
pe_plist_dict "$PF" System.System
|
|
pe_plist_dict "$PF" System.Network
|
|
pe_plist_dict "$PF" System.Network.HostNames
|
|
pe_plist_set "$PF" System.System.ComputerName string "${hostname}"
|
|
pe_plist_set "$PF" System.System.HostName string "${hostname}"
|
|
pe_plist_set "$PF" System.Network.HostNames.LocalHostName string "${hostname}"
|
|
pe_plist_set "$DATA/Library/Preferences/.GlobalPreferences.plist" AppleLocale string "${macLocale}"
|
|
ln -sfn "/var/db/timezone/zoneinfo/${timezone}" "$DATA/private/etc/localtime"
|
|
pe_plist_set "$DATA/Library/Preferences/com.apple.timezone.auto.plist" Active bool false
|
|
|
|
# --- QEMU's USB keyboard (vendor 0x0627, product 0x0001) is unknown to macOS,
|
|
# which would open the Keyboard Setup Assistant at every login: declare it ANSI
|
|
KT="$DATA/Library/Preferences/com.apple.keyboardtype.plist"
|
|
pe_plist_dict "$KT" keyboardtype
|
|
pe_plist_set "$KT" keyboardtype.1-1575-0 integer 40
|
|
|
|
${lib.optionalString persistHome ''
|
|
# --- home directories on the host-provided persistent volume (fstab by label;
|
|
# diskarbitrationd mounts it at /Users when a volume named ${homeVolumeLabel} exists)
|
|
F="$DATA/private/etc/fstab"
|
|
grep -q "LABEL=${homeVolumeLabel}" "$F" 2>/dev/null || echo "LABEL=${homeVolumeLabel} /Users apfs rw 0 2" >> "$F"
|
|
chmod 644 "$F"; chown 0:0 "$F"
|
|
''}
|
|
|
|
# --- use the whole (possibly grown) disk
|
|
STORE=$(diskutil info "$SYS_ID" | sed -n 's/.*APFS Physical Store: *//p' | awk '{print $1}')
|
|
[ -n "$STORE" ] && diskutil apfs resizeContainer "$STORE" 0 || true
|
|
'';
|
|
}
|