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
|
|
@ -107,6 +107,42 @@ let
|
|||
(unique (map pciDeviceOf vmCfg.pci.passthrough));
|
||||
|
||||
|
||||
# --- macOS: guest agent, virtio-fs shares, persistent home volume
|
||||
macosQemu = vmixLib.macos.qemu;
|
||||
qgaSock = "/run/vmix/qga-${vmCfg.name}.sock";
|
||||
macosGuestAgent = isMacos && vmCfg.macos.guestAgent.enable;
|
||||
macosShares = if isMacos then vmCfg.shares else {};
|
||||
macosShareNames = attrNames macosShares;
|
||||
macosAutomountShare = if macosShares ? automount then "automount"
|
||||
else if macosShareNames != [] then head macosShareNames else null;
|
||||
macosShareTag = n: if n == macosAutomountShare then macosQemu.automountTag else n;
|
||||
macosShareSock = n: "/run/vmix/vfs-${vmCfg.name}-${n}.sock";
|
||||
macosHome = isMacos && vmCfg.macos.homeDisk.enable;
|
||||
macosFormatHome = if macosHome then vmixLib.macos.formatVolume {
|
||||
image = vmCfg.disks.os.file; label = vmCfg.macos.homeDisk.label;
|
||||
} else null;
|
||||
# shares beyond the automounted one are mounted through the guest agent once it answers
|
||||
macosMountSharesScript = pkgs.writeShellScript "${vmCfg.name}-macos-shares-vmix" ''
|
||||
for i in $(seq 1 120); do
|
||||
[ -S ${qgaSock} ] && printf '{"execute":"guest-ping"}\n' | ${pkgs.socat}/bin/socat -T5 - UNIX-CONNECT:${qgaSock} 2>/dev/null | grep -q return && break
|
||||
sleep 5
|
||||
done
|
||||
${concatMapStrings (n: optionalString (n != macosAutomountShare) ''
|
||||
printf '%s\n' '{"execute":"guest-exec","arguments":{"path":"/bin/bash","arg":["-c","mkdir -p ${macosShares.${n}.target}; mount -t virtiofs ${n} ${macosShares.${n}.target}"]}}' \
|
||||
| ${pkgs.socat}/bin/socat -T10 - UNIX-CONNECT:${qgaSock} >/dev/null 2>&1 || true
|
||||
'') macosShareNames}
|
||||
'';
|
||||
seedHomeDiskScript = pkgs.writeShellScript "${vmCfg.name}-home-disk-vmix" ''
|
||||
F="${vmCfg.macos.homeDisk.file}"
|
||||
if [ ! -e "$F" ]; then
|
||||
echo "Creating persistent home volume $F (${vmCfg.macos.homeDisk.size}, ${vmCfg.macos.homeDisk.format})..."
|
||||
mkdir -p "$(dirname "$F")"
|
||||
${pkgs.qemu}/bin/qemu-img create -q -f ${vmCfg.macos.homeDisk.format} "$F" ${vmCfg.macos.homeDisk.size}
|
||||
chmod 600 "$F"
|
||||
${macosFormatHome} "$F" ${vmCfg.macos.homeDisk.format}
|
||||
fi
|
||||
'';
|
||||
|
||||
# Linux VMs: apply customizeImage with 9p fstab and machine-id setup
|
||||
linuxOsImage = vmixLib.linux.customizeImage vmCfg.disks.os.file {
|
||||
name = vmCfg.name;
|
||||
|
|
@ -140,7 +176,8 @@ let
|
|||
fi
|
||||
'';
|
||||
|
||||
persistExecStartPre = lib.optional (hasOsDisk && vmCfg.disks.os.persist) seedPersistentDiskScript;
|
||||
persistExecStartPre = lib.optional (hasOsDisk && vmCfg.disks.os.persist) seedPersistentDiskScript
|
||||
++ lib.optional macosHome seedHomeDiskScript;
|
||||
|
||||
# QEMU expects single-letter boot codes (e.g. c,d,n), while vmix uses readable names.
|
||||
bootOrderQemu =
|
||||
|
|
@ -175,6 +212,16 @@ let
|
|||
);
|
||||
|
||||
qemuStartVMScript = pkgs.writeShellScript "${vmCfg.name}-qemu-vmix" ''
|
||||
${optionalString (isMacos && macosShareNames != []) ''
|
||||
mkdir -p /run/vmix
|
||||
${concatMapStrings (n: ''
|
||||
rm -f ${macosShareSock n}
|
||||
${pkgs.virtiofsd}/bin/virtiofsd --socket-path=${macosShareSock n} --shared-dir ${toString macosShares.${n}.source} --cache auto --sandbox none &
|
||||
'') macosShareNames}
|
||||
for i in $(seq 1 50); do ${concatMapStringsSep " && " (n: "[ -S ${macosShareSock n} ]") macosShareNames} && break; sleep 0.2; done
|
||||
${optionalString macosGuestAgent "${macosMountSharesScript} &"}
|
||||
''}
|
||||
${optionalString macosGuestAgent "mkdir -p /run/vmix; rm -f ${qgaSock}"}
|
||||
${optionalString vmCfg.vnc.enable ''
|
||||
${optionalString (vmCfg.vnc.passwordFile != null) ''
|
||||
if [ ! -r ${escapeShellArg vmCfg.vnc.passwordFile} ]; then
|
||||
|
|
@ -208,7 +255,7 @@ let
|
|||
${optionalString vmCfg.vnc.enable "-vnc ${vncArgs}"} \
|
||||
${optionalString (vmCfg.spice.enable && vmCfg.spice.passwordFile != null) "-object secret,id=spice-pass-${vmCfg.name},file=${escapeShellArg vmCfg.spice.passwordFile}"} \
|
||||
${optionalString vmCfg.spice.enable "-spice addr=${vmCfg.spice.addr},port=${toString vmCfg.spice.port}${optionalString (vmCfg.spice.passwordFile == null) ",disable-ticketing=on"}${optionalString (vmCfg.spice.passwordFile != null) ",password-secret=spice-pass-${vmCfg.name}"}"} \
|
||||
${optionalString vmCfg.spice.enable (if vmCfg.spice.displayDevice == "qxl" && vmCfg.spice.vgamem != null then "-vga none -device qxl-vga,vgamem_mb=${toString vmCfg.spice.vgamem}" else "-vga ${vmCfg.spice.displayDevice}")} \
|
||||
${optionalString (vmCfg.spice.enable && !isMacos) (if vmCfg.spice.displayDevice == "qxl" && vmCfg.spice.vgamem != null then "-vga none -device qxl-vga,vgamem_mb=${toString vmCfg.spice.vgamem}" else "-vga ${vmCfg.spice.displayDevice}")} \
|
||||
${optionalString (vmCfg.spice.enable && vmCfg.spice.agent.enable) "-device virtio-serial-pci -chardev spicevmc,id=vdagent,debug=0,name=vdagent -device virtserialport,chardev=vdagent,name=com.redhat.spice.0"} \
|
||||
${# Guest agent channel — prevents qemu-ga from spinning when virtio-win guest tools are installed
|
||||
optionalString isWindows "${optionalString (!vmCfg.spice.enable || !vmCfg.spice.agent.enable) "-device virtio-serial-pci"} -chardev socket,path=/tmp/qga-${vmCfg.name}.sock,server=on,wait=off,id=qga0 -device virtserialport,chardev=qga0,name=org.qemu.guest_agent.0"} \
|
||||
|
|
@ -228,9 +275,14 @@ let
|
|||
-device qemu-xhci -device usb-tablet \
|
||||
-global ICH9-LMB.disable_s3=1 -global ICH9-LMB.disable_s4=1 \
|
||||
''} \
|
||||
${# macOS: AppleSMC + OSK, USB keyboard/tablet, AHCI system disk, VMware SVGA (no SPICE display device)
|
||||
${# macOS: VirtualSMC, USB keyboard/tablet, AHCI system disk, VMware SVGA (also under SPICE),
|
||||
# Apple's guest agent, virtio-fs shares (shared memory backend), virtio-blk home volume
|
||||
optionalString isMacos ''
|
||||
${vmixLib.macos.qemu.deviceArgs} ${optionalString (!vmCfg.spice.enable) vmixLib.macos.qemu.vgaArgs} \
|
||||
${macosQemu.deviceArgs} ${if vmCfg.spice.enable && vmCfg.spice.displayDevice == "std" then "-vga std" else macosQemu.vgaArgs} \
|
||||
${optionalString macosGuestAgent (macosQemu.guestAgentArgs qgaSock)} \
|
||||
${optionalString (macosShareNames != []) (macosQemu.memBackendArgs vmCfg.mem.size)} \
|
||||
${concatMapStrings (n: "${macosQemu.virtioFsArgs { tag = macosShareTag n; sock = macosShareSock n; id = n; }} \\\n ") macosShareNames} \
|
||||
${optionalString macosHome (macosQemu.virtioBlkArgs { id = "home"; file = vmCfg.macos.homeDisk.file; format = vmCfg.macos.homeDisk.format; })} \
|
||||
''} \
|
||||
${optionalString hasOsDisk (if isMacos
|
||||
then "-drive id=os,if=none,file=${osDiskPath},format=qcow2${optionalString (vmCfg.disks.os.persist == false) ",snapshot=on"} -device ide-hd,bus=sata.0,drive=os"
|
||||
|
|
@ -239,9 +291,9 @@ let
|
|||
${concatMapStrings (diskCfg: ''
|
||||
-drive file=${toString diskCfg.file},format=${diskCfg.format},if=${vmCfg.disks.bus} \
|
||||
'') (attrValues vmCfg.disks.add)} \
|
||||
${concatStrings (mapAttrsToList (shareName: shareCfg: ''
|
||||
${optionalString (!isMacos) (concatStrings (mapAttrsToList (shareName: shareCfg: ''
|
||||
-virtfs local,path=${toString shareCfg.source},security_model=passthrough,mount_tag=${shareName} \
|
||||
'') vmCfg.shares)} \
|
||||
'') vmCfg.shares))} \
|
||||
${optionalString cfg.networks.user.enable "
|
||||
-netdev user,id=user \
|
||||
-device ${vmCfg.nicModel},netdev=user${optionalString isMacos ",mac=${macosMac}${macosNicPlacement}"} \
|
||||
|
|
@ -280,6 +332,8 @@ let
|
|||
ProtectSystem = true;
|
||||
ProtectHome = true;
|
||||
PrivateNetwork = true;
|
||||
RuntimeDirectory = "vmix";
|
||||
RuntimeDirectoryPreserve = "yes";
|
||||
} // lib.optionalAttrs (vmCfg.pci.passthrough != []) {
|
||||
# VFIO passthrough needs raw device access — relax sandboxing
|
||||
ProtectSystem = lib.mkForce false;
|
||||
|
|
|
|||
|
|
@ -93,9 +93,9 @@ with lib;
|
|||
};
|
||||
};
|
||||
displayDevice = mkOption {
|
||||
type = types.enum [ "virtio" "qxl" "std" "none" ];
|
||||
type = types.enum [ "virtio" "qxl" "std" "vmware" "none" ];
|
||||
default = "qxl";
|
||||
description = "QEMU -vga type to use with SPICE (qxl, virtio, std, none).";
|
||||
description = "QEMU -vga type to use with SPICE (qxl, virtio, std, vmware, none). macOS has no QXL/virtio-gpu driver: it always uses vmware (or std).";
|
||||
};
|
||||
vgamem = mkOption {
|
||||
type = types.nullOr types.int;
|
||||
|
|
@ -212,11 +212,11 @@ with lib;
|
|||
};
|
||||
target = mkOption {
|
||||
type = types.str;
|
||||
description = "Target path inside the VM for the shared directory.";
|
||||
description = "Target path inside the VM for the shared directory. macOS: the share named `automount` (or the first one) appears at /Volumes/My Shared Files; others are mounted at target through the guest agent.";
|
||||
};
|
||||
};
|
||||
});
|
||||
description = "Shared directories.";
|
||||
description = "Shared directories (9p for Linux, virtio-fs via virtiofsd for macOS).";
|
||||
};
|
||||
|
||||
disks.bus = mkOption {
|
||||
|
|
@ -265,6 +265,38 @@ with lib;
|
|||
default = null;
|
||||
description = "MAC address of en0. Defaults to the image's macAddress (must match OpenCore's ROM for Apple ID / iMessage).";
|
||||
};
|
||||
guestAgent.enable = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Attach Apple's built-in QEMU guest agent (virtio console port org.qemu.guest_agent.0). Socket: /run/vmix/qga-<name>.sock; guest-exec runs as root.";
|
||||
};
|
||||
homeDisk = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Persistent home volume: a host disk image attached as virtio-blk, formatted APFS with label `label` by the PE on first start. The image must be generalized with persistHome = true (fstab mounts it at /Users), which makes the OS disk safely ephemeral (disks.os.persist = false).";
|
||||
};
|
||||
file = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = "Path of the home disk image, e.g. /storage/vms/mac/home.qcow2 (created if missing).";
|
||||
};
|
||||
format = mkOption {
|
||||
type = types.enum [ "qcow2" "raw" ];
|
||||
default = "qcow2";
|
||||
description = "Image format; use raw for a zvol/block device (created only for files).";
|
||||
};
|
||||
size = mkOption {
|
||||
type = types.str;
|
||||
default = "64G";
|
||||
description = "Size when the image is created.";
|
||||
};
|
||||
label = mkOption {
|
||||
type = types.str;
|
||||
default = "vmix-home";
|
||||
description = "APFS volume label (must match generalize's homeVolumeLabel).";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
tpm = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue