1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
|
# 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;
};
}
|