1# Note: This is a private API, internal to NixOS. Its interface is subject
2# to change without notice.
3#
4# The result of this builder is a single disk image, partitioned like this:
5#
6# * partition #1: a very small, 1MiB partition to leave room for Grub.
7#
8# * partition #2: boot, a partition formatted with FAT to be used for /boot.
9# FAT is chosen to support EFI.
10#
11# * partition #3: nixos, a partition dedicated to a zpool.
12#
13# This single-disk approach does not satisfy ZFS's requirements for autoexpand,
14# however automation can expand it anyway. For example, with
15# `services.zfs.expandOnBoot`.
16{
17 lib,
18 pkgs,
19 # The NixOS configuration to be installed onto the disk image.
20 config,
21
22 # size of the FAT partition, in MiB (1024x1024 bytes).
23 bootSize ? 1024,
24
25 # memory allocated for virtualized build instance, in MiB (1024x1024 bytes).
26 memSize ? 1024,
27
28 # The size of the root partition, in MiB (1024x1024 bytes).
29 rootSize ? 2048,
30
31 # The name of the ZFS pool
32 rootPoolName ? "tank",
33
34 # zpool properties
35 rootPoolProperties ? {
36 autoexpand = "on";
37 },
38 # pool-wide filesystem properties
39 rootPoolFilesystemProperties ? {
40 acltype = "posixacl";
41 atime = "off";
42 compression = "on";
43 mountpoint = "legacy";
44 xattr = "sa";
45 },
46
47 # datasets, with per-attribute options:
48 # mount: (optional) mount point in the VM
49 # properties: (optional) ZFS properties on the dataset, like filesystemProperties
50 # Notes:
51 # 1. datasets will be created from shorter to longer names as a simple topo-sort
52 # 2. you should define a root's dataset's mount for `/`
53 datasets ? { },
54
55 # The files and directories to be placed in the target file system.
56 # This is a list of attribute sets {source, target} where `source'
57 # is the file system object (regular file or directory) to be
58 # grafted in the file system at path `target'.
59 contents ? [ ],
60
61 # The initial NixOS configuration file to be copied to
62 # /etc/nixos/configuration.nix. This configuration will be embedded
63 # inside a configuration which includes the described ZFS fileSystems.
64 configFile ? null,
65
66 # Shell code executed after the VM has finished.
67 postVM ? "",
68
69 name ? "nixos-disk-image",
70
71 # Disk image format, one of qcow2, qcow2-compressed, vdi, vpc, raw.
72 format ? "raw",
73
74 # Include a copy of Nixpkgs in the disk image
75 includeChannel ? true,
76}:
77let
78 formatOpt = if format == "qcow2-compressed" then "qcow2" else format;
79
80 compress = lib.optionalString (format == "qcow2-compressed") "-c";
81
82 filenameSuffix =
83 "."
84 + {
85 qcow2 = "qcow2";
86 vdi = "vdi";
87 vpc = "vhd";
88 raw = "img";
89 }
90 .${formatOpt} or formatOpt;
91 rootFilename = "nixos.root${filenameSuffix}";
92
93 # FIXME: merge with channel.nix / make-channel.nix.
94 channelSources =
95 let
96 nixpkgs = lib.cleanSource pkgs.path;
97 in
98 pkgs.runCommand "nixos-${config.system.nixos.version}" { } ''
99 mkdir -p $out
100 cp -prd ${nixpkgs.outPath} $out/nixos
101 chmod -R u+w $out/nixos
102 if [ ! -e $out/nixos/nixpkgs ]; then
103 ln -s . $out/nixos/nixpkgs
104 fi
105 rm -rf $out/nixos/.git
106 echo -n ${config.system.nixos.versionSuffix} > $out/nixos/.version-suffix
107 '';
108
109 closureInfo = pkgs.closureInfo {
110 rootPaths = [ config.system.build.toplevel ] ++ (lib.optional includeChannel channelSources);
111 };
112
113 modulesTree = pkgs.aggregateModules (
114 with config.boot;
115 [
116 kernelPackages.kernel
117 kernelPackages.${pkgs.zfs.kernelModuleAttribute}
118 ]
119 );
120
121 tools = lib.makeBinPath (
122 with pkgs;
123 [
124 nixos-enter
125 config.system.build.nixos-install
126 dosfstools
127 e2fsprogs
128 gptfdisk
129 nix
130 parted
131 util-linux
132 zfs
133 ]
134 );
135
136 hasDefinedMount = disk: ((disk.mount or null) != null);
137
138 stringifyProperties =
139 prefix: properties:
140 lib.concatStringsSep " \\\n" (
141 lib.mapAttrsToList (
142 property: value: "${prefix} ${lib.escapeShellArg property}=${lib.escapeShellArg value}"
143 ) properties
144 );
145
146 createDatasets =
147 let
148 datasetlist = lib.mapAttrsToList lib.nameValuePair datasets;
149 sorted = lib.sort (
150 left: right: (lib.stringLength left.name) < (lib.stringLength right.name)
151 ) datasetlist;
152 cmd =
153 { name, value }:
154 let
155 properties = stringifyProperties "-o" (value.properties or { });
156 in
157 "zfs create -p ${properties} ${name}";
158 in
159 lib.concatMapStringsSep "\n" cmd sorted;
160
161 mountDatasets =
162 let
163 datasetlist = lib.mapAttrsToList lib.nameValuePair datasets;
164 mounts = lib.filter ({ value, ... }: hasDefinedMount value) datasetlist;
165 sorted = lib.sort (
166 left: right: (lib.stringLength left.value.mount) < (lib.stringLength right.value.mount)
167 ) mounts;
168 cmd =
169 { name, value }:
170 ''
171 mkdir -p /mnt${lib.escapeShellArg value.mount}
172 mount -t zfs ${name} /mnt${lib.escapeShellArg value.mount}
173 '';
174 in
175 lib.concatMapStringsSep "\n" cmd sorted;
176
177 unmountDatasets =
178 let
179 datasetlist = lib.mapAttrsToList lib.nameValuePair datasets;
180 mounts = lib.filter ({ value, ... }: hasDefinedMount value) datasetlist;
181 sorted = lib.sort (
182 left: right: (lib.stringLength left.value.mount) > (lib.stringLength right.value.mount)
183 ) mounts;
184 cmd =
185 { name, value }:
186 ''
187 umount /mnt${lib.escapeShellArg value.mount}
188 '';
189 in
190 lib.concatMapStringsSep "\n" cmd sorted;
191
192 fileSystemsCfgFile =
193 let
194 mountable = lib.filterAttrs (_: value: hasDefinedMount value) datasets;
195 in
196 pkgs.runCommand "filesystem-config.nix"
197 {
198 buildInputs = with pkgs; [
199 jq
200 nixpkgs-fmt
201 ];
202 filesystems = builtins.toJSON {
203 fileSystems = lib.mapAttrs' (dataset: attrs: {
204 name = attrs.mount;
205 value = {
206 fsType = "zfs";
207 device = "${dataset}";
208 };
209 }) mountable;
210 };
211 passAsFile = [ "filesystems" ];
212 }
213 ''
214 (
215 echo "builtins.fromJSON '''"
216 jq . < "$filesystemsPath"
217 echo "'''"
218 ) > $out
219
220 nixpkgs-fmt $out
221 '';
222
223 mergedConfig =
224 if configFile == null then
225 fileSystemsCfgFile
226 else
227 pkgs.runCommand "configuration.nix"
228 {
229 buildInputs = with pkgs; [ nixpkgs-fmt ];
230 }
231 ''
232 (
233 echo '{ imports = ['
234 printf "(%s)\n" "$(cat ${fileSystemsCfgFile})";
235 printf "(%s)\n" "$(cat ${configFile})";
236 echo ']; }'
237 ) > $out
238
239 nixpkgs-fmt $out
240 '';
241
242 image =
243 (pkgs.vmTools.override {
244 rootModules = [
245 "9p"
246 "9pnet_virtio"
247 "virtio_blk"
248 "virtio_pci"
249 "virtiofs"
250 "zfs"
251 ];
252 kernel = modulesTree;
253 }).runInLinuxVM
254 (
255 pkgs.runCommand name
256 {
257 inherit memSize;
258 QEMU_OPTS = "-drive file=$rootDiskImage,if=virtio,cache=unsafe,werror=report";
259 preVM = ''
260 PATH=$PATH:${pkgs.qemu_kvm}/bin
261 mkdir $out
262
263 rootDiskImage=root.raw
264 qemu-img create -f raw $rootDiskImage ${toString (bootSize + rootSize)}M
265 '';
266
267 postVM = ''
268 ${
269 if formatOpt == "raw" then
270 ''
271 mv $rootDiskImage $out/${rootFilename}
272 ''
273 else
274 ''
275 ${pkgs.qemu_kvm}/bin/qemu-img convert -f raw -O ${formatOpt} ${compress} $rootDiskImage $out/${rootFilename}
276 ''
277 }
278 rootDiskImage=$out/${rootFilename}
279 set -x
280 ${postVM}
281 '';
282 }
283 ''
284 export PATH=${tools}:$PATH
285 set -x
286
287 cp -sv /dev/vda /dev/sda
288 cp -sv /dev/vda /dev/xvda
289
290 parted --script /dev/vda -- \
291 mklabel gpt \
292 mkpart no-fs 1MiB 2MiB \
293 set 1 bios_grub on \
294 align-check optimal 1 \
295 mkpart primary fat32 2MiB ${toString bootSize}MiB \
296 align-check optimal 2 \
297 mkpart primary fat32 ${toString bootSize}MiB -1MiB \
298 align-check optimal 3 \
299 print
300
301 sfdisk --dump /dev/vda
302
303
304 zpool create \
305 ${stringifyProperties " -o" rootPoolProperties} \
306 ${stringifyProperties " -O" rootPoolFilesystemProperties} \
307 ${rootPoolName} /dev/vda3
308 parted --script /dev/vda -- print
309
310 ${createDatasets}
311 ${mountDatasets}
312
313 mkdir -p /mnt/boot
314 mkfs.vfat -n ESP /dev/vda2
315 mount /dev/vda2 /mnt/boot
316
317 mount
318
319 # Install a configuration.nix
320 mkdir -p /mnt/etc/nixos
321 # `cat` so it is mutable on the fs
322 cat ${mergedConfig} > /mnt/etc/nixos/configuration.nix
323
324 export NIX_STATE_DIR=$TMPDIR/state
325 nix-store --load-db < ${closureInfo}/registration
326
327 nixos-install \
328 --root /mnt \
329 --no-root-passwd \
330 --system ${config.system.build.toplevel} \
331 --substituters "" \
332 ${lib.optionalString includeChannel ''--channel ${channelSources}''}
333
334 df -h
335
336 umount /mnt/boot
337 ${unmountDatasets}
338
339 zpool export ${rootPoolName}
340 ''
341 );
342in
343image