{ config, lib, options, pkgs, ... }: let packageName = "custom-nixos-vm"; # Determine `system.build` configuration without this file's influence mergedBuildOption = with options.system; lib.mergeDefinitions build.loc build.type (lib.lists.forEach ( builtins.filter (item: !(lib.path.hasPrefix ./. (/. + item.file))) build.definitionsWithLocations) (item: { inherit (item) file value; })); # Get vanilla `config.system.build.vm` vanillaVM = mergedBuildOption.mergedValue.vm; in { options = let # Add new options to `config.virtualisation.vmVariant` and `config.virtualisation.vmVariantWithBootLoader` vmVariantOptions = { virtualisation = { runAsRoot = lib.mkOption { type = lib.types.bool; description = "Whether to launch the VM as root."; default = false; }; sharedHostKeys = lib.mkOption { type = lib.types.bool; description = "Whether to share the local host SSH keys with the VM."; default = false; }; sharedUserKeys = lib.mkOption { type = lib.types.bool; description = lib.mdDoc "Whether to load the current user's keys into every user's homer on the VM."; default = false; }; usb-redirect = lib.mkOption { type = lib.types.bool; description = lib.mdDoc "Whether to enable USB redirection to the VM."; default = false; }; virt-viewer = lib.mkOption { type = lib.types.bool; description = "Whether to use `remote-viewer` for displaying the VM."; default = false; }; qemu = { runInBackground = lib.mkOption { type = lib.types.bool; description = "Whether to run the QEMU command in a background job"; default = false; }; spice = { enable = lib.mkEnableOption "spice"; bindAddress = lib.mkOption { type = lib.types.str; description = "The IP address for listening to incoming SPICE connections."; default = "127.0.0.1"; }; port = lib.mkOption { type = lib.types.port; description = "The port for listening to incoming SPICE connections."; default = 5900; }; }; }; }; }; in { virtualisation = { vmVariant = vmVariantOptions; vmVariantWithBootLoader = vmVariantOptions; }; }; config = { virtualisation = let extendVMConfig = vmVariant: { # Prevent GRUB2 errors in `nixos-rebuild build-vm-with-bootloader` boot.loader.efi.efiSysMountPoint = lib.mkVMOverride "/boot"; virtualisation = { # Enable root permissions to get access to the `/etc/ssh` directory runAsRoot = lib.mkIf (vmVariant.virtualisation.sharedHostKeys || vmVariant.virtualisation.usb-redirect) true; # Enable spice and run QEMU in background to let `remote-viewer` take over qemu = { spice.enable = lib.mkIf vmVariant.virtualisation.virt-viewer true; runInBackground = lib.mkIf vmVariant.virtualisation.virt-viewer true; options = with { inherit (vmVariant.virtualisation.qemu) spice; inherit (vmVariant.virtualisation) usb-redirect; }; ( lib.optionals usb-redirect ( [ "-usb" "-device qemu-xhci" ] ++ (builtins.concatMap (index: let devName = "usbredirchardev${toString index}"; in [ "-chardev spicevmc,name=usbredir,id=${devName}" "-device usb-redir,chardev=${devName},id=usbredirdev${toString index}" ]) (lib.lists.range 1 3)))) ++ ( lib.optional (spice.enable) ("-spice " + ( lib.concatStringsSep "," [ "addr=${lib.escapeShellArg spice.bindAddress}" "port=${toString spice.port}" "disable-ticketing=on" ]))); }; # Map SSH keys into the vm if necessary sharedDirectories = lib.optionalAttrs (vmVariant.virtualisation.sharedHostKeys) { hostKeys = let path = "/etc/ssh"; in { source = path; target = path; }; } // (lib.optionalAttrs vmVariant.virtualisation.sharedUserKeys ( lib.attrsets.concatMapAttrs ( name: user: let prefix = if vmVariant.virtualisation.runAsRoot then "sudo -u#$SUDO_UID " else ""; homeDir = "$(${prefix}bash -c 'echo $HOME')"; in { "userKeys-${name}" = { source = "${homeDir}/.ssh"; target = "/home/${name}/.ssh"; }; }) config.users.myUsers)); }; }; inherit (config.virtualisation) vmVariant vmVariantWithBootLoader ; in { vmVariant = extendVMConfig vmVariant; vmVariantWithBootLoader = extendVMConfig vmVariantWithBootLoader; }; system.build = { vm = lib.mkForce ( let vm = vanillaVM; in if (vm.name == packageName) then vm else let originalCommand = "${vm}/bin/run-${config.system.name}-vm"; # Have the command run in background if requested suffix = lib.concatStringsSep " " ( lib.optional config.virtualisation.qemu.runInBackground "&"); vmRunner = pkgs.writeShellApplication { name = "run-${config.system.name}-vm"; runtimeInputs = lib.optional config.virtualisation.usb-redirect pkgs.spice-gtk; text = lib.strings.concatLines ( [ "${originalCommand} ${suffix}" ] ++ ( # Run `remote-viewer` as normal user to limit access ( lib.optionals config.virtualisation.virt-viewer ( let spice = config.virtualisation.qemu.spice; remoteAddress = "spice://${lib.escapeShellArg spice.bindAddress}:${toString spice.port}"; in [ "${pkgs.virt-viewer}/bin/remote-viewer ${remoteAddress}" # Kill QEMU after `remote-viewer` finished running "kill %1" ])))); }; # Run VM as root if requested wrapped = if !config.virtualisation.runAsRoot then vmRunner else pkgs.writeShellApplication { inherit (vmRunner) name; text = '' sudo -E "${vmRunner}/bin/${vmRunner.name}" ''; }; in pkgs.symlinkJoin { name = packageName; paths = [ wrapped ]; }); }; }; }