# 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 ''; }