1/*
2 Technical details
3
4 `make-disk-image` has a bit of magic to minimize the amount of work to do in a virtual machine. It also might arguably have too much, or at least too specific magic, so please consider to work towards the effort of unifying our image builders, as outlined in https://github.com/NixOS/nixpkgs/issues/324817 before adding more.
5
6 It relies on the [LKL (Linux Kernel Library) project](https://github.com/lkl/linux) which provides Linux kernel as userspace library.
7
8 The Nix-store only image only need to run LKL tools to produce an image and will never spawn a virtual machine, whereas full images will always require a virtual machine, but also use LKL.
9
10 ### Image preparation phase
11
12 Image preparation phase will produce the initial image layout in a folder:
13
14 - devise a root folder based on `$PWD`
15 - prepare the contents by copying and restoring ACLs in this root folder
16 - load in the Nix store database all additional paths computed by `pkgs.closureInfo` in a temporary Nix store
17 - run `nixos-install` in a temporary folder
18 - transfer from the temporary store the additional paths registered to the installed NixOS
19 - compute the size of the disk image based on the apparent size of the root folder
20 - partition the disk image using the corresponding script according to the partition table type
21 - format the partitions if needed
22 - use `cptofs` (LKL tool) to copy the root folder inside the disk image
23
24 At this step, the disk image already contains the Nix store, it now only needs to be converted to the desired format to be used.
25
26 ### Image conversion phase
27
28 Using `qemu-img`, the disk image is converted from a raw format to the desired format: qcow2(-compressed), vdi, vpc.
29
30 ### Image Partitioning
31
32 #### `none`
33
34 No partition table layout is written. The image is a bare filesystem image.
35
36 #### `legacy`
37
38 The image is partitioned using MBR. There is one primary ext4 partition starting at 1 MiB that fills the rest of the disk image.
39
40 This partition layout is unsuitable for UEFI.
41
42 #### `legacy+gpt`
43
44 This partition table type uses GPT and:
45
46 - create a "no filesystem" partition from 1MiB to 2MiB ;
47 - set `bios_grub` flag on this "no filesystem" partition, which marks it as a [GRUB BIOS partition](https://www.gnu.org/software/parted/manual/html_node/set.html) ;
48 - create a primary ext4 partition starting at 2MiB and extending to the full disk image ;
49 - perform optimal alignments checks on each partition
50
51 This partition layout is unsuitable for UEFI boot, because it has no ESP (EFI System Partition) partition. It can work with CSM (Compatibility Support Module) which emulates legacy (BIOS) boot for UEFI.
52
53 #### `efi`
54
55 This partition table type uses GPT and:
56
57 - creates an FAT32 ESP partition from 8MiB to specified `bootSize` parameter (256MiB by default), set it bootable ;
58 - creates an primary ext4 partition starting after the boot partition and extending to the full disk image
59
60 #### `efixbootldr`
61
62 This partition table type uses GPT and:
63
64 - creates an FAT32 ESP partition from 8MiB to 100MiB, set it bootable ;
65 - creates an FAT32 BOOT partition from 100MiB to specified `bootSize` parameter (256MiB by default), set `bls_boot` flag ;
66 - creates an primary ext4 partition starting after the boot partition and extending to the full disk image
67
68 #### `hybrid`
69
70 This partition table type uses GPT and:
71
72 - creates a "no filesystem" partition from 0 to 1MiB, set `bios_grub` flag on it ;
73 - creates an FAT32 ESP partition from 8MiB to specified `bootSize` parameter (256MiB by default), set it bootable ;
74 - creates a primary ext4 partition starting after the boot one and extending to the full disk image
75
76 This partition could be booted by a BIOS able to understand GPT layouts and recognizing the MBR at the start.
77
78 ### How to run determinism analysis on results?
79
80 Build your derivation with `--check` to rebuild it and verify it is the same.
81
82 If it fails, you will be left with two folders with one having `.check`.
83
84 You can use `diffoscope` to see the differences between the folders.
85
86 However, `diffoscope` is currently not able to diff two QCOW2 filesystems, thus, it is advised to use raw format.
87
88 Even if you use raw disks, `diffoscope` cannot diff the partition table and partitions recursively.
89
90 To solve this, you can run `fdisk -l $image` and generate `dd if=$image of=$image-p$i.raw skip=$start count=$sectors` for each `(start, sectors)` listed in the `fdisk` output. Now, you will have each partition as a separate file and you can compare them in pairs.
91*/
92{
93 pkgs,
94 lib,
95
96 # The NixOS configuration to be installed onto the disk image.
97 config,
98
99 # The size of the disk, in megabytes.
100 # if "auto" size is calculated based on the contents copied to it and
101 # additionalSpace is taken into account.
102 diskSize ? "auto",
103
104 # additional disk space to be added to the image if diskSize "auto"
105 # is used
106 additionalSpace ? "512M",
107
108 # size of the boot partition, is only used if partitionTableType is
109 # either "efi" or "hybrid"
110 # This will be undersized slightly, as this is actually the offset of
111 # the end of the partition. Generally it will be 1MiB smaller.
112 bootSize ? "256M",
113
114 # The files and directories to be placed in the target file system.
115 # This is a list of attribute sets {source, target, mode, user, group} where
116 # `source' is the file system object (regular file or directory) to be
117 # grafted in the file system at path `target', `mode' is a string containing
118 # the permissions that will be set (ex. "755"), `user' and `group' are the
119 # user and group name that will be set as owner of the files.
120 # `mode', `user', and `group' are optional.
121 # When setting one of `user' or `group', the other needs to be set too.
122 contents ? [ ],
123
124 # Type of partition table to use; described in the `Image Partitioning` section above.
125 partitionTableType ? "legacy",
126
127 # Whether to invoke `switch-to-configuration boot` during image creation
128 installBootLoader ? true,
129
130 # Whether to output have EFIVARS available in $out/efi-vars.fd and use it during disk creation
131 touchEFIVars ? false,
132
133 # OVMF firmware derivation
134 OVMF ? pkgs.OVMF.fd,
135
136 # EFI firmware
137 efiFirmware ? OVMF.firmware,
138
139 # EFI variables
140 efiVariables ? OVMF.variables,
141
142 # The root file system type.
143 fsType ? "ext4",
144
145 # Filesystem label
146 label ? if onlyNixStore then "nix-store" else "nixos",
147
148 # The initial NixOS configuration file to be copied to
149 # /etc/nixos/configuration.nix.
150 configFile ? null,
151
152 # Shell code executed after the VM has finished.
153 postVM ? "",
154
155 # Guest memory size
156 memSize ? 1024,
157
158 # Copy the contents of the Nix store to the root of the image and
159 # skip further setup. Incompatible with `contents`,
160 # `installBootLoader` and `configFile`.
161 onlyNixStore ? false,
162
163 name ? "nixos-disk-image",
164
165 # Disk image format, one of qcow2, qcow2-compressed, vdi, vpc, raw.
166 format ? "raw",
167
168 # Disk image filename, without any extensions (e.g. `image_1`).
169 baseName ? "nixos",
170
171 # Whether to fix:
172 # - GPT Disk Unique Identifier (diskGUID)
173 # - GPT Partition Unique Identifier: depends on the layout, root partition UUID can be controlled through `rootGPUID` option
174 # - GPT Partition Type Identifier: fixed according to the layout, e.g. ESP partition, etc. through `parted` invocation.
175 # - Filesystem Unique Identifier when fsType = ext4 for *root partition*.
176 # BIOS/MBR support is "best effort" at the moment.
177 # Boot partitions may not be deterministic.
178 # Also, to fix last time checked of the ext4 partition if fsType = ext4.
179 deterministic ? true,
180
181 # GPT Partition Unique Identifier for root partition.
182 rootGPUID ? "F222513B-DED1-49FA-B591-20CE86A2FE7F",
183 # When fsType = ext4, this is the root Filesystem Unique Identifier.
184 # TODO: support other filesystems someday.
185 rootFSUID ? (if fsType == "ext4" then rootGPUID else null),
186
187 # Whether a nix channel based on the current source tree should be
188 # made available inside the image. Useful for interactive use of nix
189 # utils, but changes the hash of the image when the sources are
190 # updated.
191 copyChannel ? true,
192
193 # Additional store paths to copy to the image's store.
194 additionalPaths ? [ ],
195}:
196
197assert (
198 lib.assertOneOf "partitionTableType" partitionTableType [
199 "legacy"
200 "legacy+gpt"
201 "efi"
202 "efixbootldr"
203 "hybrid"
204 "none"
205 ]
206);
207assert (
208 lib.assertMsg (fsType == "ext4" && deterministic -> rootFSUID != null)
209 "In deterministic mode with a ext4 partition, rootFSUID must be non-null, by default, it is equal to rootGPUID."
210);
211# We use -E offset=X below, which is only supported by e2fsprogs
212assert (
213 lib.assertMsg (partitionTableType != "none" -> fsType == "ext4")
214 "to produce a partition table, we need to use -E offset flag which is support only for fsType = ext4"
215);
216assert (
217 lib.assertMsg
218 (
219 touchEFIVars
220 ->
221 partitionTableType == "hybrid"
222 || partitionTableType == "efi"
223 || partitionTableType == "efixbootldr"
224 || partitionTableType == "legacy+gpt"
225 )
226 "EFI variables can be used only with a partition table of type: hybrid, efi, efixbootldr, or legacy+gpt."
227);
228# If only Nix store image, then: contents must be empty, configFile must be unset, and we should no install bootloader.
229assert (
230 lib.assertMsg (onlyNixStore -> contents == [ ] && configFile == null && !installBootLoader)
231 "In a only Nix store image, the contents must be empty, no configuration must be provided and no bootloader should be installed."
232);
233# Either both or none of {user,group} need to be set
234assert (
235 lib.assertMsg (lib.all (
236 attrs: ((attrs.user or null) == null) == ((attrs.group or null) == null)
237 ) contents) "Contents of the disk image should set none of {user, group} or both at the same time."
238);
239
240let
241 format' = format;
242in
243let
244
245 format = if format' == "qcow2-compressed" then "qcow2" else format';
246
247 compress = lib.optionalString (format' == "qcow2-compressed") "-c";
248
249 filename =
250 "${baseName}."
251 + {
252 qcow2 = "qcow2";
253 vdi = "vdi";
254 vpc = "vhd";
255 raw = "img";
256 }
257 .${format} or format;
258
259 rootPartition =
260 {
261 # switch-case
262 legacy = "1";
263 "legacy+gpt" = "2";
264 efi = "2";
265 efixbootldr = "3";
266 hybrid = "3";
267 }
268 .${partitionTableType};
269
270 partitionDiskScript =
271 {
272 # switch-case
273 legacy = ''
274 parted --script $diskImage -- \
275 mklabel msdos \
276 mkpart primary ext4 1MiB 100% \
277 print
278 '';
279 "legacy+gpt" = ''
280 parted --script $diskImage -- \
281 mklabel gpt \
282 mkpart no-fs 1MiB 2MiB \
283 set 1 bios_grub on \
284 mkpart primary ext4 2MiB 100% \
285 align-check optimal 2 \
286 print
287 ${lib.optionalString deterministic ''
288 sgdisk \
289 --disk-guid=97FD5997-D90B-4AA3-8D16-C1723AEA73C \
290 --partition-guid=1:1C06F03B-704E-4657-B9CD-681A087A2FDC \
291 --partition-guid=2:970C694F-AFD0-4B99-B750-CDB7A329AB6F \
292 --partition-guid=3:${rootGPUID} \
293 $diskImage
294 ''}
295 '';
296 efi = ''
297 parted --script $diskImage -- \
298 mklabel gpt \
299 mkpart ESP fat32 8MiB $bootSizeMiB \
300 set 1 boot on \
301 align-check optimal 1 \
302 mkpart primary ext4 $bootSizeMiB 100% \
303 align-check optimal 2 \
304 print
305 ${lib.optionalString deterministic ''
306 sgdisk \
307 --disk-guid=97FD5997-D90B-4AA3-8D16-C1723AEA73C \
308 --partition-guid=1:1C06F03B-704E-4657-B9CD-681A087A2FDC \
309 --partition-guid=2:${rootGPUID} \
310 $diskImage
311 ''}
312 '';
313 efixbootldr = ''
314 parted --script $diskImage -- \
315 mklabel gpt \
316 mkpart ESP fat32 8MiB 100MiB \
317 set 1 boot on \
318 align-check optimal 1 \
319 mkpart BOOT fat32 100MiB $bootSizeMiB \
320 set 2 bls_boot on \
321 align-check optimal 2 \
322 mkpart ROOT ext4 $bootSizeMiB 100% \
323 align-check optimal 3 \
324 print
325 ${lib.optionalString deterministic ''
326 sgdisk \
327 --disk-guid=97FD5997-D90B-4AA3-8D16-C1723AEA73C \
328 --partition-guid=1:1C06F03B-704E-4657-B9CD-681A087A2FDC \
329 --partition-guid=2:970C694F-AFD0-4B99-B750-CDB7A329AB6F \
330 --partition-guid=3:${rootGPUID} \
331 $diskImage
332 ''}
333 '';
334 hybrid = ''
335 parted --script $diskImage -- \
336 mklabel gpt \
337 mkpart ESP fat32 8MiB $bootSizeMiB \
338 set 1 boot on \
339 align-check optimal 1 \
340 mkpart no-fs 0 1024KiB \
341 set 2 bios_grub on \
342 mkpart primary ext4 $bootSizeMiB 100% \
343 align-check optimal 3 \
344 print
345 ${lib.optionalString deterministic ''
346 sgdisk \
347 --disk-guid=97FD5997-D90B-4AA3-8D16-C1723AEA73C \
348 --partition-guid=1:1C06F03B-704E-4657-B9CD-681A087A2FDC \
349 --partition-guid=2:970C694F-AFD0-4B99-B750-CDB7A329AB6F \
350 --partition-guid=3:${rootGPUID} \
351 $diskImage
352 ''}
353 '';
354 none = "";
355 }
356 .${partitionTableType};
357
358 useEFIBoot = touchEFIVars;
359
360 nixpkgs = lib.cleanSource pkgs.path;
361
362 # FIXME: merge with channel.nix / make-channel.nix.
363 channelSources = pkgs.runCommand "nixos-${config.system.nixos.version}" { } ''
364 mkdir -p $out
365 cp -prd ${nixpkgs.outPath} $out/nixos
366 chmod -R u+w $out/nixos
367 if [ ! -e $out/nixos/nixpkgs ]; then
368 ln -s . $out/nixos/nixpkgs
369 fi
370 rm -rf $out/nixos/.git
371 echo -n ${config.system.nixos.versionSuffix} > $out/nixos/.version-suffix
372 '';
373
374 binPath = lib.makeBinPath (
375 with pkgs;
376 [
377 rsync
378 util-linux
379 parted
380 e2fsprogs
381 lkl
382 config.system.build.nixos-install
383 nixos-enter
384 nix
385 systemdMinimal
386 ]
387 ++ lib.optional deterministic gptfdisk
388 ++ stdenv.initialPath
389 );
390
391 # I'm preserving the line below because I'm going to search for it across nixpkgs to consolidate
392 # image building logic. The comment right below this now appears in 4 different places in nixpkgs :)
393 # !!! should use XML.
394 sources = map (x: x.source) contents;
395 targets = map (x: x.target) contents;
396 modes = map (x: x.mode or "''") contents;
397 users = map (x: x.user or "''") contents;
398 groups = map (x: x.group or "''") contents;
399
400 basePaths = [ config.system.build.toplevel ] ++ lib.optional copyChannel channelSources;
401
402 additionalPaths' = lib.subtractLists basePaths additionalPaths;
403
404 closureInfo = pkgs.closureInfo {
405 rootPaths = basePaths ++ additionalPaths';
406 };
407
408 blockSize = toString (4 * 1024); # ext4fs block size (not block device sector size)
409
410 prepareImage = ''
411 export PATH=${binPath}
412
413 # Yes, mkfs.ext4 takes different units in different contexts. Fun.
414 sectorsToKilobytes() {
415 echo $(( ( "$1" * 512 ) / 1024 ))
416 }
417
418 sectorsToBytes() {
419 echo $(( "$1" * 512 ))
420 }
421
422 # Given lines of numbers, adds them together
423 sum_lines() {
424 local acc=0
425 while read -r number; do
426 acc=$((acc+number))
427 done
428 echo "$acc"
429 }
430
431 mebibyte=$(( 1024 * 1024 ))
432
433 # Approximative percentage of reserved space in an ext4 fs over 512MiB.
434 # 0.05208587646484375
435 # × 1000, integer part: 52
436 compute_fudge() {
437 echo $(( $1 * 52 / 1000 ))
438 }
439
440 round_to_nearest() {
441 echo $(( ( $1 / $2 + 1) * $2 ))
442 }
443
444 mkdir $out
445
446 root="$PWD/root"
447 mkdir -p $root
448
449 # Copy arbitrary other files into the image
450 # Semi-shamelessly copied from make-etc.sh.
451 set -f
452 sources_=(${lib.concatStringsSep " " sources})
453 targets_=(${lib.concatStringsSep " " targets})
454 modes_=(${lib.concatStringsSep " " modes})
455 set +f
456
457 for ((i = 0; i < ''${#targets_[@]}; i++)); do
458 source="''${sources_[$i]}"
459 target="''${targets_[$i]}"
460 mode="''${modes_[$i]}"
461
462 if [ -n "$mode" ]; then
463 rsync_chmod_flags="--chmod=$mode"
464 else
465 rsync_chmod_flags=""
466 fi
467 # Unfortunately cptofs only supports modes, not ownership, so we can't use
468 # rsync's --chown option. Instead, we change the ownerships in the
469 # VM script with chown.
470 rsync_flags="-a --no-o --no-g $rsync_chmod_flags"
471 if [[ "$source" =~ '*' ]]; then
472 # If the source name contains '*', perform globbing.
473 mkdir -p $root/$target
474 for fn in $source; do
475 rsync $rsync_flags "$fn" $root/$target/
476 done
477 else
478 mkdir -p $root/$(dirname $target)
479 if [ -e $root/$target ]; then
480 echo "duplicate entry $target -> $source"
481 exit 1
482 elif [ -d $source ]; then
483 # Append a slash to the end of source to get rsync to copy the
484 # directory _to_ the target instead of _inside_ the target.
485 # (See `man rsync`'s note on a trailing slash.)
486 rsync $rsync_flags $source/ $root/$target
487 else
488 rsync $rsync_flags $source $root/$target
489 fi
490 fi
491 done
492
493 export HOME=$TMPDIR
494
495 # Provide a Nix database so that nixos-install can copy closures.
496 export NIX_STATE_DIR=$TMPDIR/state
497 nix-store --load-db < ${closureInfo}/registration
498
499 chmod 755 "$TMPDIR"
500 echo "running nixos-install..."
501 nixos-install --root $root --no-bootloader --no-root-passwd \
502 --system ${config.system.build.toplevel} \
503 ${if copyChannel then "--channel ${channelSources}" else "--no-channel-copy"} \
504 --substituters ""
505
506 ${lib.optionalString (additionalPaths' != [ ]) ''
507 nix --extra-experimental-features nix-command copy --to $root --no-check-sigs ${lib.concatStringsSep " " additionalPaths'}
508 ''}
509
510 diskImage=nixos.raw
511
512 bootSize=$(round_to_nearest $(numfmt --from=iec '${bootSize}') $mebibyte)
513 bootSizeMiB=$(( bootSize / 1024 / 1024 ))MiB
514
515 ${
516 if diskSize == "auto" then
517 ''
518 ${
519 if
520 partitionTableType == "efi" || partitionTableType == "efixbootldr" || partitionTableType == "hybrid"
521 then
522 ''
523 # Add the GPT at the end
524 gptSpace=$(( 512 * 34 * 1 ))
525 # Normally we'd need to account for alignment and things, if bootSize
526 # represented the actual size of the boot partition. But it instead
527 # represents the offset at which it ends.
528 # So we know bootSize is the reserved space in front of the partition.
529 reservedSpace=$(( gptSpace + bootSize ))
530 ''
531 else if partitionTableType == "legacy+gpt" then
532 ''
533 # Add the GPT at the end
534 gptSpace=$(( 512 * 34 * 1 ))
535 # And include the bios_grub partition; the ext4 partition starts at 2MiB exactly.
536 reservedSpace=$(( gptSpace + 2 * mebibyte ))
537 ''
538 else if partitionTableType == "legacy" then
539 ''
540 # Add the 1MiB aligned reserved space (includes MBR)
541 reservedSpace=$(( mebibyte ))
542 ''
543 else
544 ''
545 reservedSpace=0
546 ''
547 }
548 additionalSpace=$(( $(numfmt --from=iec '${additionalSpace}') + reservedSpace ))
549
550 # Compute required space in filesystem blocks
551 diskUsage=$(find . ! -type d -print0 | du --files0-from=- --apparent-size --count-links --block-size "${blockSize}" | cut -f1 | sum_lines)
552 # Each inode takes space!
553 numInodes=$(find . | wc -l)
554 # Convert to bytes, inodes take two blocks each!
555 diskUsage=$(( (diskUsage + 2 * numInodes) * ${blockSize} ))
556 # Then increase the required space to account for the reserved blocks.
557 fudge=$(compute_fudge $diskUsage)
558 requiredFilesystemSpace=$(( diskUsage + fudge ))
559
560 # Round up to the nearest block size.
561 # This ensures whole $blockSize bytes block sizes in the filesystem
562 # and helps towards aligning partitions optimally.
563 requiredFilesystemSpace=$(round_to_nearest $requiredFilesystemSpace ${blockSize})
564
565 diskSize=$(( requiredFilesystemSpace + additionalSpace ))
566
567 # Round up to the nearest mebibyte.
568 # This ensures whole 512 bytes sector sizes in the disk image
569 # and helps towards aligning partitions optimally.
570 diskSize=$(round_to_nearest $diskSize $mebibyte)
571
572 truncate -s "$diskSize" $diskImage
573
574 printf "Automatic disk size...\n"
575 printf " Closure space use: %d bytes\n" $diskUsage
576 printf " fudge: %d bytes\n" $fudge
577 printf " Filesystem size needed: %d bytes\n" $requiredFilesystemSpace
578 printf " Additional space: %d bytes\n" $additionalSpace
579 printf " Disk image size: %d bytes\n" $diskSize
580 ''
581 else
582 ''
583 truncate -s ${toString diskSize}M $diskImage
584 ''
585 }
586
587 ${partitionDiskScript}
588
589 ${
590 if partitionTableType != "none" then
591 ''
592 # Get start & length of the root partition in sectors to $START and $SECTORS.
593 eval $(partx $diskImage -o START,SECTORS --nr ${rootPartition} --pairs)
594
595 mkfs.${fsType} -b ${blockSize} -F -L ${label} $diskImage -E offset=$(sectorsToBytes $START) $(sectorsToKilobytes $SECTORS)K
596 ''
597 else
598 ''
599 mkfs.${fsType} -b ${blockSize} -F -L ${label} $diskImage
600 ''
601 }
602
603 echo "copying staging root to image..."
604 cptofs -p ${lib.optionalString (partitionTableType != "none") "-P ${rootPartition}"} \
605 -t ${fsType} \
606 -i $diskImage \
607 $root${lib.optionalString onlyNixStore builtins.storeDir}/* / ||
608 (echo >&2 "ERROR: cptofs failed. diskSize might be too small for closure."; exit 1)
609 '';
610
611 moveOrConvertImage = ''
612 ${
613 if format == "raw" then
614 ''
615 mv $diskImage $out/${filename}
616 ''
617 else
618 ''
619 ${pkgs.qemu-utils}/bin/qemu-img convert -f raw -O ${format} ${compress} $diskImage $out/${filename}
620 ''
621 }
622 diskImage=$out/${filename}
623 '';
624
625 createEFIVars = ''
626 efiVars=$out/efi-vars.fd
627 cp ${efiVariables} $efiVars
628 chmod 0644 $efiVars
629 '';
630
631 createHydraBuildProducts = ''
632 mkdir -p $out/nix-support
633 echo "file ${format}-image $out/${filename}" >> $out/nix-support/hydra-build-products
634 '';
635
636 buildImage = pkgs.vmTools.runInLinuxVM (
637 pkgs.runCommand name
638 {
639 preVM = prepareImage + lib.optionalString touchEFIVars createEFIVars;
640 buildInputs = with pkgs; [
641 util-linux
642 e2fsprogs
643 dosfstools
644 ];
645 postVM = moveOrConvertImage + createHydraBuildProducts + postVM;
646 QEMU_OPTS = lib.concatStringsSep " " (
647 lib.optional useEFIBoot "-drive if=pflash,format=raw,unit=0,readonly=on,file=${efiFirmware}"
648 ++ lib.optionals touchEFIVars [
649 "-drive if=pflash,format=raw,unit=1,file=$efiVars"
650 ]
651 ++ lib.optionals (OVMF.systemManagementModeRequired or false) [
652 "-machine"
653 "q35,smm=on"
654 "-global"
655 "driver=cfi.pflash01,property=secure,value=on"
656 ]
657 );
658 inherit memSize;
659 }
660 ''
661 export PATH=${binPath}:$PATH
662
663 rootDisk=${if partitionTableType != "none" then "/dev/vda${rootPartition}" else "/dev/vda"}
664
665 # It is necessary to set root filesystem unique identifier in advance, otherwise
666 # bootloader might get the wrong one and fail to boot.
667 # At the end, we reset again because we want deterministic timestamps.
668 ${lib.optionalString (fsType == "ext4" && deterministic) ''
669 tune2fs -T now ${lib.optionalString deterministic "-U ${rootFSUID}"} -c 0 -i 0 $rootDisk
670 ''}
671 # make systemd-boot find ESP without udev
672 mkdir /dev/block
673 ln -s /dev/vda1 /dev/block/254:1
674
675 mountPoint=/mnt
676 mkdir $mountPoint
677 mount $rootDisk $mountPoint
678
679 # Create the ESP and mount it. Unlike e2fsprogs, mkfs.vfat doesn't support an
680 # '-E offset=X' option, so we can't do this outside the VM.
681 ${lib.optionalString (partitionTableType == "efi" || partitionTableType == "hybrid") ''
682 mkdir -p /mnt/boot
683 mkfs.vfat -n ESP /dev/vda1
684 mount /dev/vda1 /mnt/boot
685
686 ${lib.optionalString touchEFIVars "mount -t efivarfs efivarfs /sys/firmware/efi/efivars"}
687 ''}
688 ${lib.optionalString (partitionTableType == "efixbootldr") ''
689 mkdir -p /mnt/{boot,efi}
690 mkfs.vfat -n ESP /dev/vda1
691 mkfs.vfat -n BOOT /dev/vda2
692 mount /dev/vda1 /mnt/efi
693 mount /dev/vda2 /mnt/boot
694
695 ${lib.optionalString touchEFIVars "mount -t efivarfs efivarfs /sys/firmware/efi/efivars"}
696 ''}
697
698 # Install a configuration.nix
699 mkdir -p /mnt/etc/nixos
700 ${lib.optionalString (configFile != null) ''
701 cp ${configFile} /mnt/etc/nixos/configuration.nix
702 ''}
703
704 ${lib.optionalString installBootLoader ''
705 # In this throwaway resource, we only have /dev/vda, but the actual VM may refer to another disk for bootloader, e.g. /dev/vdb
706 # Use this option to create a symlink from vda to any arbitrary device you want.
707 ${lib.optionalString (config.boot.loader.grub.enable) (
708 lib.concatMapStringsSep " " (
709 device:
710 lib.optionalString (device != "/dev/vda") ''
711 mkdir -p "$(dirname ${device})"
712 ln -s /dev/vda ${device}
713 ''
714 ) config.boot.loader.grub.devices
715 )}
716
717 # Set up core system link, bootloader (sd-boot, GRUB, uboot, etc.), etc.
718
719 # NOTE: systemd-boot-builder.py calls nix-env --list-generations which
720 # clobbers $HOME/.nix-defexpr/channels/nixos This would cause a folder
721 # /homeless-shelter to show up in the final image which in turn breaks
722 # nix builds in the target image if sandboxing is turned off (through
723 # __noChroot for example).
724 export HOME=$TMPDIR
725 NIXOS_INSTALL_BOOTLOADER=1 nixos-enter --root $mountPoint -- /nix/var/nix/profiles/system/bin/switch-to-configuration boot
726 ''}
727
728 # Set the ownerships of the contents. The modes are set in preVM.
729 # No globbing on targets, so no need to set -f
730 targets_=(${lib.concatStringsSep " " targets})
731 users_=(${lib.concatStringsSep " " users})
732 groups_=(${lib.concatStringsSep " " groups})
733 for ((i = 0; i < ''${#targets_[@]}; i++)); do
734 target="''${targets_[$i]}"
735 user="''${users_[$i]}"
736 group="''${groups_[$i]}"
737 if [ -n "$user$group" ]; then
738 # We have to nixos-enter since we need to use the user and group of the VM
739 nixos-enter --root $mountPoint -- chown -R "$user:$group" "$target"
740 fi
741 done
742
743 umount -R /mnt
744
745 # Make sure resize2fs works. Note that resize2fs has stricter criteria for resizing than a normal
746 # mount, so the `-c 0` and `-i 0` don't affect it. Setting it to `now` doesn't produce deterministic
747 # output, of course, but we can fix that when/if we start making images deterministic.
748 # In deterministic mode, this is fixed to 1970-01-01 (UNIX timestamp 0).
749 # This two-step approach is necessary otherwise `tune2fs` will want a fresher filesystem to perform
750 # some changes.
751 ${lib.optionalString (fsType == "ext4") ''
752 tune2fs -T now ${lib.optionalString deterministic "-U ${rootFSUID}"} -c 0 -i 0 $rootDisk
753 ${lib.optionalString deterministic "tune2fs -f -T 19700101 $rootDisk"}
754 ''}
755 ''
756 );
757in
758if onlyNixStore then
759 pkgs.runCommand name { } (prepareImage + moveOrConvertImage + createHydraBuildProducts + postVM)
760else
761 buildImage