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