diff --git a/nixos/doc/manual/release-notes/rl-2411.section.md b/nixos/doc/manual/release-notes/rl-2411.section.md index 01b09aee1578..46fe89bff5a4 100644 --- a/nixos/doc/manual/release-notes/rl-2411.section.md +++ b/nixos/doc/manual/release-notes/rl-2411.section.md @@ -631,6 +631,8 @@ The derivation now installs "impl" headers selectively instead of by a wildcard. Use `imgui.src` if you just want to access the unpacked sources. +- The new `boot.loader.systemd-boot.windows` option makes setting up dual-booting with Windows on a different drive easier + - Linux 4.19 has been removed because it will reach its end of life within the lifespan of 24.11 - Unprivileged access to the kernel syslog via `dmesg` is now restricted by default. Users wanting to keep an diff --git a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix index 6cab35820c88..6490dc99d66f 100644 --- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix +++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix @@ -1,4 +1,9 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: with lib; @@ -10,16 +15,21 @@ let # We check the source code in a derivation that does not depend on the # system configuration so that most users don't have to redo the check and require # the necessary dependencies. - checkedSource = pkgs.runCommand "systemd-boot" { - preferLocalBuild = true; - } '' - install -m755 -D ${./systemd-boot-builder.py} $out - ${lib.getExe pkgs.buildPackages.mypy} \ - --no-implicit-optional \ - --disallow-untyped-calls \ - --disallow-untyped-defs \ - $out - ''; + checkedSource = + pkgs.runCommand "systemd-boot" + { + preferLocalBuild = true; + } + '' + install -m755 -D ${./systemd-boot-builder.py} $out + ${lib.getExe pkgs.buildPackages.mypy} \ + --no-implicit-optional \ + --disallow-untyped-calls \ + --disallow-untyped-defs \ + $out + ''; + + edk2ShellEspPath = "efi/edk2-uefi-shell/shell.efi"; systemdBootBuilder = pkgs.substituteAll rec { name = "systemd-boot"; @@ -44,13 +54,17 @@ let configurationLimit = if cfg.configurationLimit == null then 0 else cfg.configurationLimit; - inherit (cfg) consoleMode graceful editor rebootForBitlocker; + inherit (cfg) + consoleMode + graceful + editor + rebootForBitlocker + ; inherit (efi) efiSysMountPoint canTouchEfiVariables; - bootMountPoint = if cfg.xbootldrMountPoint != null - then cfg.xbootldrMountPoint - else efi.efiSysMountPoint; + bootMountPoint = + if cfg.xbootldrMountPoint != null then cfg.xbootldrMountPoint else efi.efiSysMountPoint; nixosDir = "/EFI/nixos"; @@ -60,29 +74,35 @@ let netbootxyz = optionalString cfg.netbootxyz.enable pkgs.netbootxyz-efi; + edk2-uefi-shell = optionalString cfg.edk2-uefi-shell.enable pkgs.edk2-uefi-shell; + checkMountpoints = pkgs.writeShellScript "check-mountpoints" '' fail() { echo "$1 = '$2' is not a mounted partition. Is the path configured correctly?" >&2 exit 1 } ${pkgs.util-linuxMinimal}/bin/findmnt ${efiSysMountPoint} > /dev/null || fail efiSysMountPoint ${efiSysMountPoint} - ${lib.optionalString - (cfg.xbootldrMountPoint != null) - "${pkgs.util-linuxMinimal}/bin/findmnt ${cfg.xbootldrMountPoint} > /dev/null || fail xbootldrMountPoint ${cfg.xbootldrMountPoint}"} + ${lib.optionalString (cfg.xbootldrMountPoint != null) + "${pkgs.util-linuxMinimal}/bin/findmnt ${cfg.xbootldrMountPoint} > /dev/null || fail xbootldrMountPoint ${cfg.xbootldrMountPoint}" + } ''; copyExtraFiles = pkgs.writeShellScript "copy-extra-files" '' empty_file=$(${pkgs.coreutils}/bin/mktemp) - ${concatStrings (mapAttrsToList (n: v: '' - ${pkgs.coreutils}/bin/install -Dp "${v}" "${bootMountPoint}/"${escapeShellArg n} - ${pkgs.coreutils}/bin/install -D $empty_file "${bootMountPoint}/${nixosDir}/.extra-files/"${escapeShellArg n} - '') cfg.extraFiles)} + ${concatStrings ( + mapAttrsToList (n: v: '' + ${pkgs.coreutils}/bin/install -Dp "${v}" "${bootMountPoint}/"${escapeShellArg n} + ${pkgs.coreutils}/bin/install -D $empty_file "${bootMountPoint}/${nixosDir}/.extra-files/"${escapeShellArg n} + '') cfg.extraFiles + )} - ${concatStrings (mapAttrsToList (n: v: '' - ${pkgs.coreutils}/bin/install -Dp "${pkgs.writeText n v}" "${bootMountPoint}/loader/entries/"${escapeShellArg n} - ${pkgs.coreutils}/bin/install -D $empty_file "${bootMountPoint}/${nixosDir}/.extra-files/loader/entries/"${escapeShellArg n} - '') cfg.extraEntries)} + ${concatStrings ( + mapAttrsToList (n: v: '' + ${pkgs.coreutils}/bin/install -Dp "${pkgs.writeText n v}" "${bootMountPoint}/loader/entries/"${escapeShellArg n} + ${pkgs.coreutils}/bin/install -D $empty_file "${bootMountPoint}/${nixosDir}/.extra-files/loader/entries/"${escapeShellArg n} + '') cfg.extraEntries + )} ''; }; @@ -91,23 +111,61 @@ let ${systemdBootBuilder}/bin/systemd-boot "$@" ${cfg.extraInstallCommands} ''; -in { +in +{ meta.maintainers = with lib.maintainers; [ julienmalka ]; - imports = - [ (mkRenamedOptionModule [ "boot" "loader" "gummiboot" "enable" ] [ "boot" "loader" "systemd-boot" "enable" ]) - (lib.mkChangedOptionModule - [ "boot" "loader" "systemd-boot" "memtest86" "entryFilename" ] - [ "boot" "loader" "systemd-boot" "memtest86" "sortKey" ] - (config: lib.strings.removeSuffix ".conf" config.boot.loader.systemd-boot.memtest86.entryFilename) - ) - (lib.mkChangedOptionModule - [ "boot" "loader" "systemd-boot" "netbootxyz" "entryFilename" ] - [ "boot" "loader" "systemd-boot" "netbootxyz" "sortKey" ] - (config: lib.strings.removeSuffix ".conf" config.boot.loader.systemd-boot.netbootxyz.entryFilename) - ) - ]; + imports = [ + (mkRenamedOptionModule + [ + "boot" + "loader" + "gummiboot" + "enable" + ] + [ + "boot" + "loader" + "systemd-boot" + "enable" + ] + ) + (lib.mkChangedOptionModule + [ + "boot" + "loader" + "systemd-boot" + "memtest86" + "entryFilename" + ] + [ + "boot" + "loader" + "systemd-boot" + "memtest86" + "sortKey" + ] + (config: lib.strings.removeSuffix ".conf" config.boot.loader.systemd-boot.memtest86.entryFilename) + ) + (lib.mkChangedOptionModule + [ + "boot" + "loader" + "systemd-boot" + "netbootxyz" + "entryFilename" + ] + [ + "boot" + "loader" + "systemd-boot" + "netbootxyz" + "sortKey" + ] + (config: lib.strings.removeSuffix ".conf" config.boot.loader.systemd-boot.netbootxyz.entryFilename) + ) + ]; options.boot.loader.systemd-boot = { enable = mkOption { @@ -124,7 +182,7 @@ in { sortKey = mkOption { default = "nixos"; - type = lib.types.str; + type = types.str; description = '' The sort key used for the NixOS bootloader entries. This key determines sorting relative to non-NixOS entries. @@ -218,7 +276,15 @@ in { consoleMode = mkOption { default = "keep"; - type = types.enum [ "0" "1" "2" "5" "auto" "max" "keep" ]; + type = types.enum [ + "0" + "1" + "2" + "5" + "auto" + "max" + "keep" + ]; description = '' The resolution of the console. The following values are valid: @@ -281,9 +347,32 @@ in { }; }; + edk2-uefi-shell = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Make the EDK2 UEFI Shell available from the systemd-boot menu. + It can be used to manually boot other operating systems or for debugging. + ''; + }; + + sortKey = mkOption { + type = types.str; + default = "o_edk2-uefi-shell"; + description = '' + `systemd-boot` orders the menu entries by their sort keys, + so if you want something to appear after all the NixOS entries, + it should start with {file}`o` or onwards. + + See also {option}`boot.loader.systemd-boot.sortKey`.. + ''; + }; + }; + extraEntries = mkOption { type = types.attrsOf types.lines; - default = {}; + default = { }; example = literalExpression '' { "memtest86.conf" = ''' title Memtest86+ @@ -306,7 +395,7 @@ in { extraFiles = mkOption { type = types.attrsOf types.path; - default = {}; + default = { }; example = literalExpression '' { "efi/memtest86/memtest.efi" = "''${pkgs.memtest86plus}/memtest.efi"; } ''; @@ -349,40 +438,126 @@ in { Windows can unseal the encryption key. ''; }; + + windows = mkOption { + default = { }; + description = '' + Make Windows bootable from systemd-boot. This option is not necessary when Windows and + NixOS use the same EFI System Partition (ESP). In that case, Windows will automatically be + detected by systemd-boot. + + However, if Windows is installed on a separate drive or ESP, you can use this option to add + a menu entry for each installation manually. + + The attribute name is used for the title of the menu entry and internal file names. + ''; + example = literalExpression '' + { + "10".efiDeviceHandle = "HD0c3"; + "11-ame" = { + title = "Windows 11 Ameliorated Edition"; + efiDeviceHandle = "HD0b1"; + }; + "11-home" = { + title = "Windows 11 Home"; + efiDeviceHandle = "FS1"; + sortKey = "z_windows"; + }; + } + ''; + type = types.attrsOf ( + types.submodule ( + { name, ... }: + { + options = { + efiDeviceHandle = mkOption { + type = types.str; + example = "HD1b3"; + description = '' + The device handle of the EFI System Partition (ESP) where the Windows bootloader is + located. This is the device handle that the EDK2 UEFI Shell uses to load the + bootloader. + + To find this handle, follow these steps: + 1. Set {option}`boot.loader.systemd-boot.edk2-uefi-shell.enable` to `true` + 2. Run `nixos-rebuild boot` + 3. Reboot and select "EDK2 UEFI Shell" from the systemd-boot menu + 4. Run `map -c` to list all consistent device handles + 5. For each device handle (for example, `HD0c1`), run `ls HD0c1:\EFI` + 6. If the output contains the directory `Microsoft`, you might have found the correct device handle + 7. Run `HD0c1:\EFI\Microsoft\Boot\Bootmgfw.efi` to check if Windows boots correctly + 8. If it does, this device handle is the one you need (in this example, `HD0c1`) + + This option is required, there is no useful default. + ''; + }; + + title = mkOption { + type = types.str; + example = "Michaelsoft Binbows"; + default = "Windows ${name}"; + defaultText = ''attribute name of this entry, prefixed with "Windows "''; + description = '' + The title of the boot menu entry. + ''; + }; + + sortKey = mkOption { + type = types.str; + default = "o_windows_${name}"; + defaultText = ''attribute name of this entry, prefixed with "o_windows_"''; + description = '' + `systemd-boot` orders the menu entries by their sort keys, + so if you want something to appear after all the NixOS entries, + it should start with {file}`o` or onwards. + + See also {option}`boot.loader.systemd-boot.sortKey`.. + ''; + }; + }; + } + ) + ); + }; }; config = mkIf cfg.enable { - assertions = [ - { - assertion = (hasPrefix "/" efi.efiSysMountPoint); - message = "The ESP mount point '${toString efi.efiSysMountPoint}' must be an absolute path"; - } - { - assertion = cfg.xbootldrMountPoint == null || (hasPrefix "/" cfg.xbootldrMountPoint); - message = "The XBOOTLDR mount point '${toString cfg.xbootldrMountPoint}' must be an absolute path"; - } - { - assertion = cfg.xbootldrMountPoint != efi.efiSysMountPoint; - message = "The XBOOTLDR mount point '${toString cfg.xbootldrMountPoint}' cannot be the same as the ESP mount point '${toString efi.efiSysMountPoint}'"; - } - { - assertion = (config.boot.kernelPackages.kernel.features or { efiBootStub = true; }) ? efiBootStub; - message = "This kernel does not support the EFI boot stub"; - } - { - assertion = cfg.installDeviceTree -> config.hardware.deviceTree.enable -> config.hardware.deviceTree.name != null; - message = "Cannot install devicetree without 'config.hardware.deviceTree.enable' enabled and 'config.hardware.deviceTree.name' set"; - } - ] ++ concatMap (filename: [ - { - assertion = !(hasInfix "/" filename); - message = "boot.loader.systemd-boot.extraEntries.${lib.strings.escapeNixIdentifier filename} is invalid: entries within folders are not supported"; - } - { - assertion = hasSuffix ".conf" filename; - message = "boot.loader.systemd-boot.extraEntries.${lib.strings.escapeNixIdentifier filename} is invalid: entries must have a .conf file extension"; - } - ]) (builtins.attrNames cfg.extraEntries) + assertions = + [ + { + assertion = (hasPrefix "/" efi.efiSysMountPoint); + message = "The ESP mount point '${toString efi.efiSysMountPoint}' must be an absolute path"; + } + { + assertion = cfg.xbootldrMountPoint == null || (hasPrefix "/" cfg.xbootldrMountPoint); + message = "The XBOOTLDR mount point '${toString cfg.xbootldrMountPoint}' must be an absolute path"; + } + { + assertion = cfg.xbootldrMountPoint != efi.efiSysMountPoint; + message = "The XBOOTLDR mount point '${toString cfg.xbootldrMountPoint}' cannot be the same as the ESP mount point '${toString efi.efiSysMountPoint}'"; + } + { + assertion = (config.boot.kernelPackages.kernel.features or { efiBootStub = true; }) ? efiBootStub; + message = "This kernel does not support the EFI boot stub"; + } + { + assertion = + cfg.installDeviceTree + -> config.hardware.deviceTree.enable + -> config.hardware.deviceTree.name != null; + message = "Cannot install devicetree without 'config.hardware.deviceTree.enable' enabled and 'config.hardware.deviceTree.name' set"; + } + ] + ++ concatMap (filename: [ + { + assertion = !(hasInfix "/" filename); + message = "boot.loader.systemd-boot.extraEntries.${lib.strings.escapeNixIdentifier filename} is invalid: entries within folders are not supported"; + } + { + assertion = hasSuffix ".conf" filename; + message = "boot.loader.systemd-boot.extraEntries.${lib.strings.escapeNixIdentifier filename} is invalid: entries must have a .conf file extension"; + } + ]) (builtins.attrNames cfg.extraEntries) ++ concatMap (filename: [ { assertion = !(hasPrefix "/" filename); @@ -396,7 +571,13 @@ in { assertion = !(hasInfix "nixos/.extra-files" (toLower filename)); message = "boot.loader.systemd-boot.extraFiles.${lib.strings.escapeNixIdentifier filename} is invalid: files cannot be placed in the nixos/.extra-files directory"; } - ]) (builtins.attrNames cfg.extraFiles); + ]) (builtins.attrNames cfg.extraFiles) + ++ concatMap (winVersion: [ + { + assertion = lib.match "^[-_0-9A-Za-z]+$" winVersion != null; + message = "boot.loader.systemd-boot.windows.${winVersion} is invalid: key must only contain alphanumeric characters, hyphens, and underscores"; + } + ]) (builtins.attrNames cfg.windows); boot.loader.grub.enable = mkDefault false; @@ -409,24 +590,44 @@ in { (mkIf cfg.netbootxyz.enable { "efi/netbootxyz/netboot.xyz.efi" = "${pkgs.netbootxyz-efi}"; }) + (mkIf (cfg.edk2-uefi-shell.enable || cfg.windows != { }) { + ${edk2ShellEspPath} = "${pkgs.edk2-uefi-shell}/shell.efi"; + }) ]; - boot.loader.systemd-boot.extraEntries = mkMerge [ - (mkIf cfg.memtest86.enable { - "memtest86.conf" = '' - title Memtest86+ - efi /efi/memtest86/memtest.efi - sort-key ${cfg.memtest86.sortKey} + boot.loader.systemd-boot.extraEntries = mkMerge ( + [ + (mkIf cfg.memtest86.enable { + "memtest86.conf" = '' + title Memtest86+ + efi /efi/memtest86/memtest.efi + sort-key ${cfg.memtest86.sortKey} + ''; + }) + (mkIf cfg.netbootxyz.enable { + "netbootxyz.conf" = '' + title netboot.xyz + efi /efi/netbootxyz/netboot.xyz.efi + sort-key ${cfg.netbootxyz.sortKey} + ''; + }) + (mkIf cfg.edk2-uefi-shell.enable { + "edk2-uefi-shell.conf" = '' + title EDK2 UEFI Shell + efi /${edk2ShellEspPath} + sort-key ${cfg.edk2-uefi-shell.sortKey} + ''; + }) + ] + ++ (mapAttrsToList (winVersion: cfg: { + "windows_${winVersion}.conf" = '' + title ${cfg.title} + efi /${edk2ShellEspPath} + options -nointerrupt -nomap -noversion ${cfg.efiDeviceHandle}:EFI\Microsoft\Boot\Bootmgfw.efi + sort-key ${cfg.sortKey} ''; - }) - (mkIf cfg.netbootxyz.enable { - "netbootxyz.conf" = '' - title netboot.xyz - efi /efi/netbootxyz/netboot.xyz.efi - sort-key ${cfg.netbootxyz.sortKey} - ''; - }) - ]; + }) cfg.windows) + ); boot.bootspec.extensions."org.nixos.systemd-boot" = { inherit (config.boot.loader.systemd-boot) sortKey; diff --git a/nixos/tests/systemd-boot.nix b/nixos/tests/systemd-boot.nix index 79bfcb84ebd7..812d6088ed4e 100644 --- a/nixos/tests/systemd-boot.nix +++ b/nixos/tests/systemd-boot.nix @@ -1,6 +1,7 @@ -{ system ? builtins.currentSystem, - config ? {}, - pkgs ? import ../.. { inherit system config; } +{ + system ? builtins.currentSystem, + config ? { }, + pkgs ? import ../.. { inherit system config; }, }: with import ../lib/testing-python.nix { inherit system pkgs; }; @@ -16,7 +17,13 @@ let system.switch.enable = true; }; - commonXbootldr = { config, lib, pkgs, ... }: + commonXbootldr = + { + config, + lib, + pkgs, + ... + }: let diskImage = import ../lib/make-disk-image.nix { inherit config lib pkgs; @@ -85,7 +92,10 @@ in { basic = makeTest { name = "systemd-boot"; - meta.maintainers = with pkgs.lib.maintainers; [ danielfullmer julienmalka ]; + meta.maintainers = with pkgs.lib.maintainers; [ + danielfullmer + julienmalka + ]; nodes.machine = common; @@ -117,22 +127,25 @@ in virtualisation.useSecureBoot = true; }; - testScript = let - efiArch = pkgs.stdenv.hostPlatform.efiArch; - in { nodes, ... }: '' - machine.start(allow_reboot=True) - machine.wait_for_unit("multi-user.target") + testScript = + let + efiArch = pkgs.stdenv.hostPlatform.efiArch; + in + { nodes, ... }: + '' + machine.start(allow_reboot=True) + machine.wait_for_unit("multi-user.target") - machine.succeed("sbctl create-keys") - machine.succeed("sbctl enroll-keys --yes-this-might-brick-my-machine") - machine.succeed('sbctl sign /boot/EFI/systemd/systemd-boot${efiArch}.efi') - machine.succeed('sbctl sign /boot/EFI/BOOT/BOOT${toUpper efiArch}.EFI') - machine.succeed('sbctl sign /boot/EFI/nixos/*${nodes.machine.system.boot.loader.kernelFile}.efi') + machine.succeed("sbctl create-keys") + machine.succeed("sbctl enroll-keys --yes-this-might-brick-my-machine") + machine.succeed('sbctl sign /boot/EFI/systemd/systemd-boot${efiArch}.efi') + machine.succeed('sbctl sign /boot/EFI/BOOT/BOOT${toUpper efiArch}.EFI') + machine.succeed('sbctl sign /boot/EFI/nixos/*${nodes.machine.system.boot.loader.kernelFile}.efi') - machine.reboot() + machine.reboot() - assert "Secure Boot: enabled (user)" in machine.succeed("bootctl status") - ''; + assert "Secure Boot: enabled (user)" in machine.succeed("bootctl status") + ''; }; basicXbootldr = makeTest { @@ -141,80 +154,97 @@ in nodes.machine = commonXbootldr; - testScript = { nodes, ... }: '' - ${customDiskImage nodes} + testScript = + { nodes, ... }: + '' + ${customDiskImage nodes} - machine.start() - machine.wait_for_unit("multi-user.target") + machine.start() + machine.wait_for_unit("multi-user.target") - machine.succeed("test -e /efi/EFI/systemd/systemd-bootx64.efi") - machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf") + machine.succeed("test -e /efi/EFI/systemd/systemd-bootx64.efi") + machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf") - # Ensure we actually booted using systemd-boot - # Magic number is the vendor UUID used by systemd-boot. - machine.succeed( - "test -e /sys/firmware/efi/efivars/LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f" - ) + # Ensure we actually booted using systemd-boot + # Magic number is the vendor UUID used by systemd-boot. + machine.succeed( + "test -e /sys/firmware/efi/efivars/LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f" + ) - # "bootctl install" should have created an EFI entry - machine.succeed('efibootmgr | grep "Linux Boot Manager"') - ''; + # "bootctl install" should have created an EFI entry + machine.succeed('efibootmgr | grep "Linux Boot Manager"') + ''; }; # Check that specialisations create corresponding boot entries. specialisation = makeTest { name = "systemd-boot-specialisation"; - meta.maintainers = with pkgs.lib.maintainers; [ lukegb julienmalka ]; + meta.maintainers = with pkgs.lib.maintainers; [ + lukegb + julienmalka + ]; - nodes.machine = { pkgs, lib, ... }: { - imports = [ common ]; - specialisation.something.configuration = { - boot.loader.systemd-boot.sortKey = "something"; + nodes.machine = + { pkgs, lib, ... }: + { + imports = [ common ]; + specialisation.something.configuration = { + boot.loader.systemd-boot.sortKey = "something"; - # Since qemu will dynamically create a devicetree blob when starting - # up, it is not straight forward to create an export of that devicetree - # blob without knowing before-hand all the flags we would pass to qemu - # (we would then be able to use `dumpdtb`). Thus, the following config - # will not boot, but it does allow us to assert that the boot entry has - # the correct contents. - boot.loader.systemd-boot.installDeviceTree = pkgs.stdenv.hostPlatform.isAarch64; - hardware.deviceTree.name = "dummy.dtb"; - hardware.deviceTree.package = lib.mkForce (pkgs.runCommand "dummy-devicetree-package" { } '' - mkdir -p $out - cp ${pkgs.emptyFile} $out/dummy.dtb - ''); + # Since qemu will dynamically create a devicetree blob when starting + # up, it is not straight forward to create an export of that devicetree + # blob without knowing before-hand all the flags we would pass to qemu + # (we would then be able to use `dumpdtb`). Thus, the following config + # will not boot, but it does allow us to assert that the boot entry has + # the correct contents. + boot.loader.systemd-boot.installDeviceTree = pkgs.stdenv.hostPlatform.isAarch64; + hardware.deviceTree.name = "dummy.dtb"; + hardware.deviceTree.package = lib.mkForce ( + pkgs.runCommand "dummy-devicetree-package" { } '' + mkdir -p $out + cp ${pkgs.emptyFile} $out/dummy.dtb + '' + ); + }; }; - }; - testScript = { nodes, ... }: '' - machine.start() - machine.wait_for_unit("multi-user.target") + testScript = + { nodes, ... }: + '' + machine.start() + machine.wait_for_unit("multi-user.target") - machine.succeed( - "test -e /boot/loader/entries/nixos-generation-1-specialisation-something.conf" - ) - machine.succeed( - "grep -q 'title NixOS (something)' /boot/loader/entries/nixos-generation-1-specialisation-something.conf" - ) - machine.succeed( - "grep 'sort-key something' /boot/loader/entries/nixos-generation-1-specialisation-something.conf" - ) - '' + pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isAarch64 '' - machine.succeed( - r"grep 'devicetree /EFI/nixos/[a-z0-9]\{32\}.*dummy' /boot/loader/entries/nixos-generation-1-specialisation-something.conf" - ) - ''; + machine.succeed( + "test -e /boot/loader/entries/nixos-generation-1-specialisation-something.conf" + ) + machine.succeed( + "grep -q 'title NixOS (something)' /boot/loader/entries/nixos-generation-1-specialisation-something.conf" + ) + machine.succeed( + "grep 'sort-key something' /boot/loader/entries/nixos-generation-1-specialisation-something.conf" + ) + '' + + pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isAarch64 '' + machine.succeed( + r"grep 'devicetree /EFI/nixos/[a-z0-9]\{32\}.*dummy' /boot/loader/entries/nixos-generation-1-specialisation-something.conf" + ) + ''; }; # Boot without having created an EFI entry--instead using default "/EFI/BOOT/BOOTX64.EFI" fallback = makeTest { name = "systemd-boot-fallback"; - meta.maintainers = with pkgs.lib.maintainers; [ danielfullmer julienmalka ]; + meta.maintainers = with pkgs.lib.maintainers; [ + danielfullmer + julienmalka + ]; - nodes.machine = { pkgs, lib, ... }: { - imports = [ common ]; - boot.loader.efi.canTouchEfiVariables = mkForce false; - }; + nodes.machine = + { pkgs, lib, ... }: + { + imports = [ common ]; + boot.loader.efi.canTouchEfiVariables = mkForce false; + }; testScript = '' machine.start() @@ -235,7 +265,10 @@ in update = makeTest { name = "systemd-boot-update"; - meta.maintainers = with pkgs.lib.maintainers; [ danielfullmer julienmalka ]; + meta.maintainers = with pkgs.lib.maintainers; [ + danielfullmer + julienmalka + ]; nodes.machine = common; @@ -270,29 +303,35 @@ in ''; }; - memtest86 = with pkgs.lib; optionalAttrs (meta.availableOn { inherit system; } pkgs.memtest86plus) (makeTest { - name = "systemd-boot-memtest86"; - meta.maintainers = with maintainers; [ julienmalka ]; + memtest86 = + with pkgs.lib; + optionalAttrs (meta.availableOn { inherit system; } pkgs.memtest86plus) (makeTest { + name = "systemd-boot-memtest86"; + meta.maintainers = with maintainers; [ julienmalka ]; - nodes.machine = { pkgs, lib, ... }: { - imports = [ common ]; - boot.loader.systemd-boot.memtest86.enable = true; - }; + nodes.machine = + { pkgs, lib, ... }: + { + imports = [ common ]; + boot.loader.systemd-boot.memtest86.enable = true; + }; - testScript = '' - machine.succeed("test -e /boot/loader/entries/memtest86.conf") - machine.succeed("test -e /boot/efi/memtest86/memtest.efi") - ''; - }); + testScript = '' + machine.succeed("test -e /boot/loader/entries/memtest86.conf") + machine.succeed("test -e /boot/efi/memtest86/memtest.efi") + ''; + }); netbootxyz = makeTest { name = "systemd-boot-netbootxyz"; meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ]; - nodes.machine = { pkgs, lib, ... }: { - imports = [ common ]; - boot.loader.systemd-boot.netbootxyz.enable = true; - }; + nodes.machine = + { pkgs, lib, ... }: + { + imports = [ common ]; + boot.loader.systemd-boot.netbootxyz.enable = true; + }; testScript = '' machine.succeed("test -e /boot/loader/entries/netbootxyz.conf") @@ -300,15 +339,77 @@ in ''; }; + edk2-uefi-shell = makeTest { + name = "systemd-boot-edk2-uefi-shell"; + meta.maintainers = with pkgs.lib.maintainers; [ iFreilicht ]; + + nodes.machine = { ... }: { + imports = [ common ]; + boot.loader.systemd-boot.edk2-uefi-shell.enable = true; + }; + + testScript = '' + machine.succeed("test -e /boot/loader/entries/edk2-uefi-shell.conf") + machine.succeed("test -e /boot/efi/edk2-uefi-shell/shell.efi") + ''; + }; + + windows = makeTest { + name = "systemd-boot-windows"; + meta.maintainers = with pkgs.lib.maintainers; [ iFreilicht ]; + + nodes.machine = { ... }: { + imports = [ common ]; + boot.loader.systemd-boot.windows = { + "7" = { + efiDeviceHandle = "HD0c1"; + sortKey = "before_all_others"; + }; + "Ten".efiDeviceHandle = "FS0"; + "11" = { + title = "Title with-_-punctuation ...?!"; + efiDeviceHandle = "HD0d4"; + sortKey = "zzz"; + }; + }; + }; + + testScript = '' + machine.succeed("test -e /boot/efi/edk2-uefi-shell/shell.efi") + + machine.succeed("test -e /boot/loader/entries/windows_7.conf") + machine.succeed("test -e /boot/loader/entries/windows_Ten.conf") + machine.succeed("test -e /boot/loader/entries/windows_11.conf") + + machine.succeed("grep 'efi /efi/edk2-uefi-shell/shell.efi' /boot/loader/entries/windows_7.conf") + machine.succeed("grep 'efi /efi/edk2-uefi-shell/shell.efi' /boot/loader/entries/windows_Ten.conf") + machine.succeed("grep 'efi /efi/edk2-uefi-shell/shell.efi' /boot/loader/entries/windows_11.conf") + + machine.succeed("grep 'HD0c1:EFI\\\\Microsoft\\\\Boot\\\\Bootmgfw.efi' /boot/loader/entries/windows_7.conf") + machine.succeed("grep 'FS0:EFI\\\\Microsoft\\\\Boot\\\\Bootmgfw.efi' /boot/loader/entries/windows_Ten.conf") + machine.succeed("grep 'HD0d4:EFI\\\\Microsoft\\\\Boot\\\\Bootmgfw.efi' /boot/loader/entries/windows_11.conf") + + machine.succeed("grep 'sort-key before_all_others' /boot/loader/entries/windows_7.conf") + machine.succeed("grep 'sort-key o_windows_Ten' /boot/loader/entries/windows_Ten.conf") + machine.succeed("grep 'sort-key zzz' /boot/loader/entries/windows_11.conf") + + machine.succeed("grep 'title Windows 7' /boot/loader/entries/windows_7.conf") + machine.succeed("grep 'title Windows Ten' /boot/loader/entries/windows_Ten.conf") + machine.succeed('grep "title Title with-_-punctuation ...?!" /boot/loader/entries/windows_11.conf') + ''; + }; + memtestSortKey = makeTest { name = "systemd-boot-memtest-sortkey"; meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ]; - nodes.machine = { pkgs, lib, ... }: { - imports = [ common ]; - boot.loader.systemd-boot.memtest86.enable = true; - boot.loader.systemd-boot.memtest86.sortKey = "apple"; - }; + nodes.machine = + { pkgs, lib, ... }: + { + imports = [ common ]; + boot.loader.systemd-boot.memtest86.enable = true; + boot.loader.systemd-boot.memtest86.sortKey = "apple"; + }; testScript = '' machine.succeed("test -e /boot/loader/entries/memtest86.conf") @@ -321,35 +422,41 @@ in name = "systemd-boot-entry-filename-xbootldr"; meta.maintainers = with pkgs.lib.maintainers; [ sdht0 ]; - nodes.machine = { pkgs, lib, ... }: { - imports = [ commonXbootldr ]; - boot.loader.systemd-boot.memtest86.enable = true; - }; + nodes.machine = + { pkgs, lib, ... }: + { + imports = [ commonXbootldr ]; + boot.loader.systemd-boot.memtest86.enable = true; + }; - testScript = { nodes, ... }: '' - ${customDiskImage nodes} + testScript = + { nodes, ... }: + '' + ${customDiskImage nodes} - machine.start() - machine.wait_for_unit("multi-user.target") + machine.start() + machine.wait_for_unit("multi-user.target") - machine.succeed("test -e /efi/EFI/systemd/systemd-bootx64.efi") - machine.succeed("test -e /boot/loader/entries/memtest86.conf") - machine.succeed("test -e /boot/EFI/memtest86/memtest.efi") - ''; + machine.succeed("test -e /efi/EFI/systemd/systemd-bootx64.efi") + machine.succeed("test -e /boot/loader/entries/memtest86.conf") + machine.succeed("test -e /boot/EFI/memtest86/memtest.efi") + ''; }; extraEntries = makeTest { name = "systemd-boot-extra-entries"; meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ]; - nodes.machine = { pkgs, lib, ... }: { - imports = [ common ]; - boot.loader.systemd-boot.extraEntries = { - "banana.conf" = '' - title banana - ''; + nodes.machine = + { pkgs, lib, ... }: + { + imports = [ common ]; + boot.loader.systemd-boot.extraEntries = { + "banana.conf" = '' + title banana + ''; + }; }; - }; testScript = '' machine.succeed("test -e /boot/loader/entries/banana.conf") @@ -361,12 +468,14 @@ in name = "systemd-boot-extra-files"; meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ]; - nodes.machine = { pkgs, lib, ... }: { - imports = [ common ]; - boot.loader.systemd-boot.extraFiles = { - "efi/fruits/tomato.efi" = pkgs.netbootxyz-efi; + nodes.machine = + { pkgs, lib, ... }: + { + imports = [ common ]; + boot.loader.systemd-boot.extraFiles = { + "efi/fruits/tomato.efi" = pkgs.netbootxyz-efi; + }; }; - }; testScript = '' machine.succeed("test -e /boot/efi/fruits/tomato.efi") @@ -381,55 +490,62 @@ in nodes = { inherit common; - machine = { pkgs, nodes, ... }: { - imports = [ common ]; - boot.loader.systemd-boot.extraFiles = { - "efi/fruits/tomato.efi" = pkgs.netbootxyz-efi; + machine = + { pkgs, nodes, ... }: + { + imports = [ common ]; + boot.loader.systemd-boot.extraFiles = { + "efi/fruits/tomato.efi" = pkgs.netbootxyz-efi; + }; + + # These are configs for different nodes, but we'll use them here in `machine` + system.extraDependencies = [ + nodes.common.system.build.toplevel + nodes.with_netbootxyz.system.build.toplevel + ]; }; - # These are configs for different nodes, but we'll use them here in `machine` - system.extraDependencies = [ - nodes.common.system.build.toplevel - nodes.with_netbootxyz.system.build.toplevel - ]; - }; - - with_netbootxyz = { pkgs, ... }: { - imports = [ common ]; - boot.loader.systemd-boot.netbootxyz.enable = true; - }; + with_netbootxyz = + { pkgs, ... }: + { + imports = [ common ]; + boot.loader.systemd-boot.netbootxyz.enable = true; + }; }; - testScript = { nodes, ... }: let - originalSystem = nodes.machine.system.build.toplevel; - baseSystem = nodes.common.system.build.toplevel; - finalSystem = nodes.with_netbootxyz.system.build.toplevel; - in '' - machine.succeed("test -e /boot/efi/fruits/tomato.efi") - machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi") + testScript = + { nodes, ... }: + let + originalSystem = nodes.machine.system.build.toplevel; + baseSystem = nodes.common.system.build.toplevel; + finalSystem = nodes.with_netbootxyz.system.build.toplevel; + in + '' + machine.succeed("test -e /boot/efi/fruits/tomato.efi") + machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi") - with subtest("remove files when no longer needed"): - machine.succeed("${baseSystem}/bin/switch-to-configuration boot") - machine.fail("test -e /boot/efi/fruits/tomato.efi") - machine.fail("test -d /boot/efi/fruits") - machine.succeed("test -d /boot/efi/nixos/.extra-files") - machine.fail("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi") - machine.fail("test -d /boot/efi/nixos/.extra-files/efi/fruits") + with subtest("remove files when no longer needed"): + machine.succeed("${baseSystem}/bin/switch-to-configuration boot") + machine.fail("test -e /boot/efi/fruits/tomato.efi") + machine.fail("test -d /boot/efi/fruits") + machine.succeed("test -d /boot/efi/nixos/.extra-files") + machine.fail("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi") + machine.fail("test -d /boot/efi/nixos/.extra-files/efi/fruits") - with subtest("files are added back when needed again"): - machine.succeed("${originalSystem}/bin/switch-to-configuration boot") - machine.succeed("test -e /boot/efi/fruits/tomato.efi") - machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi") + with subtest("files are added back when needed again"): + machine.succeed("${originalSystem}/bin/switch-to-configuration boot") + machine.succeed("test -e /boot/efi/fruits/tomato.efi") + machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi") - with subtest("simultaneously removing and adding files works"): - machine.succeed("${finalSystem}/bin/switch-to-configuration boot") - machine.fail("test -e /boot/efi/fruits/tomato.efi") - machine.fail("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi") - machine.succeed("test -e /boot/loader/entries/netbootxyz.conf") - machine.succeed("test -e /boot/efi/netbootxyz/netboot.xyz.efi") - machine.succeed("test -e /boot/efi/nixos/.extra-files/loader/entries/netbootxyz.conf") - machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/netbootxyz/netboot.xyz.efi") - ''; + with subtest("simultaneously removing and adding files works"): + machine.succeed("${finalSystem}/bin/switch-to-configuration boot") + machine.fail("test -e /boot/efi/fruits/tomato.efi") + machine.fail("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi") + machine.succeed("test -e /boot/loader/entries/netbootxyz.conf") + machine.succeed("test -e /boot/efi/netbootxyz/netboot.xyz.efi") + machine.succeed("test -e /boot/efi/nixos/.extra-files/loader/entries/netbootxyz.conf") + machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/netbootxyz/netboot.xyz.efi") + ''; }; garbage-collect-entry = makeTest { @@ -438,17 +554,20 @@ in nodes = { inherit common; - machine = { pkgs, nodes, ... }: { - imports = [ common ]; + machine = + { pkgs, nodes, ... }: + { + imports = [ common ]; - # These are configs for different nodes, but we'll use them here in `machine` - system.extraDependencies = [ - nodes.common.system.build.toplevel - ]; - }; + # These are configs for different nodes, but we'll use them here in `machine` + system.extraDependencies = [ + nodes.common.system.build.toplevel + ]; + }; }; - testScript = { nodes, ... }: + testScript = + { nodes, ... }: let baseSystem = nodes.common.system.build.toplevel; in @@ -461,19 +580,18 @@ in ''; }; - no-bootspec = makeTest - { - name = "systemd-boot-no-bootspec"; - meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ]; + no-bootspec = makeTest { + name = "systemd-boot-no-bootspec"; + meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ]; - nodes.machine = { - imports = [ common ]; - boot.bootspec.enable = false; - }; - - testScript = '' - machine.start() - machine.wait_for_unit("multi-user.target") - ''; + nodes.machine = { + imports = [ common ]; + boot.bootspec.enable = false; }; + + testScript = '' + machine.start() + machine.wait_for_unit("multi-user.target") + ''; + }; }