{ 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; }; 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 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; }; ( 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; }; }; }; }; virtualisation = config.virtualisation; in { vmVariant = extendVMConfig virtualisation.vmVariant; vmVariantWithBootLoader = extendVMConfig virtualisation.vmVariantWithBootLoader; }; system.build = { vm = lib.mkForce ( ( vm: 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 "&"); shellApp = pkgs.writeShellApplication { name = "run-${config.system.name}-vm"; text = lib.strings.concatLines ( [ "${originalCommand} ${suffix}" ] ++ ( let # Run `remote-viewer` as normal user to limit access viewerPrefix = "sudo -Eu\"#$SUDO_UID\" "; spice = config.virtualisation.qemu.spice; in ( lib.optionals config.virtualisation.virt-viewer [ "${viewerPrefix}${pkgs.virt-viewer}/bin/remote-viewer spice://${lib.escapeShellArg spice.bindAddress}:${toString spice.port}" # Kill QEMU after `remote-viewer` finished running "kill %1" ]))); }; # Run VM as root if requested wrapped = if !config.virtualisation.runAsRoot then shellApp else pkgs.writeShellApplication { name = shellApp.name; text = '' sudo -E ${shellApp}/bin/${shellApp.name} ''; }; in pkgs.symlinkJoin { name = packageName; paths = [ wrapped ]; }) vanillaVM); }; }; }