at master 30 kB view raw
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