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