nixos/systemd-boot: Simpler windows dual booting (#344327)

authored by

Atemu and committed by
GitHub
12ef18d2 8d4e0861

+592 -271
+2
nixos/doc/manual/release-notes/rl-2411.section.md
··· 631 631 The derivation now installs "impl" headers selectively instead of by a wildcard. 632 632 Use `imgui.src` if you just want to access the unpacked sources. 633 633 634 + - The new `boot.loader.systemd-boot.windows` option makes setting up dual-booting with Windows on a different drive easier 635 + 634 636 - Linux 4.19 has been removed because it will reach its end of life within the lifespan of 24.11 635 637 636 638 - Unprivileged access to the kernel syslog via `dmesg` is now restricted by default. Users wanting to keep an
+292 -91
nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix
··· 1 - { config, lib, pkgs, ... }: 1 + { 2 + config, 3 + lib, 4 + pkgs, 5 + ... 6 + }: 2 7 3 8 with lib; 4 9 ··· 10 15 # We check the source code in a derivation that does not depend on the 11 16 # system configuration so that most users don't have to redo the check and require 12 17 # the necessary dependencies. 13 - checkedSource = pkgs.runCommand "systemd-boot" { 14 - preferLocalBuild = true; 15 - } '' 16 - install -m755 -D ${./systemd-boot-builder.py} $out 17 - ${lib.getExe pkgs.buildPackages.mypy} \ 18 - --no-implicit-optional \ 19 - --disallow-untyped-calls \ 20 - --disallow-untyped-defs \ 21 - $out 22 - ''; 18 + checkedSource = 19 + pkgs.runCommand "systemd-boot" 20 + { 21 + preferLocalBuild = true; 22 + } 23 + '' 24 + install -m755 -D ${./systemd-boot-builder.py} $out 25 + ${lib.getExe pkgs.buildPackages.mypy} \ 26 + --no-implicit-optional \ 27 + --disallow-untyped-calls \ 28 + --disallow-untyped-defs \ 29 + $out 30 + ''; 31 + 32 + edk2ShellEspPath = "efi/edk2-uefi-shell/shell.efi"; 23 33 24 34 systemdBootBuilder = pkgs.substituteAll rec { 25 35 name = "systemd-boot"; ··· 44 54 45 55 configurationLimit = if cfg.configurationLimit == null then 0 else cfg.configurationLimit; 46 56 47 - inherit (cfg) consoleMode graceful editor rebootForBitlocker; 57 + inherit (cfg) 58 + consoleMode 59 + graceful 60 + editor 61 + rebootForBitlocker 62 + ; 48 63 49 64 inherit (efi) efiSysMountPoint canTouchEfiVariables; 50 65 51 - bootMountPoint = if cfg.xbootldrMountPoint != null 52 - then cfg.xbootldrMountPoint 53 - else efi.efiSysMountPoint; 66 + bootMountPoint = 67 + if cfg.xbootldrMountPoint != null then cfg.xbootldrMountPoint else efi.efiSysMountPoint; 54 68 55 69 nixosDir = "/EFI/nixos"; 56 70 ··· 60 74 61 75 netbootxyz = optionalString cfg.netbootxyz.enable pkgs.netbootxyz-efi; 62 76 77 + edk2-uefi-shell = optionalString cfg.edk2-uefi-shell.enable pkgs.edk2-uefi-shell; 78 + 63 79 checkMountpoints = pkgs.writeShellScript "check-mountpoints" '' 64 80 fail() { 65 81 echo "$1 = '$2' is not a mounted partition. Is the path configured correctly?" >&2 66 82 exit 1 67 83 } 68 84 ${pkgs.util-linuxMinimal}/bin/findmnt ${efiSysMountPoint} > /dev/null || fail efiSysMountPoint ${efiSysMountPoint} 69 - ${lib.optionalString 70 - (cfg.xbootldrMountPoint != null) 71 - "${pkgs.util-linuxMinimal}/bin/findmnt ${cfg.xbootldrMountPoint} > /dev/null || fail xbootldrMountPoint ${cfg.xbootldrMountPoint}"} 85 + ${lib.optionalString (cfg.xbootldrMountPoint != null) 86 + "${pkgs.util-linuxMinimal}/bin/findmnt ${cfg.xbootldrMountPoint} > /dev/null || fail xbootldrMountPoint ${cfg.xbootldrMountPoint}" 87 + } 72 88 ''; 73 89 74 90 copyExtraFiles = pkgs.writeShellScript "copy-extra-files" '' 75 91 empty_file=$(${pkgs.coreutils}/bin/mktemp) 76 92 77 - ${concatStrings (mapAttrsToList (n: v: '' 78 - ${pkgs.coreutils}/bin/install -Dp "${v}" "${bootMountPoint}/"${escapeShellArg n} 79 - ${pkgs.coreutils}/bin/install -D $empty_file "${bootMountPoint}/${nixosDir}/.extra-files/"${escapeShellArg n} 80 - '') cfg.extraFiles)} 93 + ${concatStrings ( 94 + mapAttrsToList (n: v: '' 95 + ${pkgs.coreutils}/bin/install -Dp "${v}" "${bootMountPoint}/"${escapeShellArg n} 96 + ${pkgs.coreutils}/bin/install -D $empty_file "${bootMountPoint}/${nixosDir}/.extra-files/"${escapeShellArg n} 97 + '') cfg.extraFiles 98 + )} 81 99 82 - ${concatStrings (mapAttrsToList (n: v: '' 83 - ${pkgs.coreutils}/bin/install -Dp "${pkgs.writeText n v}" "${bootMountPoint}/loader/entries/"${escapeShellArg n} 84 - ${pkgs.coreutils}/bin/install -D $empty_file "${bootMountPoint}/${nixosDir}/.extra-files/loader/entries/"${escapeShellArg n} 85 - '') cfg.extraEntries)} 100 + ${concatStrings ( 101 + mapAttrsToList (n: v: '' 102 + ${pkgs.coreutils}/bin/install -Dp "${pkgs.writeText n v}" "${bootMountPoint}/loader/entries/"${escapeShellArg n} 103 + ${pkgs.coreutils}/bin/install -D $empty_file "${bootMountPoint}/${nixosDir}/.extra-files/loader/entries/"${escapeShellArg n} 104 + '') cfg.extraEntries 105 + )} 86 106 ''; 87 107 }; 88 108 ··· 91 111 ${systemdBootBuilder}/bin/systemd-boot "$@" 92 112 ${cfg.extraInstallCommands} 93 113 ''; 94 - in { 114 + in 115 + { 95 116 96 117 meta.maintainers = with lib.maintainers; [ julienmalka ]; 97 118 98 - imports = 99 - [ (mkRenamedOptionModule [ "boot" "loader" "gummiboot" "enable" ] [ "boot" "loader" "systemd-boot" "enable" ]) 100 - (lib.mkChangedOptionModule 101 - [ "boot" "loader" "systemd-boot" "memtest86" "entryFilename" ] 102 - [ "boot" "loader" "systemd-boot" "memtest86" "sortKey" ] 103 - (config: lib.strings.removeSuffix ".conf" config.boot.loader.systemd-boot.memtest86.entryFilename) 104 - ) 105 - (lib.mkChangedOptionModule 106 - [ "boot" "loader" "systemd-boot" "netbootxyz" "entryFilename" ] 107 - [ "boot" "loader" "systemd-boot" "netbootxyz" "sortKey" ] 108 - (config: lib.strings.removeSuffix ".conf" config.boot.loader.systemd-boot.netbootxyz.entryFilename) 109 - ) 110 - ]; 119 + imports = [ 120 + (mkRenamedOptionModule 121 + [ 122 + "boot" 123 + "loader" 124 + "gummiboot" 125 + "enable" 126 + ] 127 + [ 128 + "boot" 129 + "loader" 130 + "systemd-boot" 131 + "enable" 132 + ] 133 + ) 134 + (lib.mkChangedOptionModule 135 + [ 136 + "boot" 137 + "loader" 138 + "systemd-boot" 139 + "memtest86" 140 + "entryFilename" 141 + ] 142 + [ 143 + "boot" 144 + "loader" 145 + "systemd-boot" 146 + "memtest86" 147 + "sortKey" 148 + ] 149 + (config: lib.strings.removeSuffix ".conf" config.boot.loader.systemd-boot.memtest86.entryFilename) 150 + ) 151 + (lib.mkChangedOptionModule 152 + [ 153 + "boot" 154 + "loader" 155 + "systemd-boot" 156 + "netbootxyz" 157 + "entryFilename" 158 + ] 159 + [ 160 + "boot" 161 + "loader" 162 + "systemd-boot" 163 + "netbootxyz" 164 + "sortKey" 165 + ] 166 + (config: lib.strings.removeSuffix ".conf" config.boot.loader.systemd-boot.netbootxyz.entryFilename) 167 + ) 168 + ]; 111 169 112 170 options.boot.loader.systemd-boot = { 113 171 enable = mkOption { ··· 124 182 125 183 sortKey = mkOption { 126 184 default = "nixos"; 127 - type = lib.types.str; 185 + type = types.str; 128 186 description = '' 129 187 The sort key used for the NixOS bootloader entries. 130 188 This key determines sorting relative to non-NixOS entries. ··· 218 276 consoleMode = mkOption { 219 277 default = "keep"; 220 278 221 - type = types.enum [ "0" "1" "2" "5" "auto" "max" "keep" ]; 279 + type = types.enum [ 280 + "0" 281 + "1" 282 + "2" 283 + "5" 284 + "auto" 285 + "max" 286 + "keep" 287 + ]; 222 288 223 289 description = '' 224 290 The resolution of the console. The following values are valid: ··· 281 347 }; 282 348 }; 283 349 350 + edk2-uefi-shell = { 351 + enable = mkOption { 352 + type = types.bool; 353 + default = false; 354 + description = '' 355 + Make the EDK2 UEFI Shell available from the systemd-boot menu. 356 + It can be used to manually boot other operating systems or for debugging. 357 + ''; 358 + }; 359 + 360 + sortKey = mkOption { 361 + type = types.str; 362 + default = "o_edk2-uefi-shell"; 363 + description = '' 364 + `systemd-boot` orders the menu entries by their sort keys, 365 + so if you want something to appear after all the NixOS entries, 366 + it should start with {file}`o` or onwards. 367 + 368 + See also {option}`boot.loader.systemd-boot.sortKey`.. 369 + ''; 370 + }; 371 + }; 372 + 284 373 extraEntries = mkOption { 285 374 type = types.attrsOf types.lines; 286 - default = {}; 375 + default = { }; 287 376 example = literalExpression '' 288 377 { "memtest86.conf" = ''' 289 378 title Memtest86+ ··· 306 395 307 396 extraFiles = mkOption { 308 397 type = types.attrsOf types.path; 309 - default = {}; 398 + default = { }; 310 399 example = literalExpression '' 311 400 { "efi/memtest86/memtest.efi" = "''${pkgs.memtest86plus}/memtest.efi"; } 312 401 ''; ··· 349 438 Windows can unseal the encryption key. 350 439 ''; 351 440 }; 441 + 442 + windows = mkOption { 443 + default = { }; 444 + description = '' 445 + Make Windows bootable from systemd-boot. This option is not necessary when Windows and 446 + NixOS use the same EFI System Partition (ESP). In that case, Windows will automatically be 447 + detected by systemd-boot. 448 + 449 + However, if Windows is installed on a separate drive or ESP, you can use this option to add 450 + a menu entry for each installation manually. 451 + 452 + The attribute name is used for the title of the menu entry and internal file names. 453 + ''; 454 + example = literalExpression '' 455 + { 456 + "10".efiDeviceHandle = "HD0c3"; 457 + "11-ame" = { 458 + title = "Windows 11 Ameliorated Edition"; 459 + efiDeviceHandle = "HD0b1"; 460 + }; 461 + "11-home" = { 462 + title = "Windows 11 Home"; 463 + efiDeviceHandle = "FS1"; 464 + sortKey = "z_windows"; 465 + }; 466 + } 467 + ''; 468 + type = types.attrsOf ( 469 + types.submodule ( 470 + { name, ... }: 471 + { 472 + options = { 473 + efiDeviceHandle = mkOption { 474 + type = types.str; 475 + example = "HD1b3"; 476 + description = '' 477 + The device handle of the EFI System Partition (ESP) where the Windows bootloader is 478 + located. This is the device handle that the EDK2 UEFI Shell uses to load the 479 + bootloader. 480 + 481 + To find this handle, follow these steps: 482 + 1. Set {option}`boot.loader.systemd-boot.edk2-uefi-shell.enable` to `true` 483 + 2. Run `nixos-rebuild boot` 484 + 3. Reboot and select "EDK2 UEFI Shell" from the systemd-boot menu 485 + 4. Run `map -c` to list all consistent device handles 486 + 5. For each device handle (for example, `HD0c1`), run `ls HD0c1:\EFI` 487 + 6. If the output contains the directory `Microsoft`, you might have found the correct device handle 488 + 7. Run `HD0c1:\EFI\Microsoft\Boot\Bootmgfw.efi` to check if Windows boots correctly 489 + 8. If it does, this device handle is the one you need (in this example, `HD0c1`) 490 + 491 + This option is required, there is no useful default. 492 + ''; 493 + }; 494 + 495 + title = mkOption { 496 + type = types.str; 497 + example = "Michaelsoft Binbows"; 498 + default = "Windows ${name}"; 499 + defaultText = ''attribute name of this entry, prefixed with "Windows "''; 500 + description = '' 501 + The title of the boot menu entry. 502 + ''; 503 + }; 504 + 505 + sortKey = mkOption { 506 + type = types.str; 507 + default = "o_windows_${name}"; 508 + defaultText = ''attribute name of this entry, prefixed with "o_windows_"''; 509 + description = '' 510 + `systemd-boot` orders the menu entries by their sort keys, 511 + so if you want something to appear after all the NixOS entries, 512 + it should start with {file}`o` or onwards. 513 + 514 + See also {option}`boot.loader.systemd-boot.sortKey`.. 515 + ''; 516 + }; 517 + }; 518 + } 519 + ) 520 + ); 521 + }; 352 522 }; 353 523 354 524 config = mkIf cfg.enable { 355 - assertions = [ 356 - { 357 - assertion = (hasPrefix "/" efi.efiSysMountPoint); 358 - message = "The ESP mount point '${toString efi.efiSysMountPoint}' must be an absolute path"; 359 - } 360 - { 361 - assertion = cfg.xbootldrMountPoint == null || (hasPrefix "/" cfg.xbootldrMountPoint); 362 - message = "The XBOOTLDR mount point '${toString cfg.xbootldrMountPoint}' must be an absolute path"; 363 - } 364 - { 365 - assertion = cfg.xbootldrMountPoint != efi.efiSysMountPoint; 366 - message = "The XBOOTLDR mount point '${toString cfg.xbootldrMountPoint}' cannot be the same as the ESP mount point '${toString efi.efiSysMountPoint}'"; 367 - } 368 - { 369 - assertion = (config.boot.kernelPackages.kernel.features or { efiBootStub = true; }) ? efiBootStub; 370 - message = "This kernel does not support the EFI boot stub"; 371 - } 372 - { 373 - assertion = cfg.installDeviceTree -> config.hardware.deviceTree.enable -> config.hardware.deviceTree.name != null; 374 - message = "Cannot install devicetree without 'config.hardware.deviceTree.enable' enabled and 'config.hardware.deviceTree.name' set"; 375 - } 376 - ] ++ concatMap (filename: [ 377 - { 378 - assertion = !(hasInfix "/" filename); 379 - message = "boot.loader.systemd-boot.extraEntries.${lib.strings.escapeNixIdentifier filename} is invalid: entries within folders are not supported"; 380 - } 381 - { 382 - assertion = hasSuffix ".conf" filename; 383 - message = "boot.loader.systemd-boot.extraEntries.${lib.strings.escapeNixIdentifier filename} is invalid: entries must have a .conf file extension"; 384 - } 385 - ]) (builtins.attrNames cfg.extraEntries) 525 + assertions = 526 + [ 527 + { 528 + assertion = (hasPrefix "/" efi.efiSysMountPoint); 529 + message = "The ESP mount point '${toString efi.efiSysMountPoint}' must be an absolute path"; 530 + } 531 + { 532 + assertion = cfg.xbootldrMountPoint == null || (hasPrefix "/" cfg.xbootldrMountPoint); 533 + message = "The XBOOTLDR mount point '${toString cfg.xbootldrMountPoint}' must be an absolute path"; 534 + } 535 + { 536 + assertion = cfg.xbootldrMountPoint != efi.efiSysMountPoint; 537 + message = "The XBOOTLDR mount point '${toString cfg.xbootldrMountPoint}' cannot be the same as the ESP mount point '${toString efi.efiSysMountPoint}'"; 538 + } 539 + { 540 + assertion = (config.boot.kernelPackages.kernel.features or { efiBootStub = true; }) ? efiBootStub; 541 + message = "This kernel does not support the EFI boot stub"; 542 + } 543 + { 544 + assertion = 545 + cfg.installDeviceTree 546 + -> config.hardware.deviceTree.enable 547 + -> config.hardware.deviceTree.name != null; 548 + message = "Cannot install devicetree without 'config.hardware.deviceTree.enable' enabled and 'config.hardware.deviceTree.name' set"; 549 + } 550 + ] 551 + ++ concatMap (filename: [ 552 + { 553 + assertion = !(hasInfix "/" filename); 554 + message = "boot.loader.systemd-boot.extraEntries.${lib.strings.escapeNixIdentifier filename} is invalid: entries within folders are not supported"; 555 + } 556 + { 557 + assertion = hasSuffix ".conf" filename; 558 + message = "boot.loader.systemd-boot.extraEntries.${lib.strings.escapeNixIdentifier filename} is invalid: entries must have a .conf file extension"; 559 + } 560 + ]) (builtins.attrNames cfg.extraEntries) 386 561 ++ concatMap (filename: [ 387 562 { 388 563 assertion = !(hasPrefix "/" filename); ··· 396 571 assertion = !(hasInfix "nixos/.extra-files" (toLower filename)); 397 572 message = "boot.loader.systemd-boot.extraFiles.${lib.strings.escapeNixIdentifier filename} is invalid: files cannot be placed in the nixos/.extra-files directory"; 398 573 } 399 - ]) (builtins.attrNames cfg.extraFiles); 574 + ]) (builtins.attrNames cfg.extraFiles) 575 + ++ concatMap (winVersion: [ 576 + { 577 + assertion = lib.match "^[-_0-9A-Za-z]+$" winVersion != null; 578 + message = "boot.loader.systemd-boot.windows.${winVersion} is invalid: key must only contain alphanumeric characters, hyphens, and underscores"; 579 + } 580 + ]) (builtins.attrNames cfg.windows); 400 581 401 582 boot.loader.grub.enable = mkDefault false; 402 583 ··· 408 589 }) 409 590 (mkIf cfg.netbootxyz.enable { 410 591 "efi/netbootxyz/netboot.xyz.efi" = "${pkgs.netbootxyz-efi}"; 592 + }) 593 + (mkIf (cfg.edk2-uefi-shell.enable || cfg.windows != { }) { 594 + ${edk2ShellEspPath} = "${pkgs.edk2-uefi-shell}/shell.efi"; 411 595 }) 412 596 ]; 413 597 414 - boot.loader.systemd-boot.extraEntries = mkMerge [ 415 - (mkIf cfg.memtest86.enable { 416 - "memtest86.conf" = '' 417 - title Memtest86+ 418 - efi /efi/memtest86/memtest.efi 419 - sort-key ${cfg.memtest86.sortKey} 598 + boot.loader.systemd-boot.extraEntries = mkMerge ( 599 + [ 600 + (mkIf cfg.memtest86.enable { 601 + "memtest86.conf" = '' 602 + title Memtest86+ 603 + efi /efi/memtest86/memtest.efi 604 + sort-key ${cfg.memtest86.sortKey} 605 + ''; 606 + }) 607 + (mkIf cfg.netbootxyz.enable { 608 + "netbootxyz.conf" = '' 609 + title netboot.xyz 610 + efi /efi/netbootxyz/netboot.xyz.efi 611 + sort-key ${cfg.netbootxyz.sortKey} 612 + ''; 613 + }) 614 + (mkIf cfg.edk2-uefi-shell.enable { 615 + "edk2-uefi-shell.conf" = '' 616 + title EDK2 UEFI Shell 617 + efi /${edk2ShellEspPath} 618 + sort-key ${cfg.edk2-uefi-shell.sortKey} 619 + ''; 620 + }) 621 + ] 622 + ++ (mapAttrsToList (winVersion: cfg: { 623 + "windows_${winVersion}.conf" = '' 624 + title ${cfg.title} 625 + efi /${edk2ShellEspPath} 626 + options -nointerrupt -nomap -noversion ${cfg.efiDeviceHandle}:EFI\Microsoft\Boot\Bootmgfw.efi 627 + sort-key ${cfg.sortKey} 420 628 ''; 421 - }) 422 - (mkIf cfg.netbootxyz.enable { 423 - "netbootxyz.conf" = '' 424 - title netboot.xyz 425 - efi /efi/netbootxyz/netboot.xyz.efi 426 - sort-key ${cfg.netbootxyz.sortKey} 427 - ''; 428 - }) 429 - ]; 629 + }) cfg.windows) 630 + ); 430 631 431 632 boot.bootspec.extensions."org.nixos.systemd-boot" = { 432 633 inherit (config.boot.loader.systemd-boot) sortKey;
+298 -180
nixos/tests/systemd-boot.nix
··· 1 - { system ? builtins.currentSystem, 2 - config ? {}, 3 - pkgs ? import ../.. { inherit system config; } 1 + { 2 + system ? builtins.currentSystem, 3 + config ? { }, 4 + pkgs ? import ../.. { inherit system config; }, 4 5 }: 5 6 6 7 with import ../lib/testing-python.nix { inherit system pkgs; }; ··· 16 17 system.switch.enable = true; 17 18 }; 18 19 19 - commonXbootldr = { config, lib, pkgs, ... }: 20 + commonXbootldr = 21 + { 22 + config, 23 + lib, 24 + pkgs, 25 + ... 26 + }: 20 27 let 21 28 diskImage = import ../lib/make-disk-image.nix { 22 29 inherit config lib pkgs; ··· 85 92 { 86 93 basic = makeTest { 87 94 name = "systemd-boot"; 88 - meta.maintainers = with pkgs.lib.maintainers; [ danielfullmer julienmalka ]; 95 + meta.maintainers = with pkgs.lib.maintainers; [ 96 + danielfullmer 97 + julienmalka 98 + ]; 89 99 90 100 nodes.machine = common; 91 101 ··· 117 127 virtualisation.useSecureBoot = true; 118 128 }; 119 129 120 - testScript = let 121 - efiArch = pkgs.stdenv.hostPlatform.efiArch; 122 - in { nodes, ... }: '' 123 - machine.start(allow_reboot=True) 124 - machine.wait_for_unit("multi-user.target") 130 + testScript = 131 + let 132 + efiArch = pkgs.stdenv.hostPlatform.efiArch; 133 + in 134 + { nodes, ... }: 135 + '' 136 + machine.start(allow_reboot=True) 137 + machine.wait_for_unit("multi-user.target") 125 138 126 - machine.succeed("sbctl create-keys") 127 - machine.succeed("sbctl enroll-keys --yes-this-might-brick-my-machine") 128 - machine.succeed('sbctl sign /boot/EFI/systemd/systemd-boot${efiArch}.efi') 129 - machine.succeed('sbctl sign /boot/EFI/BOOT/BOOT${toUpper efiArch}.EFI') 130 - machine.succeed('sbctl sign /boot/EFI/nixos/*${nodes.machine.system.boot.loader.kernelFile}.efi') 139 + machine.succeed("sbctl create-keys") 140 + machine.succeed("sbctl enroll-keys --yes-this-might-brick-my-machine") 141 + machine.succeed('sbctl sign /boot/EFI/systemd/systemd-boot${efiArch}.efi') 142 + machine.succeed('sbctl sign /boot/EFI/BOOT/BOOT${toUpper efiArch}.EFI') 143 + machine.succeed('sbctl sign /boot/EFI/nixos/*${nodes.machine.system.boot.loader.kernelFile}.efi') 131 144 132 - machine.reboot() 145 + machine.reboot() 133 146 134 - assert "Secure Boot: enabled (user)" in machine.succeed("bootctl status") 135 - ''; 147 + assert "Secure Boot: enabled (user)" in machine.succeed("bootctl status") 148 + ''; 136 149 }; 137 150 138 151 basicXbootldr = makeTest { ··· 141 154 142 155 nodes.machine = commonXbootldr; 143 156 144 - testScript = { nodes, ... }: '' 145 - ${customDiskImage nodes} 157 + testScript = 158 + { nodes, ... }: 159 + '' 160 + ${customDiskImage nodes} 146 161 147 - machine.start() 148 - machine.wait_for_unit("multi-user.target") 162 + machine.start() 163 + machine.wait_for_unit("multi-user.target") 149 164 150 - machine.succeed("test -e /efi/EFI/systemd/systemd-bootx64.efi") 151 - machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf") 165 + machine.succeed("test -e /efi/EFI/systemd/systemd-bootx64.efi") 166 + machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf") 152 167 153 - # Ensure we actually booted using systemd-boot 154 - # Magic number is the vendor UUID used by systemd-boot. 155 - machine.succeed( 156 - "test -e /sys/firmware/efi/efivars/LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f" 157 - ) 168 + # Ensure we actually booted using systemd-boot 169 + # Magic number is the vendor UUID used by systemd-boot. 170 + machine.succeed( 171 + "test -e /sys/firmware/efi/efivars/LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f" 172 + ) 158 173 159 - # "bootctl install" should have created an EFI entry 160 - machine.succeed('efibootmgr | grep "Linux Boot Manager"') 161 - ''; 174 + # "bootctl install" should have created an EFI entry 175 + machine.succeed('efibootmgr | grep "Linux Boot Manager"') 176 + ''; 162 177 }; 163 178 164 179 # Check that specialisations create corresponding boot entries. 165 180 specialisation = makeTest { 166 181 name = "systemd-boot-specialisation"; 167 - meta.maintainers = with pkgs.lib.maintainers; [ lukegb julienmalka ]; 182 + meta.maintainers = with pkgs.lib.maintainers; [ 183 + lukegb 184 + julienmalka 185 + ]; 168 186 169 - nodes.machine = { pkgs, lib, ... }: { 170 - imports = [ common ]; 171 - specialisation.something.configuration = { 172 - boot.loader.systemd-boot.sortKey = "something"; 187 + nodes.machine = 188 + { pkgs, lib, ... }: 189 + { 190 + imports = [ common ]; 191 + specialisation.something.configuration = { 192 + boot.loader.systemd-boot.sortKey = "something"; 173 193 174 - # Since qemu will dynamically create a devicetree blob when starting 175 - # up, it is not straight forward to create an export of that devicetree 176 - # blob without knowing before-hand all the flags we would pass to qemu 177 - # (we would then be able to use `dumpdtb`). Thus, the following config 178 - # will not boot, but it does allow us to assert that the boot entry has 179 - # the correct contents. 180 - boot.loader.systemd-boot.installDeviceTree = pkgs.stdenv.hostPlatform.isAarch64; 181 - hardware.deviceTree.name = "dummy.dtb"; 182 - hardware.deviceTree.package = lib.mkForce (pkgs.runCommand "dummy-devicetree-package" { } '' 183 - mkdir -p $out 184 - cp ${pkgs.emptyFile} $out/dummy.dtb 185 - ''); 194 + # Since qemu will dynamically create a devicetree blob when starting 195 + # up, it is not straight forward to create an export of that devicetree 196 + # blob without knowing before-hand all the flags we would pass to qemu 197 + # (we would then be able to use `dumpdtb`). Thus, the following config 198 + # will not boot, but it does allow us to assert that the boot entry has 199 + # the correct contents. 200 + boot.loader.systemd-boot.installDeviceTree = pkgs.stdenv.hostPlatform.isAarch64; 201 + hardware.deviceTree.name = "dummy.dtb"; 202 + hardware.deviceTree.package = lib.mkForce ( 203 + pkgs.runCommand "dummy-devicetree-package" { } '' 204 + mkdir -p $out 205 + cp ${pkgs.emptyFile} $out/dummy.dtb 206 + '' 207 + ); 208 + }; 186 209 }; 187 - }; 188 210 189 - testScript = { nodes, ... }: '' 190 - machine.start() 191 - machine.wait_for_unit("multi-user.target") 211 + testScript = 212 + { nodes, ... }: 213 + '' 214 + machine.start() 215 + machine.wait_for_unit("multi-user.target") 192 216 193 - machine.succeed( 194 - "test -e /boot/loader/entries/nixos-generation-1-specialisation-something.conf" 195 - ) 196 - machine.succeed( 197 - "grep -q 'title NixOS (something)' /boot/loader/entries/nixos-generation-1-specialisation-something.conf" 198 - ) 199 - machine.succeed( 200 - "grep 'sort-key something' /boot/loader/entries/nixos-generation-1-specialisation-something.conf" 201 - ) 202 - '' + pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isAarch64 '' 203 - machine.succeed( 204 - r"grep 'devicetree /EFI/nixos/[a-z0-9]\{32\}.*dummy' /boot/loader/entries/nixos-generation-1-specialisation-something.conf" 205 - ) 206 - ''; 217 + machine.succeed( 218 + "test -e /boot/loader/entries/nixos-generation-1-specialisation-something.conf" 219 + ) 220 + machine.succeed( 221 + "grep -q 'title NixOS (something)' /boot/loader/entries/nixos-generation-1-specialisation-something.conf" 222 + ) 223 + machine.succeed( 224 + "grep 'sort-key something' /boot/loader/entries/nixos-generation-1-specialisation-something.conf" 225 + ) 226 + '' 227 + + pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isAarch64 '' 228 + machine.succeed( 229 + r"grep 'devicetree /EFI/nixos/[a-z0-9]\{32\}.*dummy' /boot/loader/entries/nixos-generation-1-specialisation-something.conf" 230 + ) 231 + ''; 207 232 }; 208 233 209 234 # Boot without having created an EFI entry--instead using default "/EFI/BOOT/BOOTX64.EFI" 210 235 fallback = makeTest { 211 236 name = "systemd-boot-fallback"; 212 - meta.maintainers = with pkgs.lib.maintainers; [ danielfullmer julienmalka ]; 237 + meta.maintainers = with pkgs.lib.maintainers; [ 238 + danielfullmer 239 + julienmalka 240 + ]; 213 241 214 - nodes.machine = { pkgs, lib, ... }: { 215 - imports = [ common ]; 216 - boot.loader.efi.canTouchEfiVariables = mkForce false; 217 - }; 242 + nodes.machine = 243 + { pkgs, lib, ... }: 244 + { 245 + imports = [ common ]; 246 + boot.loader.efi.canTouchEfiVariables = mkForce false; 247 + }; 218 248 219 249 testScript = '' 220 250 machine.start() ··· 235 265 236 266 update = makeTest { 237 267 name = "systemd-boot-update"; 238 - meta.maintainers = with pkgs.lib.maintainers; [ danielfullmer julienmalka ]; 268 + meta.maintainers = with pkgs.lib.maintainers; [ 269 + danielfullmer 270 + julienmalka 271 + ]; 239 272 240 273 nodes.machine = common; 241 274 ··· 270 303 ''; 271 304 }; 272 305 273 - memtest86 = with pkgs.lib; optionalAttrs (meta.availableOn { inherit system; } pkgs.memtest86plus) (makeTest { 274 - name = "systemd-boot-memtest86"; 275 - meta.maintainers = with maintainers; [ julienmalka ]; 306 + memtest86 = 307 + with pkgs.lib; 308 + optionalAttrs (meta.availableOn { inherit system; } pkgs.memtest86plus) (makeTest { 309 + name = "systemd-boot-memtest86"; 310 + meta.maintainers = with maintainers; [ julienmalka ]; 311 + 312 + nodes.machine = 313 + { pkgs, lib, ... }: 314 + { 315 + imports = [ common ]; 316 + boot.loader.systemd-boot.memtest86.enable = true; 317 + }; 318 + 319 + testScript = '' 320 + machine.succeed("test -e /boot/loader/entries/memtest86.conf") 321 + machine.succeed("test -e /boot/efi/memtest86/memtest.efi") 322 + ''; 323 + }); 324 + 325 + netbootxyz = makeTest { 326 + name = "systemd-boot-netbootxyz"; 327 + meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ]; 328 + 329 + nodes.machine = 330 + { pkgs, lib, ... }: 331 + { 332 + imports = [ common ]; 333 + boot.loader.systemd-boot.netbootxyz.enable = true; 334 + }; 335 + 336 + testScript = '' 337 + machine.succeed("test -e /boot/loader/entries/netbootxyz.conf") 338 + machine.succeed("test -e /boot/efi/netbootxyz/netboot.xyz.efi") 339 + ''; 340 + }; 341 + 342 + edk2-uefi-shell = makeTest { 343 + name = "systemd-boot-edk2-uefi-shell"; 344 + meta.maintainers = with pkgs.lib.maintainers; [ iFreilicht ]; 276 345 277 - nodes.machine = { pkgs, lib, ... }: { 346 + nodes.machine = { ... }: { 278 347 imports = [ common ]; 279 - boot.loader.systemd-boot.memtest86.enable = true; 348 + boot.loader.systemd-boot.edk2-uefi-shell.enable = true; 280 349 }; 281 350 282 351 testScript = '' 283 - machine.succeed("test -e /boot/loader/entries/memtest86.conf") 284 - machine.succeed("test -e /boot/efi/memtest86/memtest.efi") 352 + machine.succeed("test -e /boot/loader/entries/edk2-uefi-shell.conf") 353 + machine.succeed("test -e /boot/efi/edk2-uefi-shell/shell.efi") 285 354 ''; 286 - }); 355 + }; 287 356 288 - netbootxyz = makeTest { 289 - name = "systemd-boot-netbootxyz"; 290 - meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ]; 357 + windows = makeTest { 358 + name = "systemd-boot-windows"; 359 + meta.maintainers = with pkgs.lib.maintainers; [ iFreilicht ]; 291 360 292 - nodes.machine = { pkgs, lib, ... }: { 361 + nodes.machine = { ... }: { 293 362 imports = [ common ]; 294 - boot.loader.systemd-boot.netbootxyz.enable = true; 363 + boot.loader.systemd-boot.windows = { 364 + "7" = { 365 + efiDeviceHandle = "HD0c1"; 366 + sortKey = "before_all_others"; 367 + }; 368 + "Ten".efiDeviceHandle = "FS0"; 369 + "11" = { 370 + title = "Title with-_-punctuation ...?!"; 371 + efiDeviceHandle = "HD0d4"; 372 + sortKey = "zzz"; 373 + }; 374 + }; 295 375 }; 296 376 297 377 testScript = '' 298 - machine.succeed("test -e /boot/loader/entries/netbootxyz.conf") 299 - machine.succeed("test -e /boot/efi/netbootxyz/netboot.xyz.efi") 378 + machine.succeed("test -e /boot/efi/edk2-uefi-shell/shell.efi") 379 + 380 + machine.succeed("test -e /boot/loader/entries/windows_7.conf") 381 + machine.succeed("test -e /boot/loader/entries/windows_Ten.conf") 382 + machine.succeed("test -e /boot/loader/entries/windows_11.conf") 383 + 384 + machine.succeed("grep 'efi /efi/edk2-uefi-shell/shell.efi' /boot/loader/entries/windows_7.conf") 385 + machine.succeed("grep 'efi /efi/edk2-uefi-shell/shell.efi' /boot/loader/entries/windows_Ten.conf") 386 + machine.succeed("grep 'efi /efi/edk2-uefi-shell/shell.efi' /boot/loader/entries/windows_11.conf") 387 + 388 + machine.succeed("grep 'HD0c1:EFI\\\\Microsoft\\\\Boot\\\\Bootmgfw.efi' /boot/loader/entries/windows_7.conf") 389 + machine.succeed("grep 'FS0:EFI\\\\Microsoft\\\\Boot\\\\Bootmgfw.efi' /boot/loader/entries/windows_Ten.conf") 390 + machine.succeed("grep 'HD0d4:EFI\\\\Microsoft\\\\Boot\\\\Bootmgfw.efi' /boot/loader/entries/windows_11.conf") 391 + 392 + machine.succeed("grep 'sort-key before_all_others' /boot/loader/entries/windows_7.conf") 393 + machine.succeed("grep 'sort-key o_windows_Ten' /boot/loader/entries/windows_Ten.conf") 394 + machine.succeed("grep 'sort-key zzz' /boot/loader/entries/windows_11.conf") 395 + 396 + machine.succeed("grep 'title Windows 7' /boot/loader/entries/windows_7.conf") 397 + machine.succeed("grep 'title Windows Ten' /boot/loader/entries/windows_Ten.conf") 398 + machine.succeed('grep "title Title with-_-punctuation ...?!" /boot/loader/entries/windows_11.conf') 300 399 ''; 301 400 }; 302 401 ··· 304 403 name = "systemd-boot-memtest-sortkey"; 305 404 meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ]; 306 405 307 - nodes.machine = { pkgs, lib, ... }: { 308 - imports = [ common ]; 309 - boot.loader.systemd-boot.memtest86.enable = true; 310 - boot.loader.systemd-boot.memtest86.sortKey = "apple"; 311 - }; 406 + nodes.machine = 407 + { pkgs, lib, ... }: 408 + { 409 + imports = [ common ]; 410 + boot.loader.systemd-boot.memtest86.enable = true; 411 + boot.loader.systemd-boot.memtest86.sortKey = "apple"; 412 + }; 312 413 313 414 testScript = '' 314 415 machine.succeed("test -e /boot/loader/entries/memtest86.conf") ··· 321 422 name = "systemd-boot-entry-filename-xbootldr"; 322 423 meta.maintainers = with pkgs.lib.maintainers; [ sdht0 ]; 323 424 324 - nodes.machine = { pkgs, lib, ... }: { 325 - imports = [ commonXbootldr ]; 326 - boot.loader.systemd-boot.memtest86.enable = true; 327 - }; 425 + nodes.machine = 426 + { pkgs, lib, ... }: 427 + { 428 + imports = [ commonXbootldr ]; 429 + boot.loader.systemd-boot.memtest86.enable = true; 430 + }; 328 431 329 - testScript = { nodes, ... }: '' 330 - ${customDiskImage nodes} 432 + testScript = 433 + { nodes, ... }: 434 + '' 435 + ${customDiskImage nodes} 331 436 332 - machine.start() 333 - machine.wait_for_unit("multi-user.target") 437 + machine.start() 438 + machine.wait_for_unit("multi-user.target") 334 439 335 - machine.succeed("test -e /efi/EFI/systemd/systemd-bootx64.efi") 336 - machine.succeed("test -e /boot/loader/entries/memtest86.conf") 337 - machine.succeed("test -e /boot/EFI/memtest86/memtest.efi") 338 - ''; 440 + machine.succeed("test -e /efi/EFI/systemd/systemd-bootx64.efi") 441 + machine.succeed("test -e /boot/loader/entries/memtest86.conf") 442 + machine.succeed("test -e /boot/EFI/memtest86/memtest.efi") 443 + ''; 339 444 }; 340 445 341 446 extraEntries = makeTest { 342 447 name = "systemd-boot-extra-entries"; 343 448 meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ]; 344 449 345 - nodes.machine = { pkgs, lib, ... }: { 346 - imports = [ common ]; 347 - boot.loader.systemd-boot.extraEntries = { 348 - "banana.conf" = '' 349 - title banana 350 - ''; 450 + nodes.machine = 451 + { pkgs, lib, ... }: 452 + { 453 + imports = [ common ]; 454 + boot.loader.systemd-boot.extraEntries = { 455 + "banana.conf" = '' 456 + title banana 457 + ''; 458 + }; 351 459 }; 352 - }; 353 460 354 461 testScript = '' 355 462 machine.succeed("test -e /boot/loader/entries/banana.conf") ··· 361 468 name = "systemd-boot-extra-files"; 362 469 meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ]; 363 470 364 - nodes.machine = { pkgs, lib, ... }: { 365 - imports = [ common ]; 366 - boot.loader.systemd-boot.extraFiles = { 367 - "efi/fruits/tomato.efi" = pkgs.netbootxyz-efi; 471 + nodes.machine = 472 + { pkgs, lib, ... }: 473 + { 474 + imports = [ common ]; 475 + boot.loader.systemd-boot.extraFiles = { 476 + "efi/fruits/tomato.efi" = pkgs.netbootxyz-efi; 477 + }; 368 478 }; 369 - }; 370 479 371 480 testScript = '' 372 481 machine.succeed("test -e /boot/efi/fruits/tomato.efi") ··· 381 490 nodes = { 382 491 inherit common; 383 492 384 - machine = { pkgs, nodes, ... }: { 385 - imports = [ common ]; 386 - boot.loader.systemd-boot.extraFiles = { 387 - "efi/fruits/tomato.efi" = pkgs.netbootxyz-efi; 493 + machine = 494 + { pkgs, nodes, ... }: 495 + { 496 + imports = [ common ]; 497 + boot.loader.systemd-boot.extraFiles = { 498 + "efi/fruits/tomato.efi" = pkgs.netbootxyz-efi; 499 + }; 500 + 501 + # These are configs for different nodes, but we'll use them here in `machine` 502 + system.extraDependencies = [ 503 + nodes.common.system.build.toplevel 504 + nodes.with_netbootxyz.system.build.toplevel 505 + ]; 388 506 }; 389 507 390 - # These are configs for different nodes, but we'll use them here in `machine` 391 - system.extraDependencies = [ 392 - nodes.common.system.build.toplevel 393 - nodes.with_netbootxyz.system.build.toplevel 394 - ]; 395 - }; 396 - 397 - with_netbootxyz = { pkgs, ... }: { 398 - imports = [ common ]; 399 - boot.loader.systemd-boot.netbootxyz.enable = true; 400 - }; 508 + with_netbootxyz = 509 + { pkgs, ... }: 510 + { 511 + imports = [ common ]; 512 + boot.loader.systemd-boot.netbootxyz.enable = true; 513 + }; 401 514 }; 402 515 403 - testScript = { nodes, ... }: let 404 - originalSystem = nodes.machine.system.build.toplevel; 405 - baseSystem = nodes.common.system.build.toplevel; 406 - finalSystem = nodes.with_netbootxyz.system.build.toplevel; 407 - in '' 408 - machine.succeed("test -e /boot/efi/fruits/tomato.efi") 409 - machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi") 516 + testScript = 517 + { nodes, ... }: 518 + let 519 + originalSystem = nodes.machine.system.build.toplevel; 520 + baseSystem = nodes.common.system.build.toplevel; 521 + finalSystem = nodes.with_netbootxyz.system.build.toplevel; 522 + in 523 + '' 524 + machine.succeed("test -e /boot/efi/fruits/tomato.efi") 525 + machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi") 410 526 411 - with subtest("remove files when no longer needed"): 412 - machine.succeed("${baseSystem}/bin/switch-to-configuration boot") 413 - machine.fail("test -e /boot/efi/fruits/tomato.efi") 414 - machine.fail("test -d /boot/efi/fruits") 415 - machine.succeed("test -d /boot/efi/nixos/.extra-files") 416 - machine.fail("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi") 417 - machine.fail("test -d /boot/efi/nixos/.extra-files/efi/fruits") 527 + with subtest("remove files when no longer needed"): 528 + machine.succeed("${baseSystem}/bin/switch-to-configuration boot") 529 + machine.fail("test -e /boot/efi/fruits/tomato.efi") 530 + machine.fail("test -d /boot/efi/fruits") 531 + machine.succeed("test -d /boot/efi/nixos/.extra-files") 532 + machine.fail("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi") 533 + machine.fail("test -d /boot/efi/nixos/.extra-files/efi/fruits") 418 534 419 - with subtest("files are added back when needed again"): 420 - machine.succeed("${originalSystem}/bin/switch-to-configuration boot") 421 - machine.succeed("test -e /boot/efi/fruits/tomato.efi") 422 - machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi") 535 + with subtest("files are added back when needed again"): 536 + machine.succeed("${originalSystem}/bin/switch-to-configuration boot") 537 + machine.succeed("test -e /boot/efi/fruits/tomato.efi") 538 + machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi") 423 539 424 - with subtest("simultaneously removing and adding files works"): 425 - machine.succeed("${finalSystem}/bin/switch-to-configuration boot") 426 - machine.fail("test -e /boot/efi/fruits/tomato.efi") 427 - machine.fail("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi") 428 - machine.succeed("test -e /boot/loader/entries/netbootxyz.conf") 429 - machine.succeed("test -e /boot/efi/netbootxyz/netboot.xyz.efi") 430 - machine.succeed("test -e /boot/efi/nixos/.extra-files/loader/entries/netbootxyz.conf") 431 - machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/netbootxyz/netboot.xyz.efi") 432 - ''; 540 + with subtest("simultaneously removing and adding files works"): 541 + machine.succeed("${finalSystem}/bin/switch-to-configuration boot") 542 + machine.fail("test -e /boot/efi/fruits/tomato.efi") 543 + machine.fail("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi") 544 + machine.succeed("test -e /boot/loader/entries/netbootxyz.conf") 545 + machine.succeed("test -e /boot/efi/netbootxyz/netboot.xyz.efi") 546 + machine.succeed("test -e /boot/efi/nixos/.extra-files/loader/entries/netbootxyz.conf") 547 + machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/netbootxyz/netboot.xyz.efi") 548 + ''; 433 549 }; 434 550 435 551 garbage-collect-entry = makeTest { ··· 438 554 439 555 nodes = { 440 556 inherit common; 441 - machine = { pkgs, nodes, ... }: { 442 - imports = [ common ]; 557 + machine = 558 + { pkgs, nodes, ... }: 559 + { 560 + imports = [ common ]; 443 561 444 - # These are configs for different nodes, but we'll use them here in `machine` 445 - system.extraDependencies = [ 446 - nodes.common.system.build.toplevel 447 - ]; 448 - }; 562 + # These are configs for different nodes, but we'll use them here in `machine` 563 + system.extraDependencies = [ 564 + nodes.common.system.build.toplevel 565 + ]; 566 + }; 449 567 }; 450 568 451 - testScript = { nodes, ... }: 569 + testScript = 570 + { nodes, ... }: 452 571 let 453 572 baseSystem = nodes.common.system.build.toplevel; 454 573 in ··· 461 580 ''; 462 581 }; 463 582 464 - no-bootspec = makeTest 465 - { 466 - name = "systemd-boot-no-bootspec"; 467 - meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ]; 583 + no-bootspec = makeTest { 584 + name = "systemd-boot-no-bootspec"; 585 + meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ]; 468 586 469 - nodes.machine = { 470 - imports = [ common ]; 471 - boot.bootspec.enable = false; 472 - }; 587 + nodes.machine = { 588 + imports = [ common ]; 589 + boot.bootspec.enable = false; 590 + }; 473 591 474 - testScript = '' 475 - machine.start() 476 - machine.wait_for_unit("multi-user.target") 477 - ''; 478 - }; 592 + testScript = '' 593 + machine.start() 594 + machine.wait_for_unit("multi-user.target") 595 + ''; 596 + }; 479 597 }