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

This commit is contained in:
Atemu 2024-10-11 20:25:08 +02:00 committed by GitHub
commit 12ef18d2e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 593 additions and 272 deletions

View file

@ -631,6 +631,8 @@
The derivation now installs "impl" headers selectively instead of by a wildcard. The derivation now installs "impl" headers selectively instead of by a wildcard.
Use `imgui.src` if you just want to access the unpacked sources. 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 - 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 - Unprivileged access to the kernel syslog via `dmesg` is now restricted by default. Users wanting to keep an

View file

@ -1,4 +1,9 @@
{ config, lib, pkgs, ... }: {
config,
lib,
pkgs,
...
}:
with lib; with lib;
@ -10,16 +15,21 @@ let
# We check the source code in a derivation that does not depend on the # 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 # system configuration so that most users don't have to redo the check and require
# the necessary dependencies. # the necessary dependencies.
checkedSource = pkgs.runCommand "systemd-boot" { checkedSource =
preferLocalBuild = true; pkgs.runCommand "systemd-boot"
} '' {
install -m755 -D ${./systemd-boot-builder.py} $out preferLocalBuild = true;
${lib.getExe pkgs.buildPackages.mypy} \ }
--no-implicit-optional \ ''
--disallow-untyped-calls \ install -m755 -D ${./systemd-boot-builder.py} $out
--disallow-untyped-defs \ ${lib.getExe pkgs.buildPackages.mypy} \
$out --no-implicit-optional \
''; --disallow-untyped-calls \
--disallow-untyped-defs \
$out
'';
edk2ShellEspPath = "efi/edk2-uefi-shell/shell.efi";
systemdBootBuilder = pkgs.substituteAll rec { systemdBootBuilder = pkgs.substituteAll rec {
name = "systemd-boot"; name = "systemd-boot";
@ -44,13 +54,17 @@ let
configurationLimit = if cfg.configurationLimit == null then 0 else cfg.configurationLimit; 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; inherit (efi) efiSysMountPoint canTouchEfiVariables;
bootMountPoint = if cfg.xbootldrMountPoint != null bootMountPoint =
then cfg.xbootldrMountPoint if cfg.xbootldrMountPoint != null then cfg.xbootldrMountPoint else efi.efiSysMountPoint;
else efi.efiSysMountPoint;
nixosDir = "/EFI/nixos"; nixosDir = "/EFI/nixos";
@ -60,29 +74,35 @@ let
netbootxyz = optionalString cfg.netbootxyz.enable pkgs.netbootxyz-efi; 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" '' checkMountpoints = pkgs.writeShellScript "check-mountpoints" ''
fail() { fail() {
echo "$1 = '$2' is not a mounted partition. Is the path configured correctly?" >&2 echo "$1 = '$2' is not a mounted partition. Is the path configured correctly?" >&2
exit 1 exit 1
} }
${pkgs.util-linuxMinimal}/bin/findmnt ${efiSysMountPoint} > /dev/null || fail efiSysMountPoint ${efiSysMountPoint} ${pkgs.util-linuxMinimal}/bin/findmnt ${efiSysMountPoint} > /dev/null || fail efiSysMountPoint ${efiSysMountPoint}
${lib.optionalString ${lib.optionalString (cfg.xbootldrMountPoint != null)
(cfg.xbootldrMountPoint != null) "${pkgs.util-linuxMinimal}/bin/findmnt ${cfg.xbootldrMountPoint} > /dev/null || fail xbootldrMountPoint ${cfg.xbootldrMountPoint}"
"${pkgs.util-linuxMinimal}/bin/findmnt ${cfg.xbootldrMountPoint} > /dev/null || fail xbootldrMountPoint ${cfg.xbootldrMountPoint}"} }
''; '';
copyExtraFiles = pkgs.writeShellScript "copy-extra-files" '' copyExtraFiles = pkgs.writeShellScript "copy-extra-files" ''
empty_file=$(${pkgs.coreutils}/bin/mktemp) empty_file=$(${pkgs.coreutils}/bin/mktemp)
${concatStrings (mapAttrsToList (n: v: '' ${concatStrings (
${pkgs.coreutils}/bin/install -Dp "${v}" "${bootMountPoint}/"${escapeShellArg n} mapAttrsToList (n: v: ''
${pkgs.coreutils}/bin/install -D $empty_file "${bootMountPoint}/${nixosDir}/.extra-files/"${escapeShellArg n} ${pkgs.coreutils}/bin/install -Dp "${v}" "${bootMountPoint}/"${escapeShellArg n}
'') cfg.extraFiles)} ${pkgs.coreutils}/bin/install -D $empty_file "${bootMountPoint}/${nixosDir}/.extra-files/"${escapeShellArg n}
'') cfg.extraFiles
)}
${concatStrings (mapAttrsToList (n: v: '' ${concatStrings (
${pkgs.coreutils}/bin/install -Dp "${pkgs.writeText n v}" "${bootMountPoint}/loader/entries/"${escapeShellArg n} mapAttrsToList (n: v: ''
${pkgs.coreutils}/bin/install -D $empty_file "${bootMountPoint}/${nixosDir}/.extra-files/loader/entries/"${escapeShellArg n} ${pkgs.coreutils}/bin/install -Dp "${pkgs.writeText n v}" "${bootMountPoint}/loader/entries/"${escapeShellArg n}
'') cfg.extraEntries)} ${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 "$@" ${systemdBootBuilder}/bin/systemd-boot "$@"
${cfg.extraInstallCommands} ${cfg.extraInstallCommands}
''; '';
in { in
{
meta.maintainers = with lib.maintainers; [ julienmalka ]; meta.maintainers = with lib.maintainers; [ julienmalka ];
imports = imports = [
[ (mkRenamedOptionModule [ "boot" "loader" "gummiboot" "enable" ] [ "boot" "loader" "systemd-boot" "enable" ]) (mkRenamedOptionModule
(lib.mkChangedOptionModule [
[ "boot" "loader" "systemd-boot" "memtest86" "entryFilename" ] "boot"
[ "boot" "loader" "systemd-boot" "memtest86" "sortKey" ] "loader"
(config: lib.strings.removeSuffix ".conf" config.boot.loader.systemd-boot.memtest86.entryFilename) "gummiboot"
) "enable"
(lib.mkChangedOptionModule ]
[ "boot" "loader" "systemd-boot" "netbootxyz" "entryFilename" ] [
[ "boot" "loader" "systemd-boot" "netbootxyz" "sortKey" ] "boot"
(config: lib.strings.removeSuffix ".conf" config.boot.loader.systemd-boot.netbootxyz.entryFilename) "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 = { options.boot.loader.systemd-boot = {
enable = mkOption { enable = mkOption {
@ -124,7 +182,7 @@ in {
sortKey = mkOption { sortKey = mkOption {
default = "nixos"; default = "nixos";
type = lib.types.str; type = types.str;
description = '' description = ''
The sort key used for the NixOS bootloader entries. The sort key used for the NixOS bootloader entries.
This key determines sorting relative to non-NixOS entries. This key determines sorting relative to non-NixOS entries.
@ -218,7 +276,15 @@ in {
consoleMode = mkOption { consoleMode = mkOption {
default = "keep"; default = "keep";
type = types.enum [ "0" "1" "2" "5" "auto" "max" "keep" ]; type = types.enum [
"0"
"1"
"2"
"5"
"auto"
"max"
"keep"
];
description = '' description = ''
The resolution of the console. The following values are valid: 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 { extraEntries = mkOption {
type = types.attrsOf types.lines; type = types.attrsOf types.lines;
default = {}; default = { };
example = literalExpression '' example = literalExpression ''
{ "memtest86.conf" = ''' { "memtest86.conf" = '''
title Memtest86+ title Memtest86+
@ -306,7 +395,7 @@ in {
extraFiles = mkOption { extraFiles = mkOption {
type = types.attrsOf types.path; type = types.attrsOf types.path;
default = {}; default = { };
example = literalExpression '' example = literalExpression ''
{ "efi/memtest86/memtest.efi" = "''${pkgs.memtest86plus}/memtest.efi"; } { "efi/memtest86/memtest.efi" = "''${pkgs.memtest86plus}/memtest.efi"; }
''; '';
@ -349,40 +438,126 @@ in {
Windows can unseal the encryption key. 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 { config = mkIf cfg.enable {
assertions = [ assertions =
{ [
assertion = (hasPrefix "/" efi.efiSysMountPoint); {
message = "The ESP mount point '${toString efi.efiSysMountPoint}' must be an absolute path"; 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 == 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 = 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 = (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"; assertion =
} cfg.installDeviceTree
] ++ concatMap (filename: [ -> config.hardware.deviceTree.enable
{ -> config.hardware.deviceTree.name != null;
assertion = !(hasInfix "/" filename); message = "Cannot install devicetree without 'config.hardware.deviceTree.enable' enabled and 'config.hardware.deviceTree.name' set";
message = "boot.loader.systemd-boot.extraEntries.${lib.strings.escapeNixIdentifier filename} is invalid: entries within folders are not supported"; }
} ]
{ ++ concatMap (filename: [
assertion = hasSuffix ".conf" filename; {
message = "boot.loader.systemd-boot.extraEntries.${lib.strings.escapeNixIdentifier filename} is invalid: entries must have a .conf file extension"; assertion = !(hasInfix "/" filename);
} message = "boot.loader.systemd-boot.extraEntries.${lib.strings.escapeNixIdentifier filename} is invalid: entries within folders are not supported";
]) (builtins.attrNames cfg.extraEntries) }
{
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: [ ++ concatMap (filename: [
{ {
assertion = !(hasPrefix "/" filename); assertion = !(hasPrefix "/" filename);
@ -396,7 +571,13 @@ in {
assertion = !(hasInfix "nixos/.extra-files" (toLower filename)); 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"; 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; boot.loader.grub.enable = mkDefault false;
@ -409,24 +590,44 @@ in {
(mkIf cfg.netbootxyz.enable { (mkIf cfg.netbootxyz.enable {
"efi/netbootxyz/netboot.xyz.efi" = "${pkgs.netbootxyz-efi}"; "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 [ boot.loader.systemd-boot.extraEntries = mkMerge (
(mkIf cfg.memtest86.enable { [
"memtest86.conf" = '' (mkIf cfg.memtest86.enable {
title Memtest86+ "memtest86.conf" = ''
efi /efi/memtest86/memtest.efi title Memtest86+
sort-key ${cfg.memtest86.sortKey} 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}
''; '';
}) }) cfg.windows)
(mkIf cfg.netbootxyz.enable { );
"netbootxyz.conf" = ''
title netboot.xyz
efi /efi/netbootxyz/netboot.xyz.efi
sort-key ${cfg.netbootxyz.sortKey}
'';
})
];
boot.bootspec.extensions."org.nixos.systemd-boot" = { boot.bootspec.extensions."org.nixos.systemd-boot" = {
inherit (config.boot.loader.systemd-boot) sortKey; inherit (config.boot.loader.systemd-boot) sortKey;

View file

@ -1,6 +1,7 @@
{ system ? builtins.currentSystem, {
config ? {}, system ? builtins.currentSystem,
pkgs ? import ../.. { inherit system config; } config ? { },
pkgs ? import ../.. { inherit system config; },
}: }:
with import ../lib/testing-python.nix { inherit system pkgs; }; with import ../lib/testing-python.nix { inherit system pkgs; };
@ -16,7 +17,13 @@ let
system.switch.enable = true; system.switch.enable = true;
}; };
commonXbootldr = { config, lib, pkgs, ... }: commonXbootldr =
{
config,
lib,
pkgs,
...
}:
let let
diskImage = import ../lib/make-disk-image.nix { diskImage = import ../lib/make-disk-image.nix {
inherit config lib pkgs; inherit config lib pkgs;
@ -85,7 +92,10 @@ in
{ {
basic = makeTest { basic = makeTest {
name = "systemd-boot"; name = "systemd-boot";
meta.maintainers = with pkgs.lib.maintainers; [ danielfullmer julienmalka ]; meta.maintainers = with pkgs.lib.maintainers; [
danielfullmer
julienmalka
];
nodes.machine = common; nodes.machine = common;
@ -117,22 +127,25 @@ in
virtualisation.useSecureBoot = true; virtualisation.useSecureBoot = true;
}; };
testScript = let testScript =
efiArch = pkgs.stdenv.hostPlatform.efiArch; let
in { nodes, ... }: '' efiArch = pkgs.stdenv.hostPlatform.efiArch;
machine.start(allow_reboot=True) in
machine.wait_for_unit("multi-user.target") { nodes, ... }:
''
machine.start(allow_reboot=True)
machine.wait_for_unit("multi-user.target")
machine.succeed("sbctl create-keys") machine.succeed("sbctl create-keys")
machine.succeed("sbctl enroll-keys --yes-this-might-brick-my-machine") 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/systemd/systemd-boot${efiArch}.efi')
machine.succeed('sbctl sign /boot/EFI/BOOT/BOOT${toUpper 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 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 { basicXbootldr = makeTest {
@ -141,80 +154,97 @@ in
nodes.machine = commonXbootldr; nodes.machine = commonXbootldr;
testScript = { nodes, ... }: '' testScript =
${customDiskImage nodes} { nodes, ... }:
''
${customDiskImage nodes}
machine.start() machine.start()
machine.wait_for_unit("multi-user.target") machine.wait_for_unit("multi-user.target")
machine.succeed("test -e /efi/EFI/systemd/systemd-bootx64.efi") 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 /boot/loader/entries/nixos-generation-1.conf")
# Ensure we actually booted using systemd-boot # Ensure we actually booted using systemd-boot
# Magic number is the vendor UUID used by systemd-boot. # Magic number is the vendor UUID used by systemd-boot.
machine.succeed( machine.succeed(
"test -e /sys/firmware/efi/efivars/LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f" "test -e /sys/firmware/efi/efivars/LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
) )
# "bootctl install" should have created an EFI entry # "bootctl install" should have created an EFI entry
machine.succeed('efibootmgr | grep "Linux Boot Manager"') machine.succeed('efibootmgr | grep "Linux Boot Manager"')
''; '';
}; };
# Check that specialisations create corresponding boot entries. # Check that specialisations create corresponding boot entries.
specialisation = makeTest { specialisation = makeTest {
name = "systemd-boot-specialisation"; name = "systemd-boot-specialisation";
meta.maintainers = with pkgs.lib.maintainers; [ lukegb julienmalka ]; meta.maintainers = with pkgs.lib.maintainers; [
lukegb
julienmalka
];
nodes.machine = { pkgs, lib, ... }: { nodes.machine =
imports = [ common ]; { pkgs, lib, ... }:
specialisation.something.configuration = { {
boot.loader.systemd-boot.sortKey = "something"; imports = [ common ];
specialisation.something.configuration = {
boot.loader.systemd-boot.sortKey = "something";
# Since qemu will dynamically create a devicetree blob when starting # Since qemu will dynamically create a devicetree blob when starting
# up, it is not straight forward to create an export of that devicetree # 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 # 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 # (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 # will not boot, but it does allow us to assert that the boot entry has
# the correct contents. # the correct contents.
boot.loader.systemd-boot.installDeviceTree = pkgs.stdenv.hostPlatform.isAarch64; boot.loader.systemd-boot.installDeviceTree = pkgs.stdenv.hostPlatform.isAarch64;
hardware.deviceTree.name = "dummy.dtb"; hardware.deviceTree.name = "dummy.dtb";
hardware.deviceTree.package = lib.mkForce (pkgs.runCommand "dummy-devicetree-package" { } '' hardware.deviceTree.package = lib.mkForce (
mkdir -p $out pkgs.runCommand "dummy-devicetree-package" { } ''
cp ${pkgs.emptyFile} $out/dummy.dtb mkdir -p $out
''); cp ${pkgs.emptyFile} $out/dummy.dtb
''
);
};
}; };
};
testScript = { nodes, ... }: '' testScript =
machine.start() { nodes, ... }:
machine.wait_for_unit("multi-user.target") ''
machine.start()
machine.wait_for_unit("multi-user.target")
machine.succeed( machine.succeed(
"test -e /boot/loader/entries/nixos-generation-1-specialisation-something.conf" "test -e /boot/loader/entries/nixos-generation-1-specialisation-something.conf"
) )
machine.succeed( machine.succeed(
"grep -q 'title NixOS (something)' /boot/loader/entries/nixos-generation-1-specialisation-something.conf" "grep -q 'title NixOS (something)' /boot/loader/entries/nixos-generation-1-specialisation-something.conf"
) )
machine.succeed( machine.succeed(
"grep 'sort-key something' /boot/loader/entries/nixos-generation-1-specialisation-something.conf" "grep 'sort-key something' /boot/loader/entries/nixos-generation-1-specialisation-something.conf"
) )
'' + pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isAarch64 '' ''
machine.succeed( + pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isAarch64 ''
r"grep 'devicetree /EFI/nixos/[a-z0-9]\{32\}.*dummy' /boot/loader/entries/nixos-generation-1-specialisation-something.conf" 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" # Boot without having created an EFI entry--instead using default "/EFI/BOOT/BOOTX64.EFI"
fallback = makeTest { fallback = makeTest {
name = "systemd-boot-fallback"; name = "systemd-boot-fallback";
meta.maintainers = with pkgs.lib.maintainers; [ danielfullmer julienmalka ]; meta.maintainers = with pkgs.lib.maintainers; [
danielfullmer
julienmalka
];
nodes.machine = { pkgs, lib, ... }: { nodes.machine =
imports = [ common ]; { pkgs, lib, ... }:
boot.loader.efi.canTouchEfiVariables = mkForce false; {
}; imports = [ common ];
boot.loader.efi.canTouchEfiVariables = mkForce false;
};
testScript = '' testScript = ''
machine.start() machine.start()
@ -235,7 +265,10 @@ in
update = makeTest { update = makeTest {
name = "systemd-boot-update"; name = "systemd-boot-update";
meta.maintainers = with pkgs.lib.maintainers; [ danielfullmer julienmalka ]; meta.maintainers = with pkgs.lib.maintainers; [
danielfullmer
julienmalka
];
nodes.machine = common; nodes.machine = common;
@ -270,29 +303,35 @@ in
''; '';
}; };
memtest86 = with pkgs.lib; optionalAttrs (meta.availableOn { inherit system; } pkgs.memtest86plus) (makeTest { memtest86 =
name = "systemd-boot-memtest86"; with pkgs.lib;
meta.maintainers = with maintainers; [ julienmalka ]; optionalAttrs (meta.availableOn { inherit system; } pkgs.memtest86plus) (makeTest {
name = "systemd-boot-memtest86";
meta.maintainers = with maintainers; [ julienmalka ];
nodes.machine = { pkgs, lib, ... }: { nodes.machine =
imports = [ common ]; { pkgs, lib, ... }:
boot.loader.systemd-boot.memtest86.enable = true; {
}; imports = [ common ];
boot.loader.systemd-boot.memtest86.enable = true;
};
testScript = '' testScript = ''
machine.succeed("test -e /boot/loader/entries/memtest86.conf") machine.succeed("test -e /boot/loader/entries/memtest86.conf")
machine.succeed("test -e /boot/efi/memtest86/memtest.efi") machine.succeed("test -e /boot/efi/memtest86/memtest.efi")
''; '';
}); });
netbootxyz = makeTest { netbootxyz = makeTest {
name = "systemd-boot-netbootxyz"; name = "systemd-boot-netbootxyz";
meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ]; meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];
nodes.machine = { pkgs, lib, ... }: { nodes.machine =
imports = [ common ]; { pkgs, lib, ... }:
boot.loader.systemd-boot.netbootxyz.enable = true; {
}; imports = [ common ];
boot.loader.systemd-boot.netbootxyz.enable = true;
};
testScript = '' testScript = ''
machine.succeed("test -e /boot/loader/entries/netbootxyz.conf") 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 { memtestSortKey = makeTest {
name = "systemd-boot-memtest-sortkey"; name = "systemd-boot-memtest-sortkey";
meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ]; meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];
nodes.machine = { pkgs, lib, ... }: { nodes.machine =
imports = [ common ]; { pkgs, lib, ... }:
boot.loader.systemd-boot.memtest86.enable = true; {
boot.loader.systemd-boot.memtest86.sortKey = "apple"; imports = [ common ];
}; boot.loader.systemd-boot.memtest86.enable = true;
boot.loader.systemd-boot.memtest86.sortKey = "apple";
};
testScript = '' testScript = ''
machine.succeed("test -e /boot/loader/entries/memtest86.conf") machine.succeed("test -e /boot/loader/entries/memtest86.conf")
@ -321,35 +422,41 @@ in
name = "systemd-boot-entry-filename-xbootldr"; name = "systemd-boot-entry-filename-xbootldr";
meta.maintainers = with pkgs.lib.maintainers; [ sdht0 ]; meta.maintainers = with pkgs.lib.maintainers; [ sdht0 ];
nodes.machine = { pkgs, lib, ... }: { nodes.machine =
imports = [ commonXbootldr ]; { pkgs, lib, ... }:
boot.loader.systemd-boot.memtest86.enable = true; {
}; imports = [ commonXbootldr ];
boot.loader.systemd-boot.memtest86.enable = true;
};
testScript = { nodes, ... }: '' testScript =
${customDiskImage nodes} { nodes, ... }:
''
${customDiskImage nodes}
machine.start() machine.start()
machine.wait_for_unit("multi-user.target") machine.wait_for_unit("multi-user.target")
machine.succeed("test -e /efi/EFI/systemd/systemd-bootx64.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/loader/entries/memtest86.conf")
machine.succeed("test -e /boot/EFI/memtest86/memtest.efi") machine.succeed("test -e /boot/EFI/memtest86/memtest.efi")
''; '';
}; };
extraEntries = makeTest { extraEntries = makeTest {
name = "systemd-boot-extra-entries"; name = "systemd-boot-extra-entries";
meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ]; meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];
nodes.machine = { pkgs, lib, ... }: { nodes.machine =
imports = [ common ]; { pkgs, lib, ... }:
boot.loader.systemd-boot.extraEntries = { {
"banana.conf" = '' imports = [ common ];
title banana boot.loader.systemd-boot.extraEntries = {
''; "banana.conf" = ''
title banana
'';
};
}; };
};
testScript = '' testScript = ''
machine.succeed("test -e /boot/loader/entries/banana.conf") machine.succeed("test -e /boot/loader/entries/banana.conf")
@ -361,12 +468,14 @@ in
name = "systemd-boot-extra-files"; name = "systemd-boot-extra-files";
meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ]; meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];
nodes.machine = { pkgs, lib, ... }: { nodes.machine =
imports = [ common ]; { pkgs, lib, ... }:
boot.loader.systemd-boot.extraFiles = { {
"efi/fruits/tomato.efi" = pkgs.netbootxyz-efi; imports = [ common ];
boot.loader.systemd-boot.extraFiles = {
"efi/fruits/tomato.efi" = pkgs.netbootxyz-efi;
};
}; };
};
testScript = '' testScript = ''
machine.succeed("test -e /boot/efi/fruits/tomato.efi") machine.succeed("test -e /boot/efi/fruits/tomato.efi")
@ -381,55 +490,62 @@ in
nodes = { nodes = {
inherit common; inherit common;
machine = { pkgs, nodes, ... }: { machine =
imports = [ common ]; { pkgs, nodes, ... }:
boot.loader.systemd-boot.extraFiles = { {
"efi/fruits/tomato.efi" = pkgs.netbootxyz-efi; 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` with_netbootxyz =
system.extraDependencies = [ { pkgs, ... }:
nodes.common.system.build.toplevel {
nodes.with_netbootxyz.system.build.toplevel imports = [ common ];
]; boot.loader.systemd-boot.netbootxyz.enable = true;
}; };
with_netbootxyz = { pkgs, ... }: {
imports = [ common ];
boot.loader.systemd-boot.netbootxyz.enable = true;
};
}; };
testScript = { nodes, ... }: let testScript =
originalSystem = nodes.machine.system.build.toplevel; { nodes, ... }:
baseSystem = nodes.common.system.build.toplevel; let
finalSystem = nodes.with_netbootxyz.system.build.toplevel; originalSystem = nodes.machine.system.build.toplevel;
in '' baseSystem = nodes.common.system.build.toplevel;
machine.succeed("test -e /boot/efi/fruits/tomato.efi") finalSystem = nodes.with_netbootxyz.system.build.toplevel;
machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi") 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"): with subtest("remove files when no longer needed"):
machine.succeed("${baseSystem}/bin/switch-to-configuration boot") machine.succeed("${baseSystem}/bin/switch-to-configuration boot")
machine.fail("test -e /boot/efi/fruits/tomato.efi") machine.fail("test -e /boot/efi/fruits/tomato.efi")
machine.fail("test -d /boot/efi/fruits") machine.fail("test -d /boot/efi/fruits")
machine.succeed("test -d /boot/efi/nixos/.extra-files") 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 -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi")
machine.fail("test -d /boot/efi/nixos/.extra-files/efi/fruits") machine.fail("test -d /boot/efi/nixos/.extra-files/efi/fruits")
with subtest("files are added back when needed again"): with subtest("files are added back when needed again"):
machine.succeed("${originalSystem}/bin/switch-to-configuration boot") machine.succeed("${originalSystem}/bin/switch-to-configuration boot")
machine.succeed("test -e /boot/efi/fruits/tomato.efi") machine.succeed("test -e /boot/efi/fruits/tomato.efi")
machine.succeed("test -e /boot/efi/nixos/.extra-files/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"): with subtest("simultaneously removing and adding files works"):
machine.succeed("${finalSystem}/bin/switch-to-configuration boot") machine.succeed("${finalSystem}/bin/switch-to-configuration boot")
machine.fail("test -e /boot/efi/fruits/tomato.efi") machine.fail("test -e /boot/efi/fruits/tomato.efi")
machine.fail("test -e /boot/efi/nixos/.extra-files/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/loader/entries/netbootxyz.conf")
machine.succeed("test -e /boot/efi/netbootxyz/netboot.xyz.efi") 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/loader/entries/netbootxyz.conf")
machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/netbootxyz/netboot.xyz.efi") machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/netbootxyz/netboot.xyz.efi")
''; '';
}; };
garbage-collect-entry = makeTest { garbage-collect-entry = makeTest {
@ -438,17 +554,20 @@ in
nodes = { nodes = {
inherit common; inherit common;
machine = { pkgs, nodes, ... }: { machine =
imports = [ common ]; { pkgs, nodes, ... }:
{
imports = [ common ];
# These are configs for different nodes, but we'll use them here in `machine` # These are configs for different nodes, but we'll use them here in `machine`
system.extraDependencies = [ system.extraDependencies = [
nodes.common.system.build.toplevel nodes.common.system.build.toplevel
]; ];
}; };
}; };
testScript = { nodes, ... }: testScript =
{ nodes, ... }:
let let
baseSystem = nodes.common.system.build.toplevel; baseSystem = nodes.common.system.build.toplevel;
in in
@ -461,19 +580,18 @@ in
''; '';
}; };
no-bootspec = makeTest no-bootspec = makeTest {
{ name = "systemd-boot-no-bootspec";
name = "systemd-boot-no-bootspec"; meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];
meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];
nodes.machine = { nodes.machine = {
imports = [ common ]; imports = [ common ];
boot.bootspec.enable = false; boot.bootspec.enable = false;
};
testScript = ''
machine.start()
machine.wait_for_unit("multi-user.target")
'';
}; };
testScript = ''
machine.start()
machine.wait_for_unit("multi-user.target")
'';
};
} }