nixos/preload: init
[NixPkgs.git] / nixos / lib / make-disk-image.nix
blobe5d82f4de7c9d28d00405e9246f141f66dfef4ee
1 /* Technical details
3 `make-disk-image` has a bit of magic to minimize the amount of work to do in a virtual machine.
5 It relies on the [LKL (Linux Kernel Library) project](https://github.com/lkl/linux) which provides Linux kernel as userspace library.
7 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 ### Image preparation phase
11 Image preparation phase will produce the initial image layout in a folder:
13 - devise a root folder based on `$PWD`
14 - prepare the contents by copying and restoring ACLs in this root folder
15 - load in the Nix store database all additional paths computed by `pkgs.closureInfo` in a temporary Nix store
16 - run `nixos-install` in a temporary folder
17 - transfer from the temporary store the additional paths registered to the installed NixOS
18 - compute the size of the disk image based on the apparent size of the root folder
19 - partition the disk image using the corresponding script according to the partition table type
20 - format the partitions if needed
21 - use `cptofs` (LKL tool) to copy the root folder inside the disk image
23 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 ### Image conversion phase
27 Using `qemu-img`, the disk image is converted from a raw format to the desired format: qcow2(-compressed), vdi, vpc.
29 ### Image Partitioning
31 #### `none`
33 No partition table layout is written. The image is a bare filesystem image.
35 #### `legacy`
37 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 This partition layout is unsuitable for UEFI.
41 #### `legacy+gpt`
43 This partition table type uses GPT and:
45 - create a "no filesystem" partition from 1MiB to 2MiB ;
46 - 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) ;
47 - create a primary ext4 partition starting at 2MiB and extending to the full disk image ;
48 - perform optimal alignments checks on each partition
50 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 #### `efi`
54 This partition table type uses GPT and:
56 - creates an FAT32 ESP partition from 8MiB to specified `bootSize` parameter (256MiB by default), set it bootable ;
57 - creates an primary ext4 partition starting after the boot partition and extending to the full disk image
59 #### `hybrid`
61 This partition table type uses GPT and:
63 - creates a "no filesystem" partition from 0 to 1MiB, set `bios_grub` flag on it ;
64 - creates an FAT32 ESP partition from 8MiB to specified `bootSize` parameter (256MiB by default), set it bootable ;
65 - creates a primary ext4 partition starting after the boot one and extending to the full disk image
67 This partition could be booted by a BIOS able to understand GPT layouts and recognizing the MBR at the start.
69 ### How to run determinism analysis on results?
71 Build your derivation with `--check` to rebuild it and verify it is the same.
73 If it fails, you will be left with two folders with one having `.check`.
75 You can use `diffoscope` to see the differences between the folders.
77 However, `diffoscope` is currently not able to diff two QCOW2 filesystems, thus, it is advised to use raw format.
79 Even if you use raw disks, `diffoscope` cannot diff the partition table and partitions recursively.
81 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.
83 { pkgs
84 , lib
86 , # The NixOS configuration to be installed onto the disk image.
87   config
89 , # The size of the disk, in megabytes.
90   # if "auto" size is calculated based on the contents copied to it and
91   #   additionalSpace is taken into account.
92   diskSize ? "auto"
94 , # additional disk space to be added to the image if diskSize "auto"
95   # is used
96   additionalSpace ? "512M"
98 , # size of the boot partition, is only used if partitionTableType is
99   # either "efi" or "hybrid"
100   # This will be undersized slightly, as this is actually the offset of
101   # the end of the partition. Generally it will be 1MiB smaller.
102   bootSize ? "256M"
104 , # The files and directories to be placed in the target file system.
105   # This is a list of attribute sets {source, target, mode, user, group} where
106   # `source' is the file system object (regular file or directory) to be
107   # grafted in the file system at path `target', `mode' is a string containing
108   # the permissions that will be set (ex. "755"), `user' and `group' are the
109   # user and group name that will be set as owner of the files.
110   # `mode', `user', and `group' are optional.
111   # When setting one of `user' or `group', the other needs to be set too.
112   contents ? []
114 , # Type of partition table to use; either "legacy", "efi", or "none".
115   # For "efi" images, the GPT partition table is used and a mandatory ESP
116   #   partition of reasonable size is created in addition to the root partition.
117   # For "legacy", the msdos partition table is used and a single large root
118   #   partition is created.
119   # For "legacy+gpt", the GPT partition table is used, a 1MiB no-fs partition for
120   #   use by the bootloader is created, and a single large root partition is
121   #   created.
122   # For "hybrid", the GPT partition table is used and a mandatory ESP
123   #   partition of reasonable size is created in addition to the root partition.
124   #   Also a legacy MBR will be present.
125   # For "none", no partition table is created. Enabling `installBootLoader`
126   #   most likely fails as GRUB will probably refuse to install.
127   partitionTableType ? "legacy"
129 , # Whether to invoke `switch-to-configuration boot` during image creation
130   installBootLoader ? true
132 , # Whether to output have EFIVARS available in $out/efi-vars.fd and use it during disk creation
133   touchEFIVars ? false
135 , # OVMF firmware derivation
136   OVMF ? pkgs.OVMF.fd
138 , # EFI firmware
139   efiFirmware ? OVMF.firmware
141 , # EFI variables
142   efiVariables ? OVMF.variables
144 , # The root file system type.
145   fsType ? "ext4"
147 , # Filesystem label
148   label ? if onlyNixStore then "nix-store" else "nixos"
150 , # The initial NixOS configuration file to be copied to
151   # /etc/nixos/configuration.nix.
152   configFile ? null
154 , # Shell code executed after the VM has finished.
155   postVM ? ""
157 , # Guest memory size
158   memSize ? 1024
160 , # Copy the contents of the Nix store to the root of the image and
161   # skip further setup. Incompatible with `contents`,
162   # `installBootLoader` and `configFile`.
163   onlyNixStore ? false
165 , name ? "nixos-disk-image"
167 , # Disk image format, one of qcow2, qcow2-compressed, vdi, vpc, raw.
168   format ? "raw"
170   # Whether to fix:
171   #   - GPT Disk Unique Identifier (diskGUID)
172   #   - GPT Partition Unique Identifier: depends on the layout, root partition UUID can be controlled through `rootGPUID` option
173   #   - GPT Partition Type Identifier: fixed according to the layout, e.g. ESP partition, etc. through `parted` invocation.
174   #   - Filesystem Unique Identifier when fsType = ext4 for *root partition*.
175   # BIOS/MBR support is "best effort" at the moment.
176   # Boot partitions may not be deterministic.
177   # Also, to fix last time checked of the ext4 partition if fsType = ext4.
178 , deterministic ? true
180   # GPT Partition Unique Identifier for root partition.
181 , rootGPUID ? "F222513B-DED1-49FA-B591-20CE86A2FE7F"
182   # When fsType = ext4, this is the root Filesystem Unique Identifier.
183   # TODO: support other filesystems someday.
184 , rootFSUID ? (if fsType == "ext4" then rootGPUID else null)
186 , # Whether a nix channel based on the current source tree should be
187   # made available inside the image. Useful for interactive use of nix
188   # utils, but changes the hash of the image when the sources are
189   # updated.
190   copyChannel ? true
192 , # Additional store paths to copy to the image's store.
193   additionalPaths ? []
196 assert (lib.assertOneOf "partitionTableType" partitionTableType [ "legacy" "legacy+gpt" "efi" "hybrid" "none" ]);
197 assert (lib.assertMsg (fsType == "ext4" && deterministic -> rootFSUID != null) "In deterministic mode with a ext4 partition, rootFSUID must be non-null, by default, it is equal to rootGPUID.");
198   # We use -E offset=X below, which is only supported by e2fsprogs
199 assert (lib.assertMsg (partitionTableType != "none" -> fsType == "ext4") "to produce a partition table, we need to use -E offset flag which is support only for fsType = ext4");
200 assert (lib.assertMsg (touchEFIVars -> partitionTableType == "hybrid" || partitionTableType == "efi" || partitionTableType == "legacy+gpt") "EFI variables can be used only with a partition table of type: hybrid, efi or legacy+gpt.");
201   # If only Nix store image, then: contents must be empty, configFile must be unset, and we should no install bootloader.
202 assert (lib.assertMsg (onlyNixStore -> contents == [] && configFile == null && !installBootLoader) "In a only Nix store image, the contents must be empty, no configuration must be provided and no bootloader should be installed.");
203 # Either both or none of {user,group} need to be set
204 assert (lib.assertMsg (lib.all
205          (attrs: ((attrs.user  or null) == null)
206               == ((attrs.group or null) == null))
207         contents) "Contents of the disk image should set none of {user, group} or both at the same time.");
209 with lib;
211 let format' = format; in let
213   format = if format' == "qcow2-compressed" then "qcow2" else format';
215   compress = optionalString (format' == "qcow2-compressed") "-c";
217   filename = "nixos." + {
218     qcow2 = "qcow2";
219     vdi   = "vdi";
220     vpc   = "vhd";
221     raw   = "img";
222   }.${format} or format;
224   rootPartition = { # switch-case
225     legacy = "1";
226     "legacy+gpt" = "2";
227     efi = "2";
228     hybrid = "3";
229   }.${partitionTableType};
231   partitionDiskScript = { # switch-case
232     legacy = ''
233       parted --script $diskImage -- \
234         mklabel msdos \
235         mkpart primary ext4 1MiB -1
236     '';
237     "legacy+gpt" = ''
238       parted --script $diskImage -- \
239         mklabel gpt \
240         mkpart no-fs 1MB 2MB \
241         set 1 bios_grub on \
242         align-check optimal 1 \
243         mkpart primary ext4 2MB -1 \
244         align-check optimal 2 \
245         print
246       ${optionalString deterministic ''
247           sgdisk \
248           --disk-guid=97FD5997-D90B-4AA3-8D16-C1723AEA73C \
249           --partition-guid=1:1C06F03B-704E-4657-B9CD-681A087A2FDC \
250           --partition-guid=2:970C694F-AFD0-4B99-B750-CDB7A329AB6F \
251           --partition-guid=3:${rootGPUID} \
252           $diskImage
253       ''}
254     '';
255     efi = ''
256       parted --script $diskImage -- \
257         mklabel gpt \
258         mkpart ESP fat32 8MiB ${bootSize} \
259         set 1 boot on \
260         mkpart primary ext4 ${bootSize} -1
261       ${optionalString deterministic ''
262           sgdisk \
263           --disk-guid=97FD5997-D90B-4AA3-8D16-C1723AEA73C \
264           --partition-guid=1:1C06F03B-704E-4657-B9CD-681A087A2FDC \
265           --partition-guid=2:${rootGPUID} \
266           $diskImage
267       ''}
268     '';
269     hybrid = ''
270       parted --script $diskImage -- \
271         mklabel gpt \
272         mkpart ESP fat32 8MiB ${bootSize} \
273         set 1 boot on \
274         mkpart no-fs 0 1024KiB \
275         set 2 bios_grub on \
276         mkpart primary ext4 ${bootSize} -1
277       ${optionalString deterministic ''
278           sgdisk \
279           --disk-guid=97FD5997-D90B-4AA3-8D16-C1723AEA73C \
280           --partition-guid=1:1C06F03B-704E-4657-B9CD-681A087A2FDC \
281           --partition-guid=2:970C694F-AFD0-4B99-B750-CDB7A329AB6F \
282           --partition-guid=3:${rootGPUID} \
283           $diskImage
284       ''}
285     '';
286     none = "";
287   }.${partitionTableType};
289   useEFIBoot = touchEFIVars;
291   nixpkgs = cleanSource pkgs.path;
293   # FIXME: merge with channel.nix / make-channel.nix.
294   channelSources = pkgs.runCommand "nixos-${config.system.nixos.version}" {} ''
295     mkdir -p $out
296     cp -prd ${nixpkgs.outPath} $out/nixos
297     chmod -R u+w $out/nixos
298     if [ ! -e $out/nixos/nixpkgs ]; then
299       ln -s . $out/nixos/nixpkgs
300     fi
301     rm -rf $out/nixos/.git
302     echo -n ${config.system.nixos.versionSuffix} > $out/nixos/.version-suffix
303   '';
305   binPath = with pkgs; makeBinPath (
306     [ rsync
307       util-linux
308       parted
309       e2fsprogs
310       lkl
311       config.system.build.nixos-install
312       config.system.build.nixos-enter
313       nix
314       systemdMinimal
315     ]
316     ++ lib.optional deterministic gptfdisk
317     ++ stdenv.initialPath);
319   # I'm preserving the line below because I'm going to search for it across nixpkgs to consolidate
320   # image building logic. The comment right below this now appears in 4 different places in nixpkgs :)
321   # !!! should use XML.
322   sources = map (x: x.source) contents;
323   targets = map (x: x.target) contents;
324   modes   = map (x: x.mode  or "''") contents;
325   users   = map (x: x.user  or "''") contents;
326   groups  = map (x: x.group or "''") contents;
328   basePaths = [ config.system.build.toplevel ]
329     ++ lib.optional copyChannel channelSources;
331   additionalPaths' = subtractLists basePaths additionalPaths;
333   closureInfo = pkgs.closureInfo {
334     rootPaths = basePaths ++ additionalPaths';
335   };
337   blockSize = toString (4 * 1024); # ext4fs block size (not block device sector size)
339   prepareImage = ''
340     export PATH=${binPath}
342     # Yes, mkfs.ext4 takes different units in different contexts. Fun.
343     sectorsToKilobytes() {
344       echo $(( ( "$1" * 512 ) / 1024 ))
345     }
347     sectorsToBytes() {
348       echo $(( "$1" * 512  ))
349     }
351     # Given lines of numbers, adds them together
352     sum_lines() {
353       local acc=0
354       while read -r number; do
355         acc=$((acc+number))
356       done
357       echo "$acc"
358     }
360     mebibyte=$(( 1024 * 1024 ))
362     # Approximative percentage of reserved space in an ext4 fs over 512MiB.
363     # 0.05208587646484375
364     #  Ã— 1000, integer part: 52
365     compute_fudge() {
366       echo $(( $1 * 52 / 1000 ))
367     }
369     mkdir $out
371     root="$PWD/root"
372     mkdir -p $root
374     # Copy arbitrary other files into the image
375     # Semi-shamelessly copied from make-etc.sh. I (@copumpkin) shall factor this stuff out as part of
376     # https://github.com/NixOS/nixpkgs/issues/23052.
377     set -f
378     sources_=(${concatStringsSep " " sources})
379     targets_=(${concatStringsSep " " targets})
380     modes_=(${concatStringsSep " " modes})
381     set +f
383     for ((i = 0; i < ''${#targets_[@]}; i++)); do
384       source="''${sources_[$i]}"
385       target="''${targets_[$i]}"
386       mode="''${modes_[$i]}"
388       if [ -n "$mode" ]; then
389         rsync_chmod_flags="--chmod=$mode"
390       else
391         rsync_chmod_flags=""
392       fi
393       # Unfortunately cptofs only supports modes, not ownership, so we can't use
394       # rsync's --chown option. Instead, we change the ownerships in the
395       # VM script with chown.
396       rsync_flags="-a --no-o --no-g $rsync_chmod_flags"
397       if [[ "$source" =~ '*' ]]; then
398         # If the source name contains '*', perform globbing.
399         mkdir -p $root/$target
400         for fn in $source; do
401           rsync $rsync_flags "$fn" $root/$target/
402         done
403       else
404         mkdir -p $root/$(dirname $target)
405         if [ -e $root/$target ]; then
406           echo "duplicate entry $target -> $source"
407           exit 1
408         elif [ -d $source ]; then
409           # Append a slash to the end of source to get rsync to copy the
410           # directory _to_ the target instead of _inside_ the target.
411           # (See `man rsync`'s note on a trailing slash.)
412           rsync $rsync_flags $source/ $root/$target
413         else
414           rsync $rsync_flags $source $root/$target
415         fi
416       fi
417     done
419     export HOME=$TMPDIR
421     # Provide a Nix database so that nixos-install can copy closures.
422     export NIX_STATE_DIR=$TMPDIR/state
423     nix-store --load-db < ${closureInfo}/registration
425     chmod 755 "$TMPDIR"
426     echo "running nixos-install..."
427     nixos-install --root $root --no-bootloader --no-root-passwd \
428       --system ${config.system.build.toplevel} \
429       ${if copyChannel then "--channel ${channelSources}" else "--no-channel-copy"} \
430       --substituters ""
432     ${optionalString (additionalPaths' != []) ''
433       nix --extra-experimental-features nix-command copy --to $root --no-check-sigs ${concatStringsSep " " additionalPaths'}
434     ''}
436     diskImage=nixos.raw
438     ${if diskSize == "auto" then ''
439       ${if partitionTableType == "efi" || partitionTableType == "hybrid" then ''
440         # Add the GPT at the end
441         gptSpace=$(( 512 * 34 * 1 ))
442         # Normally we'd need to account for alignment and things, if bootSize
443         # represented the actual size of the boot partition. But it instead
444         # represents the offset at which it ends.
445         # So we know bootSize is the reserved space in front of the partition.
446         reservedSpace=$(( gptSpace + $(numfmt --from=iec '${bootSize}') ))
447       '' else if partitionTableType == "legacy+gpt" then ''
448         # Add the GPT at the end
449         gptSpace=$(( 512 * 34 * 1 ))
450         # And include the bios_grub partition; the ext4 partition starts at 2MB exactly.
451         reservedSpace=$(( gptSpace + 2 * mebibyte ))
452       '' else if partitionTableType == "legacy" then ''
453         # Add the 1MiB aligned reserved space (includes MBR)
454         reservedSpace=$(( mebibyte ))
455       '' else ''
456         reservedSpace=0
457       ''}
458       additionalSpace=$(( $(numfmt --from=iec '${additionalSpace}') + reservedSpace ))
460       # Compute required space in filesystem blocks
461       diskUsage=$(find . ! -type d -print0 | du --files0-from=- --apparent-size --block-size "${blockSize}" | cut -f1 | sum_lines)
462       # Each inode takes space!
463       numInodes=$(find . | wc -l)
464       # Convert to bytes, inodes take two blocks each!
465       diskUsage=$(( (diskUsage + 2 * numInodes) * ${blockSize} ))
466       # Then increase the required space to account for the reserved blocks.
467       fudge=$(compute_fudge $diskUsage)
468       requiredFilesystemSpace=$(( diskUsage + fudge ))
470       diskSize=$(( requiredFilesystemSpace  + additionalSpace ))
472       # Round up to the nearest mebibyte.
473       # This ensures whole 512 bytes sector sizes in the disk image
474       # and helps towards aligning partitions optimally.
475       if (( diskSize % mebibyte )); then
476         diskSize=$(( ( diskSize / mebibyte + 1) * mebibyte ))
477       fi
479       truncate -s "$diskSize" $diskImage
481       printf "Automatic disk size...\n"
482       printf "  Closure space use: %d bytes\n" $diskUsage
483       printf "  fudge: %d bytes\n" $fudge
484       printf "  Filesystem size needed: %d bytes\n" $requiredFilesystemSpace
485       printf "  Additional space: %d bytes\n" $additionalSpace
486       printf "  Disk image size: %d bytes\n" $diskSize
487     '' else ''
488       truncate -s ${toString diskSize}M $diskImage
489     ''}
491     ${partitionDiskScript}
493     ${if partitionTableType != "none" then ''
494       # Get start & length of the root partition in sectors to $START and $SECTORS.
495       eval $(partx $diskImage -o START,SECTORS --nr ${rootPartition} --pairs)
497       mkfs.${fsType} -b ${blockSize} -F -L ${label} $diskImage -E offset=$(sectorsToBytes $START) $(sectorsToKilobytes $SECTORS)K
498     '' else ''
499       mkfs.${fsType} -b ${blockSize} -F -L ${label} $diskImage
500     ''}
502     echo "copying staging root to image..."
503     cptofs -p ${optionalString (partitionTableType != "none") "-P ${rootPartition}"} \
504            -t ${fsType} \
505            -i $diskImage \
506            $root${optionalString onlyNixStore builtins.storeDir}/* / ||
507       (echo >&2 "ERROR: cptofs failed. diskSize might be too small for closure."; exit 1)
508   '';
510   moveOrConvertImage = ''
511     ${if format == "raw" then ''
512       mv $diskImage $out/${filename}
513     '' else ''
514       ${pkgs.qemu-utils}/bin/qemu-img convert -f raw -O ${format} ${compress} $diskImage $out/${filename}
515     ''}
516     diskImage=$out/${filename}
517   '';
519   createEFIVars = ''
520     efiVars=$out/efi-vars.fd
521     cp ${efiVariables} $efiVars
522     chmod 0644 $efiVars
523   '';
525   buildImage = pkgs.vmTools.runInLinuxVM (
526     pkgs.runCommand name {
527       preVM = prepareImage + lib.optionalString touchEFIVars createEFIVars;
528       buildInputs = with pkgs; [ util-linux e2fsprogs dosfstools ];
529       postVM = moveOrConvertImage + postVM;
530       QEMU_OPTS =
531         concatStringsSep " " (lib.optional useEFIBoot "-drive if=pflash,format=raw,unit=0,readonly=on,file=${efiFirmware}"
532         ++ lib.optionals touchEFIVars [
533           "-drive if=pflash,format=raw,unit=1,file=$efiVars"
534         ]
535       );
536       inherit memSize;
537     } ''
538       export PATH=${binPath}:$PATH
540       rootDisk=${if partitionTableType != "none" then "/dev/vda${rootPartition}" else "/dev/vda"}
542       # It is necessary to set root filesystem unique identifier in advance, otherwise
543       # bootloader might get the wrong one and fail to boot.
544       # At the end, we reset again because we want deterministic timestamps.
545       ${optionalString (fsType == "ext4" && deterministic) ''
546         tune2fs -T now ${optionalString deterministic "-U ${rootFSUID}"} -c 0 -i 0 $rootDisk
547       ''}
548       # make systemd-boot find ESP without udev
549       mkdir /dev/block
550       ln -s /dev/vda1 /dev/block/254:1
552       mountPoint=/mnt
553       mkdir $mountPoint
554       mount $rootDisk $mountPoint
556       # Create the ESP and mount it. Unlike e2fsprogs, mkfs.vfat doesn't support an
557       # '-E offset=X' option, so we can't do this outside the VM.
558       ${optionalString (partitionTableType == "efi" || partitionTableType == "hybrid") ''
559         mkdir -p /mnt/boot
560         mkfs.vfat -n ESP /dev/vda1
561         mount /dev/vda1 /mnt/boot
563         ${optionalString touchEFIVars "mount -t efivarfs efivarfs /sys/firmware/efi/efivars"}
564       ''}
566       # Install a configuration.nix
567       mkdir -p /mnt/etc/nixos
568       ${optionalString (configFile != null) ''
569         cp ${configFile} /mnt/etc/nixos/configuration.nix
570       ''}
572       ${lib.optionalString installBootLoader ''
573         # In this throwaway resource, we only have /dev/vda, but the actual VM may refer to another disk for bootloader, e.g. /dev/vdb
574         # Use this option to create a symlink from vda to any arbitrary device you want.
575         ${optionalString (config.boot.loader.grub.enable && config.boot.loader.grub.device != "/dev/vda") ''
576             mkdir -p $(dirname ${config.boot.loader.grub.device})
577             ln -s /dev/vda ${config.boot.loader.grub.device}
578         ''}
580         # Set up core system link, bootloader (sd-boot, GRUB, uboot, etc.), etc.
581         NIXOS_INSTALL_BOOTLOADER=1 nixos-enter --root $mountPoint -- /nix/var/nix/profiles/system/bin/switch-to-configuration boot
583         # The above scripts will generate a random machine-id and we don't want to bake a single ID into all our images
584         rm -f $mountPoint/etc/machine-id
585       ''}
587       # Set the ownerships of the contents. The modes are set in preVM.
588       # No globbing on targets, so no need to set -f
589       targets_=(${concatStringsSep " " targets})
590       users_=(${concatStringsSep " " users})
591       groups_=(${concatStringsSep " " groups})
592       for ((i = 0; i < ''${#targets_[@]}; i++)); do
593         target="''${targets_[$i]}"
594         user="''${users_[$i]}"
595         group="''${groups_[$i]}"
596         if [ -n "$user$group" ]; then
597           # We have to nixos-enter since we need to use the user and group of the VM
598           nixos-enter --root $mountPoint -- chown -R "$user:$group" "$target"
599         fi
600       done
602       umount -R /mnt
604       # Make sure resize2fs works. Note that resize2fs has stricter criteria for resizing than a normal
605       # mount, so the `-c 0` and `-i 0` don't affect it. Setting it to `now` doesn't produce deterministic
606       # output, of course, but we can fix that when/if we start making images deterministic.
607       # In deterministic mode, this is fixed to 1970-01-01 (UNIX timestamp 0).
608       # This two-step approach is necessary otherwise `tune2fs` will want a fresher filesystem to perform
609       # some changes.
610       ${optionalString (fsType == "ext4") ''
611         tune2fs -T now ${optionalString deterministic "-U ${rootFSUID}"} -c 0 -i 0 $rootDisk
612         ${optionalString deterministic "tune2fs -f -T 19700101 $rootDisk"}
613       ''}
614     ''
615   );
617   if onlyNixStore then
618     pkgs.runCommand name {}
619       (prepareImage + moveOrConvertImage + postVM)
620   else buildImage