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