diff --git a/.gitignore b/.gitignore index 9afbe9fa888..7a72a8bf8d0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ lima.REJECTED.yaml default-template.yaml schema-limayaml.json .config +.DS_Store diff --git a/pkg/driver/vz/spice_darwin.go b/pkg/driver/vz/spice_darwin.go new file mode 100644 index 00000000000..2dc31bbfa1e --- /dev/null +++ b/pkg/driver/vz/spice_darwin.go @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +//go:build darwin + +package vz + +import ( + "github.com/Code-Hex/vz/v3" + "github.com/sirupsen/logrus" + + "github.com/lima-vm/lima/v2/pkg/limatype" +) + +// attachSpiceAgent configures SPICE agent for clipboard sharing. +// This enables bidirectional clipboard sharing between host and guest. +// SPICE agent requires a display to be useful. +func attachSpiceAgent(inst *limatype.Instance, vmConfig *vz.VirtualMachineConfiguration) error { + // SPICE agent only makes sense with a display + if inst.Config.Video.Display == nil || *inst.Config.Video.Display == "none" { + return nil + } + + // Check clipboard configuration + // Default: enabled when display is present + // Respect explicit user configuration (true or false) + if inst.Config.Video.Clipboard != nil && !*inst.Config.Video.Clipboard { + logrus.Debug("Clipboard sharing explicitly disabled in configuration") + return nil + } + + // Get the SPICE agent port name + portName, err := vz.SpiceAgentPortAttachmentName() + if err != nil { + logrus.Warnf("Failed to get SPICE agent port name: %v", err) + return nil // Not fatal, clipboard just won't work + } + + // Create SPICE agent port attachment + spiceAgent, err := vz.NewSpiceAgentPortAttachment() + if err != nil { + logrus.Warnf("Failed to create SPICE agent: %v", err) + return nil // Not fatal, clipboard just won't work + } + + // Enable clipboard sharing + spiceAgent.SetSharesClipboard(true) + + // Create virtio console device if not already configured + consoleDevice, err := vz.NewVirtioConsoleDeviceConfiguration() + if err != nil { + logrus.Warnf("Failed to create console device for SPICE: %v", err) + return nil + } + + // Create console port configuration with SPICE agent + portConfig, err := vz.NewVirtioConsolePortConfiguration( + vz.WithVirtioConsolePortConfigurationName(portName), + vz.WithVirtioConsolePortConfigurationAttachment(spiceAgent), + ) + if err != nil { + logrus.Warnf("Failed to create SPICE agent port configuration: %v", err) + return nil + } + + // Attach the port to the console device + // Port 0 is typically the first console port + consoleDevice.SetVirtioConsolePortConfiguration(0, portConfig) + + // Set the console device in the VM configuration + vmConfig.SetConsoleDevicesVirtualMachineConfiguration([]vz.ConsoleDeviceConfiguration{ + consoleDevice, + }) + + logrus.Info("SPICE agent configured for clipboard sharing") + return nil +} diff --git a/pkg/driver/vz/vm_darwin.go b/pkg/driver/vz/vm_darwin.go index 397c3d5f3d2..cd5e5f3ca59 100644 --- a/pkg/driver/vz/vm_darwin.go +++ b/pkg/driver/vz/vm_darwin.go @@ -211,6 +211,11 @@ func createVM(ctx context.Context, inst *limatype.Instance) (*vz.VirtualMachine, return nil, err } + // Attach SPICE agent for clipboard sharing (requires macOS 13+) + if err = attachSpiceAgent(inst, vmConfig); err != nil { + return nil, err + } + if err = attachFolderMounts(inst, vmConfig); err != nil { return nil, err } @@ -551,14 +556,28 @@ func attachDisks(ctx context.Context, inst *limatype.Instance, vmConfig *vz.Virt func attachDisplay(inst *limatype.Instance, vmConfig *vz.VirtualMachineConfiguration) error { switch *inst.Config.Video.Display { case "vz", "default": + width := 1920 + height := 1200 + + if inst.Config.Video.VZ.Width != nil { + width = *inst.Config.Video.VZ.Width + } + if inst.Config.Video.VZ.Height != nil { + height = *inst.Config.Video.VZ.Height + } + if inst.Config.Video.VZ.PixelsPerInch != nil { + logrus.Warnf("video.vz.pixelsPerInch is not yet supported by Apple Virtualization.framework") + } + graphicsDeviceConfiguration, err := vz.NewVirtioGraphicsDeviceConfiguration() if err != nil { return err } - scanoutConfiguration, err := vz.NewVirtioGraphicsScanoutConfiguration(1920, 1200) + scanoutConfiguration, err := vz.NewVirtioGraphicsScanoutConfiguration(int64(width), int64(height)) if err != nil { return err } + graphicsDeviceConfiguration.SetScanouts(scanoutConfiguration) vmConfig.SetGraphicsDevicesVirtualMachineConfiguration([]vz.GraphicsDeviceConfiguration{ @@ -626,18 +645,49 @@ func attachFolderMounts(inst *limatype.Instance, vmConfig *vz.VirtualMachineConf func attachAudio(inst *limatype.Instance, config *vz.VirtualMachineConfiguration) error { switch *inst.Config.Audio.Device { case "vz", "default": - outputStream, err := vz.NewVirtioSoundDeviceHostOutputStreamConfiguration() - if err != nil { - return err + // Check what's enabled (default: output only, no input/microphone) + inputEnabled := false + outputEnabled := true + + if inst.Config.Audio.VZ.InputEnabled != nil { + inputEnabled = *inst.Config.Audio.VZ.InputEnabled } + if inst.Config.Audio.VZ.OutputEnabled != nil { + outputEnabled = *inst.Config.Audio.VZ.OutputEnabled + } + soundDeviceConfiguration, err := vz.NewVirtioSoundDeviceConfiguration() if err != nil { return err } - soundDeviceConfiguration.SetStreams(outputStream) - config.SetAudioDevicesVirtualMachineConfiguration([]vz.AudioDeviceConfiguration{ - soundDeviceConfiguration, - }) + + var streams []vz.VirtioSoundDeviceStreamConfiguration + + if outputEnabled { + outputStream, err := vz.NewVirtioSoundDeviceHostOutputStreamConfiguration() + if err != nil { + return err + } + streams = append(streams, outputStream) + logrus.Debug("VZ audio output enabled") + } + + if inputEnabled { + inputStream, err := vz.NewVirtioSoundDeviceHostInputStreamConfiguration() + if err != nil { + return err + } + streams = append(streams, inputStream) + logrus.Info("VZ audio input (microphone) enabled") + } + + if len(streams) > 0 { + soundDeviceConfiguration.SetStreams(streams...) + config.SetAudioDevicesVirtualMachineConfiguration([]vz.AudioDeviceConfiguration{ + soundDeviceConfiguration, + }) + } + return nil case "", "none": return nil diff --git a/pkg/driver/vz/vz_driver_darwin.go b/pkg/driver/vz/vz_driver_darwin.go index 566ebe602e1..5772935cbbc 100644 --- a/pkg/driver/vz/vz_driver_darwin.go +++ b/pkg/driver/vz/vz_driver_darwin.go @@ -352,40 +352,76 @@ func (l *LimaVzDriver) canRunGUI() bool { } func (l *LimaVzDriver) RunGUI() error { - if l.canRunGUI() { - return l.machine.StartGraphicApplication(1920, 1200) + if !l.canRunGUI() { + return fmt.Errorf("RunGUI is not supported for the given driver '%s' and display '%s'", "vz", *l.Instance.Config.Video.Display) } - return fmt.Errorf("RunGUI is not supported for the given driver '%s' and display '%s'", "vz", *l.Instance.Config.Video.Display) + + if l.machine == nil { + return fmt.Errorf("cannot show GUI: VM is not running (machine not initialized). Start the VM first with 'limactl start %s'", l.Instance.Name) + } + + // Default to a reasonable window size if not specified + width := 1440 + height := 900 + + if l.Instance.Config.Video.VZ.Width != nil { + width = *l.Instance.Config.Video.VZ.Width + } + if l.Instance.Config.Video.VZ.Height != nil { + height = *l.Instance.Config.Video.VZ.Height + } + + title := fmt.Sprintf("Lima: %s", l.Instance.Name) + + return l.machine.StartGraphicApplication( + float64(width), + float64(height), + vz.WithWindowTitle(title), + vz.WithController(true), + ) } func (l *LimaVzDriver) Stop(_ context.Context) error { - logrus.Info("Shutting down VZ") - canStop := l.machine.CanRequestStop() + logrus.Info("Shutting down VZ with graceful stop request") - if canStop { - _, err := l.machine.RequestStop() - if err != nil { - return err - } + if !l.machine.CanRequestStop() { + logrus.Warn("VZ: CanRequestStop is not supported, forcing immediate stop") + return l.machine.Stop() + } - timeout := time.After(5 * time.Second) - ticker := time.NewTicker(500 * time.Millisecond) - for { - select { - case <-timeout: - return errors.New("vz timeout while waiting for stop status") - case <-ticker.C: - l.machine.mu.Lock() - stopped := l.machine.stopped - l.machine.mu.Unlock() - if stopped { - return nil - } + // Request graceful shutdown (similar to ACPI power button) + result, err := l.machine.RequestStop() + if err != nil { + logrus.WithError(err).Warn("Failed to send stop request, forcing immediate stop") + return l.machine.Stop() + } + + if !result { + logrus.Warn("VZ: RequestStop returned false, forcing immediate stop") + return l.machine.Stop() + } + + // Wait for graceful shutdown with timeout (30 seconds like QEMU) + timeout := time.After(30 * time.Second) + ticker := time.NewTicker(500 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-timeout: + logrus.Warn("VZ timeout while waiting for graceful shutdown, forcing stop") + return l.machine.Stop() + + case <-ticker.C: + l.machine.mu.Lock() + stopped := l.machine.stopped + l.machine.mu.Unlock() + if stopped { + logrus.Info("VZ has shut down gracefully") + return nil } } } - - return errors.New("vz: CanRequestStop is not supported") } func (l *LimaVzDriver) GuestAgentConn(_ context.Context) (net.Conn, string, error) { diff --git a/pkg/limatype/lima_yaml.go b/pkg/limatype/lima_yaml.go index fc48766cc85..8a9c9b81620 100644 --- a/pkg/limatype/lima_yaml.go +++ b/pkg/limatype/lima_yaml.go @@ -212,19 +212,43 @@ type Firmware struct { Images []FileWithVMType `yaml:"images,omitempty" json:"images,omitempty"` } +type VZAudioOptions struct { + // InputEnabled enables audio input (microphone) from host to guest (default: false) + InputEnabled *bool `yaml:"inputEnabled,omitempty" json:"inputEnabled,omitempty" jsonschema:"nullable"` + // OutputEnabled enables audio output (speakers) from guest to host (default: true) + OutputEnabled *bool `yaml:"outputEnabled,omitempty" json:"outputEnabled,omitempty" jsonschema:"nullable"` +} + type Audio struct { - // Device is a QEMU audiodev string - Device *string `yaml:"device,omitempty" json:"device,omitempty" jsonschema:"nullable"` + // Device is a QEMU audiodev string, or "vz" for VZ driver + Device *string `yaml:"device,omitempty" json:"device,omitempty" jsonschema:"nullable"` + VZ VZAudioOptions `yaml:"vz,omitempty" json:"vz,omitempty"` } type VNCOptions struct { Display *string `yaml:"display,omitempty" json:"display,omitempty" jsonschema:"nullable"` } +type VZOptions struct { + // Width is the display width in pixels (default: 1920) + Width *int `yaml:"width,omitempty" json:"width,omitempty" jsonschema:"nullable"` + // Height is the display height in pixels (default: 1200) + Height *int `yaml:"height,omitempty" json:"height,omitempty" jsonschema:"nullable"` + // PixelsPerInch configures display density (reserved for future use, not yet supported by Apple Virtualization.framework) + // Intended for Retina/HiDPI support: standard ~80-100, Retina 144+ + PixelsPerInch *int `yaml:"pixelsPerInch,omitempty" json:"pixelsPerInch,omitempty" jsonschema:"nullable"` +} + type Video struct { // Display is a QEMU display string Display *string `yaml:"display,omitempty" json:"display,omitempty" jsonschema:"nullable"` VNC VNCOptions `yaml:"vnc,omitempty" json:"vnc,omitempty"` + VZ VZOptions `yaml:"vz,omitempty" json:"vz,omitempty"` + // Clipboard enables clipboard sharing between host and guest (default: true when display is enabled) + // For VZ: Uses SPICE agent protocol via virtio console. Requires display and spice-vdagent in guest. + // For QEMU: Depends on the display backend capabilities. + // Note: Clipboard requires a graphical display to be configured. + Clipboard *bool `yaml:"clipboard,omitempty" json:"clipboard,omitempty" jsonschema:"nullable"` } type ProvisionMode = string diff --git a/templates/experimental/debian-trixie-desktop.yaml b/templates/experimental/debian-trixie-desktop.yaml new file mode 100644 index 00000000000..e33a423f45d --- /dev/null +++ b/templates/experimental/debian-trixie-desktop.yaml @@ -0,0 +1,114 @@ +# Debian Trixie Desktop - VZ with XFCE, SPICE agent, GUI, and audio +# Includes clipboard sharing, auto-login, and graphical display + +vmType: vz + +video: + display: "vz" + vz: + width: 1440 + height: 900 + +audio: + device: "vz" + +images: +- location: "https://cloud.debian.org/images/cloud/trixie/latest/debian-13-generic-amd64.qcow2" + arch: "x86_64" +- location: "https://cloud.debian.org/images/cloud/trixie/latest/debian-13-generic-arm64.qcow2" + arch: "aarch64" + +cpus: 4 +memory: "8GiB" +disk: "50GiB" + +mounts: +- location: "~" + writable: false +- location: "/tmp/lima" + writable: true + +ssh: + localPort: 0 + loadDotSSHPubKeys: true + +hostResolver: + enabled: true + +provision: +- mode: system + script: | + #!/bin/bash + set -eux -o pipefail + + # Update package lists + apt-get update + + # Install XFCE desktop environment and required packages + DEBIAN_FRONTEND=noninteractive apt-get install -y \ + xfce4 \ + xfce4-goodies \ + lightdm \ + dbus-x11 \ + pulseaudio \ + spice-vdagent + + # Enable SPICE agent for clipboard sharing with host + systemctl enable spice-vdagentd.socket || true + + # Set graphical target + systemctl set-default graphical.target + + # Try to load virtio-sound module if available + if modinfo virtio_snd >/dev/null 2>&1; then + modprobe virtio_snd || true + modprobe snd_virtio || true + fi + + # Configure LightDM auto-login + # Use LIMA_CIDATA_USER which is already set by Lima's boot.sh + if [ -z "${LIMA_CIDATA_USER:-}" ]; then + echo "ERROR: LIMA_CIDATA_USER not set" + exit 1 + fi + + # Set a default password for the user (useful if auto-login fails or for manual login) + # Default password: "lima" (users should change this if needed) + echo "${LIMA_CIDATA_USER}:lima" | chpasswd + echo "Set default password 'lima' for user ${LIMA_CIDATA_USER}" + + mkdir -p /etc/lightdm/lightdm.conf.d + cat > /etc/lightdm/lightdm.conf.d/50-autologin.conf </dev/null 2>&1; then + modprobe virtio_snd || echo "virtio_snd module not available" + modprobe snd_virtio || echo "snd_virtio module not available" + else + echo "virtio_snd module not found in kernel" + fi + + # Configure SDDM for auto-login (no password) + # Use LIMA_CIDATA_USER which is already set by Lima's boot.sh + if [ -z "${LIMA_CIDATA_USER:-}" ]; then + echo "ERROR: LIMA_CIDATA_USER not set" + exit 1 + fi + + echo "Configuring auto-login for user: ${LIMA_CIDATA_USER}" + + # Enable passwordless sudo for the user (should already be configured, but ensure it) + echo "${LIMA_CIDATA_USER} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/90-${LIMA_CIDATA_USER} + chmod 0440 /etc/sudoers.d/90-${LIMA_CIDATA_USER} + + # Set a default password for the user (useful if auto-login fails or for manual login) + # Default password: "lima" (users should change this if needed) + echo "${LIMA_CIDATA_USER}:lima" | chpasswd + echo "Set default password 'lima' for user ${LIMA_CIDATA_USER}" + + mkdir -p /etc/sddm.conf.d + + # Create SDDM autologin config + # Use X11 session for SPICE clipboard compatibility (Wayland clipboard not yet supported by spice-vdagent) + echo "[Autologin]" > /etc/sddm.conf.d/autologin.conf + echo "User=${LIMA_CIDATA_USER}" >> /etc/sddm.conf.d/autologin.conf + echo "Session=plasmax11" >> /etc/sddm.conf.d/autologin.conf + echo "Relogin=false" >> /etc/sddm.conf.d/autologin.conf + echo "" >> /etc/sddm.conf.d/autologin.conf + echo "[General]" >> /etc/sddm.conf.d/autologin.conf + echo "HaltCommand=/usr/bin/systemctl poweroff" >> /etc/sddm.conf.d/autologin.conf + echo "RebootCommand=/usr/bin/systemctl reboot" >> /etc/sddm.conf.d/autologin.conf + echo "" >> /etc/sddm.conf.d/autologin.conf + echo "[Theme]" >> /etc/sddm.conf.d/autologin.conf + echo "Current=breeze" >> /etc/sddm.conf.d/autologin.conf + echo "CursorTheme=breeze_cursors" >> /etc/sddm.conf.d/autologin.conf + + # Configure PulseAudio for optimal performance + mkdir -p /etc/pulse/daemon.conf.d + cat > /etc/pulse/daemon.conf.d/50-optimize.conf < ~/.config/pulse/client.conf <<'EOF' + autospawn = yes + daemon-binary = /usr/bin/pulseaudio + enable-shm = yes + EOF + + # Configure KDE for optimal performance + mkdir -p ~/.config + + # Disable screen locking and power management (VM use case) + cat > ~/.config/kscreenlockerrc <<'KDEEOF' + [Daemon] + Autolock=false + LockOnResume=false + Timeout=0 + KDEEOF + + # Disable screen saver + cat > ~/.config/kscreensaverrc <<'KDEEOF' + [ScreenSaver] + Enabled=false + Timeout=0 + KDEEOF + + # Disable power management suspend/hibernate + cat > ~/.config/powermanagementprofilesrc <<'KDEEOF' + [AC] + icon=battery-charging + + [AC][DimDisplay] + idleTime=300000 + + [AC][SuspendSession] + idleTime=0 + suspendThenHibernate=false + suspendType=0 + KDEEOF + + # Optimize KDE compositor settings + cat > ~/.config/kwinrc <<'KDEEOF' + [Compositing] + Enabled=true + OpenGLIsUnsafe=false + AnimationSpeed=2 + + [Plugins] + blurEnabled=true + contrastEnabled=true + + [Windows] + FocusPolicy=ClickToFocus + + [Wayland] + InputMethod[$e]= + KDEEOF + + # Disable unwanted KDE background services + cat > ~/.config/kded5rc <<'KDEEOF' + [Module-kwrited] + autoload=false + + [Module-networkmanagement] + autoload=true + + [Module-bluedevil] + autoload=false + KDEEOF + + # Disable desktop effects that can cause issues in VMs + cat > ~/.config/kwinrulesrc <<'KDEEOF' + [General] + count=0 + KDEEOF + + # Disable Baloo file indexing (not needed in VM, saves CPU/disk) + cat > ~/.config/baloofilerc <<'KDEEOF' + [Basic Settings] + Indexing-Enabled=false + KDEEOF + + # Configure KDE wallet for passwordless operation + # Keep wallet enabled for browser SSO/OAuth tokens (GitHub, VS Code, etc.) + # but make it auto-unlock without prompting + cat > ~/.config/kwalletrc <<'KDEEOF' + [Wallet] + Enabled=true + First Use=false + Prompt on Open=false + Prompt on Close=false + Close When Idle=false + Use One Wallet=true + + [Auto Allow] + kdewallet=true + KDEEOF + + +- mode: system + script: | + #!/bin/bash + # Post-reboot verification + echo "=== Service Status ===" + systemctl status sddm --no-pager || true + + echo "=== Display Manager ===" + systemctl get-default + + echo "=== Audio Devices ===" + aplay -l || echo "Run as user to see audio devices" + +message: | + 🚀 Debian Trixie KDE Plasma Desktop (X11) - VZ Configuration + + ✨ Installed Features: + - ✅ KDE Plasma Desktop (X11 session for clipboard compatibility) + - ✅ 1920x1080 High-Resolution Display + - ✅ Auto-login (No Password Required) + - ✅ Clipboard Sharing (via SPICE agent) + - ✅ Audio Support (PulseAudio + VirtIO-Sound) + - ✅ Chromium Browser + - ✅ Essential Apps: Konsole, Dolphin, Kate + - ⚙️ Optional Apps: Ark, Gwenview, Okular (via kde-standard) + + 🎨 Desktop Configuration: + - Breeze theme + - 6 CPUs, 12GB RAM + - 80GB disk space + + 🔧 Optimizations: + - Screen lock and power management disabled + - Baloo file indexing disabled + - KDE Wallet auto-unlock enabled + + ⚠️ Note: Uses X11 instead of Wayland for SPICE clipboard compatibility. + + 🚀 Launch: limactl start --name=plasma templates/experimental/debian-trixie-plasma.yaml + 💻 Shell: limactl shell plasma diff --git a/templates/experimental/ubuntu-desktop-vz.yaml b/templates/experimental/ubuntu-desktop-vz.yaml new file mode 100644 index 00000000000..d9c5033e712 --- /dev/null +++ b/templates/experimental/ubuntu-desktop-vz.yaml @@ -0,0 +1,72 @@ +# Ubuntu Desktop with VZ Native Display +# Uses VZ driver for native macOS GUI window with desktop environment + +vmType: vz + +# VZ native display (better than SPICE for macOS) +video: + display: "vz" + +# VZ native audio +audio: + device: "vz" + +# Ubuntu cloud image +images: +- location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img" + arch: "x86_64" +- location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64.img" + arch: "aarch64" + +cpus: 4 +memory: "8GiB" +disk: "50GiB" + +mounts: +- location: "~" +- location: "/tmp/lima" + writable: true + +ssh: + localPort: 0 + loadDotSSHPubKeys: true + +# Install Xfce desktop on the cloud image +provision: +- mode: system + script: | + #!/bin/bash + set -eux -o pipefail + + # Update package lists + apt-get update + + # Install Xfce desktop environment and display manager + DEBIAN_FRONTEND=noninteractive apt-get install -y \ + xubuntu-desktop \ + xorg \ + dbus-x11 \ + spice-vdagent + + # Enable SPICE agent for clipboard sharing with host + systemctl enable spice-vdagentd.socket || true + + # Configure automatic login for lima user + mkdir -p /etc/lightdm/lightdm.conf.d + cat > /etc/lightdm/lightdm.conf.d/50-autologin.conf <<'EOF' + [Seat:*] + autologin-user=lima + autologin-user-timeout=0 + EOF + + # Enable graphical target + systemctl set-default graphical.target + + echo "Desktop environment installed. Rebooting to start GUI..." + sleep 3 + systemctl reboot + +hostResolver: + enabled: true + +message: "======================================================================\nUbuntu Desktop (Xubuntu) with VZ Native Display\n======================================================================\n #magic___^_^___line\nInstalling Xubuntu desktop environment (5-10 minutes).\nThe VM will automatically reboot when installation completes.\n #magic___^_^___line\nAfter reboot, the VZ display window opens automatically.\n #magic___^_^___line\nNo external viewer needed - VZ provides native macOS window!\n #magic___^_^___line\nFeatures:\n - Native macOS window with Metal acceleration\n - Xfce Desktop Environment (lightweight)\n - Built-in audio support \n - Auto-login as user 'lima'\n - Better performance than QEMU/SPICE on macOS\n======================================================================\n"