Files
nixpkgs/nixos/modules/virtualisation/vm-base.nix
2026-07-21 15:53:47 +02:00

205 lines
6.2 KiB
Nix

# Backend-neutral pieces of running a NixOS guest in a VM.
#
# `qemu-vm.nix` still carries its own copies of these options: the two modules are
# never imported together because a configuration has exactly one VM backend, so
# the duplicated declarations cannot collide.
# Deduping `qemu-vm.nix` onto this module is a refactor left for later.
{
config,
lib,
pkgs,
...
}:
let
cfg = config.virtualisation;
in
{
options = {
virtualisation.memorySize = lib.mkOption {
type = lib.types.ints.positive;
default = 1024;
description = ''
The memory size in megabytes of the virtual machine.
'';
};
virtualisation.cores = lib.mkOption {
type = lib.types.ints.positive;
default = 1;
description = ''
Specify the number of cores the guest is permitted to use.
The number can be higher than the available cores on the
host system.
'';
};
# `virtualisation.diskSize` comes from `disk-size-option.nix` in the default module list.
virtualisation.additionalPaths = lib.mkOption {
type = lib.types.listOf lib.types.path;
default = [ ];
description = ''
A list of paths whose closure should be made available to the VM.
The closure is copied into the VM's Nix store image and registered in
the guest's Nix database.
'';
};
virtualisation.writableStore = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
If enabled, the Nix store in the VM is made writable by layering an
overlay filesystem on top of the (read-only) store image.
'';
};
virtualisation.writableStoreUseTmpfs = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Use a tmpfs for the writable store instead of writing to a disk image.
Turning this off makes store writes survive a reboot, at the cost of
needing a disk to put them on.
'';
};
virtualisation.useHostCerts = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
If enabled, when `NIX_SSL_CERT_FILE` is set on the host,
pass the CA certificates from the host to the VM.
'';
};
virtualisation.sharedDirectories = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule (
{ name, ... }:
{
options.source = lib.mkOption {
type = lib.types.str;
description = "The path of the directory to share, can be a shell variable";
};
options.target = lib.mkOption {
type = lib.types.path;
description = "The mount point of the directory inside the virtual machine";
};
options.tag = lib.mkOption {
type = lib.types.str;
default = name;
description = ''
The tag the guest mounts this share by. Defaults to the attribute
name. Backends impose their own length limits on tags.
'';
};
}
)
);
default = { };
example = {
my-share = {
source = "/path/to/be/shared";
target = "/mnt/shared";
};
};
description = ''
An attribute set of directories that will be shared with the virtual
machine. The attribute name is used as the mount tag.
'';
};
virtualisation.host.pkgs = lib.mkOption {
type = lib.types.pkgs;
default = pkgs;
defaultText = lib.literalExpression "pkgs";
example = lib.literalExpression ''
import pkgs.path { system = "aarch64-darwin"; }
'';
description = ''
Package set to use for the host-side tooling that launches the VM.
This is not the guest's package set: the host may well be a different
platform than the guest, which is the entire point of running a VM.
'';
};
};
config = {
# Passed on the kernel command line: a direct reference would make the closure self-referential.
systemd.services.register-nix-paths = lib.mkIf config.nix.enable {
# Runs early so the store DB is populated first; `--load-db` needs no daemon.
unitConfig.DefaultDependencies = false;
wantedBy = [ "sysinit.target" ];
before = [
"sysinit.target"
"shutdown.target"
"nix-daemon.socket"
"nix-daemon.service"
];
after = [ "local-fs.target" ];
conflicts = [ "shutdown.target" ];
restartIfChanged = false;
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
User = lib.mkIf (config.nix.daemonUser != "root") config.nix.daemonUser;
Group = lib.mkIf (config.nix.daemonGroup != "root") config.nix.daemonGroup;
};
script = ''
if [[ "$(cat /proc/cmdline)" =~ regInfo=([^ ]*) ]]; then
${lib.getExe' config.nix.package.out "nix-store"} --load-db < "''${BASH_REMATCH[1]}"
fi
'';
};
virtualisation.additionalPaths = [ config.system.build.toplevel ];
# Read-only erofs store, overlaid when writable. Override per entry: `mkVMOverride` on
# the whole set would drop other modules' filesystems, including the Rosetta share.
fileSystems = {
"/nix/.ro-store" = lib.mkVMOverride {
device = "/dev/disk/by-label/nix-store";
fsType = "erofs";
neededForBoot = true;
options = [ "ro" ];
};
"/nix/store" = lib.mkVMOverride (
if cfg.writableStore then
{
overlay = {
lowerdir = [ "/nix/.ro-store" ];
upperdir = "/nix/.rw-store/upper";
workdir = "/nix/.rw-store/work";
};
}
else
{
device = "/nix/.ro-store";
fsType = "none";
options = [ "bind" ];
}
);
"/nix/.rw-store" = lib.mkIf (cfg.writableStore && cfg.writableStoreUseTmpfs) (
lib.mkVMOverride {
fsType = "tmpfs";
options = [ "mode=0755" ];
neededForBoot = true;
}
);
};
swapDevices = lib.mkVMOverride [ ];
boot.initrd.luks.devices = lib.mkVMOverride { };
# The host keeps time for us.
services.timesyncd.enable = false;
};
}