macOS: guest agent, online templates, virtio-fs shares, persistent home volume
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
This commit is contained in:
parent
50ad8d521c
commit
0f9373263d
13 changed files with 600 additions and 19 deletions
|
|
@ -2,6 +2,9 @@
|
|||
rec {
|
||||
generalize = import ./generalize.nix { inherit pkgs lib; };
|
||||
|
||||
software = import ./software { inherit pkgs lib; };
|
||||
profile = import ./profile { inherit pkgs lib; };
|
||||
|
||||
essentials = {
|
||||
remoteAccess = import ./essentials/remote-access.nix { };
|
||||
noUpdates = import ./essentials/no-updates.nix { };
|
||||
|
|
|
|||
|
|
@ -22,6 +22,13 @@
|
|||
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,
|
||||
}:
|
||||
|
|
@ -92,6 +99,11 @@ in
|
|||
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"
|
||||
''}
|
||||
|
|
@ -119,6 +131,14 @@ in
|
|||
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
|
||||
|
|
|
|||
58
lib/images/macos/templates/profile/default.nix
Normal file
58
lib/images/macos/templates/profile/default.nix
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# User profile templates, applied on the booted image inside the logged-in
|
||||
# user's session through the guest agent (after generalize with autoLogon).
|
||||
# settings — { hideWidgets, wallpaper, dockApps, dockAutohide, showHiddenFiles }
|
||||
{ pkgs, lib, ... }:
|
||||
let
|
||||
# Apple Events (osascript → System Events) need per-app automation consent that a
|
||||
# headless session cannot grant; desktoppr sets the wallpaper through NSWorkspace
|
||||
# inside the user's session instead (scriptingosx/desktoppr, pinned).
|
||||
desktoppr = pkgs.fetchurl {
|
||||
url = "https://github.com/scriptingosx/desktoppr/releases/download/v0.5/desktoppr-0.5-218.pkg";
|
||||
hash = "sha256-HPtn1wI7xrx7HjyMz1yJGhutIodt938/vHfSeHfMe50=";
|
||||
};
|
||||
in
|
||||
rec {
|
||||
settings = {
|
||||
hideWidgets ? true, # no desktop widgets (Sonoma+)
|
||||
wallpaper ? null, # image file (drv/path) set as the desktop picture
|
||||
dockApps ? null, # list of app paths, e.g. [ "/System/Applications/Utilities/Terminal.app" ]; null = untouched
|
||||
dockAutohide ? false,
|
||||
showHiddenFiles ? false,
|
||||
darkMode ? null, # true/false/null
|
||||
}: {
|
||||
name = "profile";
|
||||
files = lib.optionals (wallpaper != null) [
|
||||
{ source = wallpaper; name = "wallpaper.${lib.last (lib.splitString "." (baseNameOf (toString wallpaper)))}"; }
|
||||
{ source = desktoppr; name = "desktoppr.pkg"; }
|
||||
];
|
||||
bootScript = ''
|
||||
[ -n "$CONSOLE_USER" ] || { echo "vmix: profile needs a logged-in user (generalize with autoLogon)"; exit 1; }
|
||||
H=$(dscl . -read "/Users/$CONSOLE_USER" NFSHomeDirectory | awk '{print $2}')
|
||||
D() { as_user defaults write "$@"; }
|
||||
${lib.optionalString hideWidgets ''
|
||||
D com.apple.WindowManager StandardHideWidgets -int 1
|
||||
D com.apple.WindowManager StageManagerHideWidgets -int 1
|
||||
D com.apple.widgets widgetAppearance -int 0
|
||||
''}
|
||||
${lib.optionalString (wallpaper != null) ''
|
||||
W="/Library/Desktop Pictures/vmix-wallpaper.${lib.last (lib.splitString "." (baseNameOf (toString wallpaper)))}"
|
||||
mkdir -p "/Library/Desktop Pictures"; cp "$V/wallpaper".* "$W"; chmod 644 "$W"
|
||||
installer -pkg "$V/desktoppr.pkg" -target / >/dev/null || echo "vmix: WARNING: desktoppr install failed"
|
||||
as_user /usr/local/bin/desktoppr "$W" || echo "vmix: WARNING: could not set the wallpaper"
|
||||
''}
|
||||
${lib.optionalString (dockApps != null) ''
|
||||
D com.apple.dock persistent-apps -array
|
||||
${lib.concatMapStringsSep "\n" (a: ''
|
||||
D com.apple.dock persistent-apps -array-add "<dict><key>tile-data</key><dict><key>file-data</key><dict><key>_CFURLString</key><string>${a}</string><key>_CFURLStringType</key><integer>0</integer></dict></dict></dict>"
|
||||
'') dockApps}
|
||||
''}
|
||||
${lib.optionalString dockAutohide ''D com.apple.dock autohide -bool true''}
|
||||
${lib.optionalString showHiddenFiles ''D com.apple.finder AppleShowAllFiles -bool true''}
|
||||
${lib.optionalString (darkMode != null) (if darkMode
|
||||
then ''D -g AppleInterfaceStyle Dark''
|
||||
else ''as_user defaults delete -g AppleInterfaceStyle 2>/dev/null || true'')}
|
||||
as_user killall Dock Finder WindowManager 2>/dev/null || true
|
||||
sleep 3
|
||||
'';
|
||||
};
|
||||
}
|
||||
51
lib/images/macos/templates/software/default.nix
Normal file
51
lib/images/macos/templates/software/default.nix
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# Software installation templates.
|
||||
# pkg — install a flat/distribution .pkg offline from the PE (`installer -target`)
|
||||
# app — copy an .app bundle (from a directory or zip) into /Applications offline
|
||||
# script — run a shell script as root on the booted image (network available)
|
||||
{ pkgs, lib, ... }:
|
||||
rec {
|
||||
pkg = { name, src, choices ? null }: {
|
||||
name = "pkg-${name}";
|
||||
files = [ { source = src; name = "${name}.pkg"; } ]
|
||||
++ lib.optional (choices != null) { source = choices; name = "${name}.choices.xml"; };
|
||||
script = ''
|
||||
echo "vmix: installing ${name}.pkg into $SYS"
|
||||
installer -verboseR -pkg "$V/${name}.pkg" -target "$SYS" \
|
||||
${lib.optionalString (choices != null) ''-applyChoiceChangesXML "$V/${name}.choices.xml"''} \
|
||||
|| pe_fail "installer ${name}.pkg"
|
||||
'';
|
||||
};
|
||||
|
||||
app = { name, src }: {
|
||||
name = "app-${name}";
|
||||
files = [ { source = src; name = "${name}.app"; } ];
|
||||
script = ''
|
||||
echo "vmix: copying ${name}.app to $DATA/Applications"
|
||||
mkdir -p "$DATA/Applications"
|
||||
rm -rf "$DATA/Applications/${name}.app"
|
||||
ditto "$V/${name}.app" "$DATA/Applications/${name}.app" || pe_fail "ditto ${name}.app"
|
||||
chown -R 0:80 "$DATA/Applications/${name}.app"
|
||||
xattr -dr com.apple.quarantine "$DATA/Applications/${name}.app" 2>/dev/null || true
|
||||
'';
|
||||
};
|
||||
|
||||
script = { name, script, files ? [], network ? true }: {
|
||||
name = "script-${name}";
|
||||
inherit files network;
|
||||
bootScript = script;
|
||||
};
|
||||
|
||||
# Homebrew (needs network; installs for the console user or the given user)
|
||||
homebrew = { user ? null, formulae ? [], casks ? [] }: {
|
||||
name = "homebrew";
|
||||
bootScript = ''
|
||||
U=${if user == null then "$CONSOLE_USER" else user}
|
||||
[ -n "$U" ] || { echo "vmix: no user to install Homebrew for"; exit 1; }
|
||||
launchctl asuser "$(id -u "$U")" sudo -u "$U" env NONINTERACTIVE=1 \
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" || exit 1
|
||||
B=/usr/local/bin/brew
|
||||
${lib.optionalString (formulae != []) ''launchctl asuser "$(id -u "$U")" sudo -u "$U" $B install ${lib.escapeShellArgs formulae} || exit 1''}
|
||||
${lib.optionalString (casks != []) ''launchctl asuser "$(id -u "$U")" sudo -u "$U" $B install --cask ${lib.escapeShellArgs casks} || exit 1''}
|
||||
'';
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue