mirror of
https://github.com/NixOS/nixpkgs.git
synced 2026-07-06 17:13:24 -05:00
systemd v261 lists the new systemd-boot-fallback binary in `bootctl status`, so our regex picked up the older fallback version and ran `bootctl update` on an already current ESP, which then exits non-zero. Switch from parsing the `bootctl --status` output to having bootctl compare versions itself by calling io.systemd.BootControl.Install over Varlink. bootctl is spawned as a stdio Varlink server so that we always use the bootctl from the target closure. An already current binary comes back as an io.systemd.System error carrying ESTALE, which we treat as success.
949 lines
30 KiB
Nix
949 lines
30 KiB
Nix
{
|
|
runTest,
|
|
runTestOn,
|
|
lib,
|
|
...
|
|
}:
|
|
|
|
let
|
|
testScriptPreamble =
|
|
# python
|
|
''
|
|
def check_current_system(system_path):
|
|
machine.succeed(f'test $(readlink -f /run/current-system) = "{system_path}"')
|
|
|
|
def check_generation(generation: int, tries_left=0, tries_failed=0, specialisation=None) -> list[str]:
|
|
if specialisation:
|
|
title = f"NixOS ({specialisation})"
|
|
else:
|
|
title = "NixOS"
|
|
|
|
conf_files = machine.succeed(
|
|
f"grep --files-with-matches 'version Generation {generation} NixOS' /boot/loader/entries/nixos-*.conf | xargs grep --line-regexp --fixed-strings --files-with-matches 'title {title}'"
|
|
).split("\n")
|
|
|
|
suffix = ""
|
|
if tries_left:
|
|
suffix += f"+{tries_left}"
|
|
if tries_failed:
|
|
suffix += f"-{tries_failed}"
|
|
|
|
assert conf_files[0].endswith(f"{suffix}.conf"), f"Expected {conf_files[0]} to end with {suffix}.conf"
|
|
return conf_files
|
|
'';
|
|
|
|
common =
|
|
{ pkgs, ... }:
|
|
{
|
|
virtualisation.useBootLoader = true;
|
|
virtualisation.useEFIBoot = true;
|
|
boot.loader.systemd-boot.enable = true;
|
|
boot.loader.efi.canTouchEfiVariables = true;
|
|
environment.systemPackages = [ pkgs.efibootmgr ];
|
|
system.switch.enable = true;
|
|
# Needed for machine-id to be persisted between reboots.
|
|
# Must be a valid (non-zero) ID, otherwise sd_id128_get_machine()
|
|
# returns -ENOMEDIUM and dbus-broker refuses to start.
|
|
environment.etc."machine-id".text = "1234567890abcdef1234567890abcdef\n";
|
|
};
|
|
|
|
commonXbootldr =
|
|
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
let
|
|
diskImage = import ../lib/make-disk-image.nix {
|
|
inherit config lib pkgs;
|
|
label = "nixos";
|
|
format = "qcow2";
|
|
partitionTableType = "efixbootldr";
|
|
touchEFIVars = true;
|
|
installBootLoader = true;
|
|
# Don't copy the channel to avoid rebuilding this image, and all tests
|
|
# that use it, every time that nixpkgs changes
|
|
copyChannel = false;
|
|
};
|
|
in
|
|
{
|
|
imports = [ common ];
|
|
virtualisation.useBootLoader = lib.mkForce false; # Only way to tell qemu-vm not to create the default system image
|
|
virtualisation.directBoot.enable = false; # But don't direct boot either because we're testing systemd-boot
|
|
|
|
system.build.diskImage = diskImage; # Use custom disk image with an XBOOTLDR partition
|
|
virtualisation.efi.variables = "${diskImage}/efi-vars.fd";
|
|
|
|
virtualisation.useDefaultFilesystems = false; # Needs custom setup for `diskImage`
|
|
virtualisation.bootPartition = null;
|
|
virtualisation.fileSystems = {
|
|
"/" = {
|
|
device = "/dev/vda3";
|
|
fsType = "ext4";
|
|
};
|
|
"/boot" = {
|
|
device = "/dev/vda2";
|
|
fsType = "vfat";
|
|
noCheck = true;
|
|
};
|
|
"/efi" = {
|
|
device = "/dev/vda1";
|
|
fsType = "vfat";
|
|
noCheck = true;
|
|
};
|
|
};
|
|
|
|
boot.loader.systemd-boot.enable = true;
|
|
boot.loader.efi.efiSysMountPoint = "/efi";
|
|
boot.loader.systemd-boot.xbootldrMountPoint = "/boot";
|
|
};
|
|
|
|
customDiskImage =
|
|
nodes:
|
|
# python
|
|
''
|
|
import os
|
|
import subprocess
|
|
import tempfile
|
|
|
|
tmp_disk_image = tempfile.NamedTemporaryFile()
|
|
|
|
subprocess.run([
|
|
"${nodes.machine.virtualisation.qemu.package}/bin/qemu-img",
|
|
"create",
|
|
"-f",
|
|
"qcow2",
|
|
"-b",
|
|
"${nodes.machine.system.build.diskImage}/nixos.qcow2",
|
|
"-F",
|
|
"qcow2",
|
|
tmp_disk_image.name,
|
|
])
|
|
|
|
# Set NIX_DISK_IMAGE so that the qemu script finds the right disk image.
|
|
os.environ['NIX_DISK_IMAGE'] = tmp_disk_image.name
|
|
'';
|
|
|
|
# Check that we are booting the default entry and not the generation with largest version number
|
|
defaultEntry =
|
|
{
|
|
withBootCounting ? false,
|
|
...
|
|
}:
|
|
runTest {
|
|
name = "systemd-boot-default-entry" + lib.optionalString withBootCounting "-with-boot-counting";
|
|
meta.maintainers = with lib.maintainers; [ julienmalka ];
|
|
|
|
nodes = {
|
|
machine =
|
|
{ nodes, ... }:
|
|
{
|
|
imports = [ common ];
|
|
system.extraDependencies = [ nodes.other_machine.system.build.toplevel ];
|
|
boot.loader.systemd-boot.bootCounting.enable = withBootCounting;
|
|
};
|
|
|
|
other_machine =
|
|
{ pkgs, ... }:
|
|
{
|
|
imports = [ common ];
|
|
boot.loader.systemd-boot.bootCounting.enable = withBootCounting;
|
|
environment.systemPackages = [ pkgs.hello ];
|
|
};
|
|
};
|
|
testScript =
|
|
{ nodes, ... }:
|
|
let
|
|
orig = nodes.machine.system.build.toplevel;
|
|
other = nodes.other_machine.system.build.toplevel;
|
|
in
|
|
# python
|
|
''
|
|
${testScriptPreamble}
|
|
|
|
orig = "${orig}"
|
|
other = "${other}"
|
|
|
|
check_current_system(orig)
|
|
|
|
# Switch to other configuration
|
|
machine.succeed("nix-env -p /nix/var/nix/profiles/system --set ${other}")
|
|
machine.succeed(f"{other}/bin/switch-to-configuration boot")
|
|
# Rollback, default entry is now generation 1
|
|
machine.succeed("nix-env -p /nix/var/nix/profiles/system --rollback")
|
|
machine.succeed(f"{orig}/bin/switch-to-configuration boot")
|
|
machine.shutdown()
|
|
machine.start()
|
|
machine.wait_for_unit("multi-user.target")
|
|
# Check that we booted generation 1 (default)
|
|
# even though generation 2 comes first in alphabetical order
|
|
check_current_system(orig)
|
|
'';
|
|
};
|
|
|
|
garbage-collect-entry =
|
|
{
|
|
withBootCounting ? false,
|
|
...
|
|
}:
|
|
runTest (
|
|
{ lib, ... }:
|
|
{
|
|
name =
|
|
"systemd-boot-garbage-collect-entry" + lib.optionalString withBootCounting "-with-boot-counting";
|
|
meta.maintainers = with lib.maintainers; [ julienmalka ];
|
|
|
|
nodes = {
|
|
inherit common;
|
|
machine =
|
|
{ nodes, ... }:
|
|
{
|
|
imports = [ common ];
|
|
|
|
boot.loader.systemd-boot.bootCounting.enable = withBootCounting;
|
|
boot.loader.systemd-boot.memtest86.enable = true;
|
|
|
|
# These are configs for different nodes, but we'll use them here in `machine`
|
|
system.extraDependencies = [
|
|
nodes.common.system.build.toplevel
|
|
];
|
|
};
|
|
};
|
|
|
|
testScript =
|
|
{ nodes, ... }:
|
|
let
|
|
baseSystem = nodes.common.system.build.toplevel;
|
|
in
|
|
# python
|
|
''
|
|
${testScriptPreamble}
|
|
|
|
machine.succeed("nix-env -p /nix/var/nix/profiles/system --set ${baseSystem}")
|
|
machine.succeed("nix-env -p /nix/var/nix/profiles/system --delete-generations 1")
|
|
|
|
conf_file = check_generation(1)[0]
|
|
new_conf_file = conf_file.replace(".conf", "+1-3.conf")
|
|
|
|
# At this point generation 1 has already been marked as good so we reintroduce counters artificially
|
|
${lib.optionalString withBootCounting ''
|
|
machine.succeed(f"mv {conf_file} {new_conf_file}")
|
|
''}
|
|
machine.succeed("${baseSystem}/bin/switch-to-configuration boot")
|
|
machine.fail(
|
|
"grep --files-with-matches 'version Generation 1 NixOS' /boot/loader/entries/nixos-*.conf"
|
|
)
|
|
check_generation(2)
|
|
'';
|
|
}
|
|
);
|
|
in
|
|
{
|
|
defaultEntry = defaultEntry { };
|
|
garbage-collect-entry = garbage-collect-entry { };
|
|
|
|
basic = runTest (
|
|
{ lib, ... }:
|
|
{
|
|
name = "systemd-boot";
|
|
meta.maintainers = with lib.maintainers; [
|
|
danielfullmer
|
|
julienmalka
|
|
];
|
|
|
|
nodes.machine = common;
|
|
|
|
testScript = # python
|
|
''
|
|
${testScriptPreamble}
|
|
|
|
machine.start()
|
|
machine.wait_for_unit("multi-user.target")
|
|
|
|
conf_file = check_generation(1)[0]
|
|
|
|
machine.succeed(f"grep 'sort-key nixos' {conf_file}")
|
|
|
|
# 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"')
|
|
'';
|
|
}
|
|
);
|
|
|
|
# Test that systemd-boot works with secure boot
|
|
secureBoot = runTest (
|
|
{ pkgs, lib, ... }:
|
|
{
|
|
name = "systemd-boot-secure-boot";
|
|
|
|
nodes.machine =
|
|
{ pkgs, ... }:
|
|
{
|
|
imports = [ common ];
|
|
environment.systemPackages = [ pkgs.sbctl ];
|
|
virtualisation.useSecureBoot = true;
|
|
};
|
|
|
|
testScript =
|
|
{ nodes, ... }:
|
|
let
|
|
efiArch = pkgs.stdenv.hostPlatform.efiArch;
|
|
in
|
|
#python
|
|
''
|
|
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${lib.toUpper efiArch}.EFI')
|
|
machine.succeed('sbctl sign /boot/EFI/nixos/*${nodes.machine.system.boot.loader.kernelFile}.efi')
|
|
|
|
machine.reboot()
|
|
|
|
assert "Secure Boot: enabled (user)" in machine.succeed("bootctl status")
|
|
'';
|
|
}
|
|
);
|
|
|
|
# TODO: Fix on aarch64-linux
|
|
basicXbootldr = runTestOn [ "x86_64-linux" ] (
|
|
{ lib, ... }:
|
|
{
|
|
name = "systemd-boot-xbootldr";
|
|
meta.maintainers = with lib.maintainers; [ sdht0 ];
|
|
|
|
nodes.machine = commonXbootldr;
|
|
|
|
testScript =
|
|
{ nodes, ... }:
|
|
#python
|
|
''
|
|
${testScriptPreamble}
|
|
|
|
${customDiskImage nodes}
|
|
|
|
machine.start()
|
|
machine.wait_for_unit("multi-user.target")
|
|
|
|
machine.succeed("test -e /efi/EFI/systemd/systemd-bootx64.efi")
|
|
check_generation(1)
|
|
|
|
# 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"')
|
|
'';
|
|
}
|
|
);
|
|
|
|
# Check that specialisations create corresponding boot entries.
|
|
specialisation = runTest (
|
|
{ pkgs, lib, ... }:
|
|
{
|
|
name = "systemd-boot-specialisation";
|
|
meta.maintainers = with lib.maintainers; [
|
|
lukegb
|
|
julienmalka
|
|
];
|
|
|
|
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
|
|
''
|
|
);
|
|
};
|
|
};
|
|
|
|
testScript =
|
|
# python
|
|
''
|
|
${testScriptPreamble}
|
|
|
|
machine.start()
|
|
machine.wait_for_unit("multi-user.target")
|
|
|
|
conf_files = check_generation(1, specialisation="something")
|
|
machine.succeed(
|
|
f"grep --fixed-strings --line-regexp 'sort-key something' {" ".join(conf_files)}"
|
|
)
|
|
|
|
${lib.optionalString pkgs.stdenv.hostPlatform.isAarch64
|
|
#python
|
|
''
|
|
machine.succeed(
|
|
fr"grep 'devicetree /EFI/nixos/[a-z0-9]\{32\}.*dummy' {" ".join(conf_files)}"
|
|
)
|
|
''
|
|
}
|
|
'';
|
|
}
|
|
);
|
|
|
|
# Boot without having created an EFI entry--instead using default "/EFI/BOOT/BOOTX64.EFI"
|
|
fallback = runTest (
|
|
{ lib, ... }:
|
|
{
|
|
name = "systemd-boot-fallback";
|
|
meta.maintainers = with lib.maintainers; [
|
|
danielfullmer
|
|
julienmalka
|
|
];
|
|
|
|
nodes.machine =
|
|
{ lib, ... }:
|
|
{
|
|
imports = [ common ];
|
|
boot.loader.efi.canTouchEfiVariables = lib.mkForce false;
|
|
};
|
|
|
|
testScript =
|
|
# python
|
|
''
|
|
${testScriptPreamble}
|
|
|
|
machine.start()
|
|
machine.wait_for_unit("multi-user.target")
|
|
|
|
check_generation(1)
|
|
|
|
# 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 _not_ have created an EFI entry
|
|
machine.fail('efibootmgr | grep "Linux Boot Manager"')
|
|
'';
|
|
}
|
|
);
|
|
|
|
# TODO: Fix on aarch64-linux
|
|
update = runTestOn [ "x86_64-linux" ] (
|
|
{ lib, ... }:
|
|
{
|
|
name = "systemd-boot-update";
|
|
meta.maintainers = with lib.maintainers; [
|
|
danielfullmer
|
|
julienmalka
|
|
];
|
|
|
|
nodes.machine = common;
|
|
|
|
testScript =
|
|
let
|
|
oldVersion = "222";
|
|
in
|
|
# python
|
|
''
|
|
machine.succeed("mount -o remount,rw /boot")
|
|
|
|
def switch():
|
|
# Replace version inside sd-boot with something older. See magic[] string in systemd src/boot/efi/boot.c
|
|
machine.succeed(
|
|
"""
|
|
find /boot -iname '*boot*.efi' -print0 | \
|
|
xargs -0 -I '{}' sed -i 's/#### LoaderInfo: systemd-boot .* ####/#### LoaderInfo: systemd-boot ${oldVersion} ####/' '{}'
|
|
"""
|
|
)
|
|
return machine.succeed("/run/current-system/bin/switch-to-configuration boot 2>&1")
|
|
|
|
output = switch()
|
|
assert 'to "/boot/EFI/systemd/systemd-bootx64.efi"' in output, "systemd-boot not copied to to /boot/EFI/systemd/systemd-bootx64.efi"
|
|
assert 'to "/boot/EFI/BOOT/BOOTX64.EFI"' in output, "systemd-boot not copied to to /boot/EFI/BOOT/BOOTX64.EFI"
|
|
|
|
with subtest("Test that updating works with lowercase bootx64.efi"):
|
|
machine.succeed(
|
|
# Move to tmp file name first, otherwise mv complains the new location is the same
|
|
"mv /boot/EFI/BOOT/BOOTX64.EFI /boot/EFI/BOOT/bootx64.efi.new",
|
|
"mv /boot/EFI/BOOT/bootx64.efi.new /boot/EFI/BOOT/bootx64.efi",
|
|
)
|
|
output = switch()
|
|
assert 'to "/boot/EFI/systemd/systemd-bootx64.efi"' in output, "systemd-boot not copied to to /boot/EFI/systemd/systemd-bootx64.efi"
|
|
assert 'to "/boot/EFI/BOOT/BOOTX64.EFI"' in output, "systemd-boot not copied to to /boot/EFI/BOOT/BOOTX64.EFI"
|
|
|
|
with subtest("Test that switching with an up-to-date bootloader is a no-op"):
|
|
output = machine.succeed("/run/current-system/bin/switch-to-configuration boot 2>&1")
|
|
assert "same boot loader version in place already" in output, "Expected bootctl to skip already-current binary"
|
|
'';
|
|
}
|
|
);
|
|
|
|
memtest86 = runTestOn [ "x86_64-linux" ] (
|
|
{ lib, ... }:
|
|
{
|
|
name = "systemd-boot-memtest86";
|
|
meta.maintainers = with lib.maintainers; [ julienmalka ];
|
|
|
|
nodes.machine = {
|
|
imports = [ common ];
|
|
boot.loader.systemd-boot.memtest86.enable = true;
|
|
};
|
|
|
|
testScript =
|
|
# python
|
|
''
|
|
machine.succeed("test -e /boot/loader/entries/memtest86.conf")
|
|
machine.succeed("test -e /boot/efi/memtest86/memtest.efi")
|
|
'';
|
|
}
|
|
);
|
|
|
|
netbootxyz = runTest (
|
|
{ lib, ... }:
|
|
{
|
|
name = "systemd-boot-netbootxyz";
|
|
meta.maintainers = with lib.maintainers; [ julienmalka ];
|
|
|
|
nodes.machine = {
|
|
imports = [ common ];
|
|
boot.loader.systemd-boot.netbootxyz.enable = true;
|
|
};
|
|
|
|
testScript =
|
|
# python
|
|
''
|
|
machine.succeed("test -e /boot/loader/entries/netbootxyz.conf")
|
|
machine.succeed("test -e /boot/efi/netbootxyz/netboot.xyz.efi")
|
|
'';
|
|
}
|
|
);
|
|
|
|
edk2-uefi-shell = runTest (
|
|
{ lib, ... }:
|
|
{
|
|
name = "systemd-boot-edk2-uefi-shell";
|
|
meta.maintainers = with lib.maintainers; [ iFreilicht ];
|
|
|
|
nodes.machine =
|
|
{ ... }:
|
|
{
|
|
imports = [ common ];
|
|
boot.loader.systemd-boot.edk2-uefi-shell.enable = true;
|
|
};
|
|
|
|
testScript =
|
|
# python
|
|
''
|
|
machine.succeed("test -e /boot/loader/entries/edk2-uefi-shell.conf")
|
|
machine.succeed("test -e /boot/efi/edk2-uefi-shell/shell.efi")
|
|
'';
|
|
}
|
|
);
|
|
|
|
windows = runTest (
|
|
{ lib, ... }:
|
|
{
|
|
name = "systemd-boot-windows";
|
|
meta.maintainers = with 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 =
|
|
# python
|
|
''
|
|
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 = runTest (
|
|
{ lib, ... }:
|
|
{
|
|
name = "systemd-boot-memtest-sortkey";
|
|
meta.maintainers = with lib.maintainers; [ julienmalka ];
|
|
|
|
nodes.machine = {
|
|
imports = [ common ];
|
|
boot.loader.systemd-boot.memtest86.enable = true;
|
|
boot.loader.systemd-boot.memtest86.sortKey = "apple";
|
|
};
|
|
|
|
testScript =
|
|
# python
|
|
''
|
|
machine.succeed("test -e /boot/loader/entries/memtest86.conf")
|
|
machine.succeed("test -e /boot/efi/memtest86/memtest.efi")
|
|
machine.succeed("grep 'sort-key apple' /boot/loader/entries/memtest86.conf")
|
|
'';
|
|
}
|
|
);
|
|
|
|
# TODO: Fix on aarch64-linux
|
|
entryFilenameXbootldr = runTestOn [ "x86_64-linux" ] (
|
|
{ lib, ... }:
|
|
{
|
|
name = "systemd-boot-entry-filename-xbootldr";
|
|
meta.maintainers = with lib.maintainers; [ sdht0 ];
|
|
|
|
nodes.machine = {
|
|
imports = [ commonXbootldr ];
|
|
boot.loader.systemd-boot.memtest86.enable = true;
|
|
};
|
|
|
|
testScript =
|
|
{ nodes, ... }:
|
|
# python
|
|
''
|
|
${customDiskImage nodes}
|
|
|
|
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")
|
|
'';
|
|
}
|
|
);
|
|
|
|
extraEntries = runTest (
|
|
{ lib, ... }:
|
|
{
|
|
name = "systemd-boot-extra-entries";
|
|
meta.maintainers = with lib.maintainers; [ julienmalka ];
|
|
|
|
nodes.machine = {
|
|
imports = [ common ];
|
|
boot.loader.systemd-boot.extraEntries = {
|
|
"banana.conf" = ''
|
|
title banana
|
|
'';
|
|
};
|
|
};
|
|
|
|
testScript =
|
|
# python
|
|
''
|
|
machine.succeed("test -e /boot/loader/entries/banana.conf")
|
|
machine.succeed("test -e /boot/efi/nixos/.extra-files/loader/entries/banana.conf")
|
|
'';
|
|
}
|
|
);
|
|
|
|
extraFiles = runTest (
|
|
{ lib, ... }:
|
|
{
|
|
name = "systemd-boot-extra-files";
|
|
meta.maintainers = with lib.maintainers; [ julienmalka ];
|
|
|
|
nodes.machine =
|
|
{ pkgs, ... }:
|
|
{
|
|
imports = [ common ];
|
|
boot.loader.systemd-boot.extraFiles = {
|
|
"efi/fruits/tomato.efi" = pkgs.netbootxyz-efi;
|
|
};
|
|
};
|
|
|
|
testScript =
|
|
# python
|
|
''
|
|
machine.succeed("test -e /boot/efi/fruits/tomato.efi")
|
|
machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi")
|
|
'';
|
|
}
|
|
);
|
|
|
|
switch-test = runTest (
|
|
{ lib, ... }:
|
|
{
|
|
name = "systemd-boot-switch-test";
|
|
meta.maintainers = with lib.maintainers; [ julienmalka ];
|
|
|
|
nodes = {
|
|
inherit common;
|
|
|
|
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
|
|
];
|
|
};
|
|
|
|
with_netbootxyz = {
|
|
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
|
|
# python
|
|
''
|
|
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("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")
|
|
'';
|
|
}
|
|
);
|
|
|
|
bootCounting =
|
|
let
|
|
baseConfig = {
|
|
imports = [ common ];
|
|
|
|
boot.loader.systemd-boot.bootCounting = {
|
|
enable = true;
|
|
tries = 2;
|
|
};
|
|
};
|
|
in
|
|
runTest {
|
|
name = "systemd-boot-counting";
|
|
meta.maintainers = with lib.maintainers; [ julienmalka ];
|
|
|
|
nodes = {
|
|
machine =
|
|
{ nodes, ... }:
|
|
{
|
|
imports = [ baseConfig ];
|
|
system.extraDependencies = [
|
|
nodes.bad_machine.system.build.toplevel
|
|
nodes.unused_machine.system.build.toplevel
|
|
];
|
|
};
|
|
|
|
unused_machine =
|
|
{ pkgs, ... }:
|
|
{
|
|
imports = [ baseConfig ];
|
|
# Distinguish this closure from `machine` without pulling in extra deps.
|
|
environment.systemPackages = [ pkgs.hello ];
|
|
};
|
|
|
|
bad_machine = {
|
|
imports = [ baseConfig ];
|
|
|
|
systemd.services."failing" = {
|
|
script = "exit 1";
|
|
requiredBy = [ "boot-complete.target" ];
|
|
before = [ "boot-complete.target" ];
|
|
serviceConfig.Type = "oneshot";
|
|
};
|
|
};
|
|
};
|
|
testScript =
|
|
{ nodes, ... }:
|
|
let
|
|
orig = nodes.machine.system.build.toplevel;
|
|
bad = nodes.bad_machine.system.build.toplevel;
|
|
unused = nodes.unused_machine.system.build.toplevel;
|
|
in
|
|
# python
|
|
''
|
|
${testScriptPreamble}
|
|
|
|
orig = "${orig}"
|
|
bad = "${bad}"
|
|
unused = "${unused}"
|
|
|
|
machine.start(allow_reboot=True)
|
|
|
|
# Ensure we booted using an entry with counters enabled
|
|
machine.succeed(
|
|
"test -e /sys/firmware/efi/efivars/LoaderBootCountPath-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
|
|
)
|
|
|
|
# systemd-bless-boot should have already removed the "+2" suffix from the boot entry
|
|
machine.wait_for_unit("systemd-bless-boot.service")
|
|
conf_file = check_generation(1)
|
|
check_current_system(orig)
|
|
|
|
print(machine.succeed("cat /boot/loader/entries/*.conf"))
|
|
|
|
# Register the bad configuration as generation 2 and another good
|
|
# configuration as generation 3, then make generation 2 the default.
|
|
# This verifies that `preferred` in loader.conf selects gen 2 even
|
|
# though gen 3 sorts higher, and that once gen 2 is marked bad we
|
|
# fall back to the newest non-bad entry (gen 3).
|
|
machine.succeed("nix-env -p /nix/var/nix/profiles/system --set ${bad}")
|
|
machine.succeed("nix-env -p /nix/var/nix/profiles/system --set ${unused}")
|
|
machine.succeed(f"{bad}/bin/switch-to-configuration boot")
|
|
|
|
# Ensure new bootloader entries have initialized counters
|
|
check_generation(1)
|
|
check_generation(2, 2)
|
|
check_generation(3, 2)
|
|
|
|
machine.reboot()
|
|
|
|
machine.wait_for_unit("multi-user.target")
|
|
check_current_system(bad)
|
|
check_generation(1)
|
|
check_generation(2, 1, 1)
|
|
check_generation(3, 2)
|
|
|
|
machine.reboot()
|
|
|
|
machine.wait_for_unit("multi-user.target")
|
|
check_current_system(bad)
|
|
check_generation(1)
|
|
check_generation(2, 0, 2)
|
|
check_generation(3, 2)
|
|
|
|
machine.reboot()
|
|
|
|
machine.wait_for_unit("multi-user.target")
|
|
# Gen 2 has exhausted its tries; `preferred` skips it and `default
|
|
# nixos-*` resolves to the newest non-bad entry, which is gen 3.
|
|
check_current_system(unused)
|
|
machine.wait_for_unit("systemd-bless-boot.service")
|
|
check_generation(2, 0, 2)
|
|
check_generation(3)
|
|
'';
|
|
};
|
|
|
|
bootCountingSpecialisation =
|
|
let
|
|
baseConfig = {
|
|
imports = [ common ];
|
|
boot.loader.systemd-boot.bootCounting = {
|
|
enable = true;
|
|
tries = 2;
|
|
};
|
|
};
|
|
|
|
specialisationName = "+something+-+that+-+breaks-parsing+-+";
|
|
in
|
|
runTest {
|
|
name = "systemd-boot-counting-specialisation";
|
|
meta.maintainers = with lib.maintainers; [ julienmalka ];
|
|
|
|
nodes = {
|
|
machine =
|
|
{ nodes, lib, ... }:
|
|
{
|
|
imports = [ baseConfig ];
|
|
specialisation.${specialisationName}.configuration = {
|
|
boot.loader.systemd-boot.sortKey = "something";
|
|
};
|
|
};
|
|
};
|
|
testScript =
|
|
{ nodes, ... }:
|
|
let
|
|
orig = nodes.machine.system.build.toplevel;
|
|
in
|
|
# python
|
|
''
|
|
${testScriptPreamble}
|
|
|
|
orig = "${orig}"
|
|
|
|
# Ensure we booted using an entry with counters enabled
|
|
machine.succeed(
|
|
"test -e /sys/firmware/efi/efivars/LoaderBootCountPath-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
|
|
)
|
|
|
|
check_generation(1)
|
|
check_current_system(orig)
|
|
|
|
# Ensure the bootloader entry for the specialisation has initialized the boot counter
|
|
check_generation(1, 2, specialisation="${specialisationName}")
|
|
'';
|
|
};
|
|
|
|
defaultEntryWithBootCounting = defaultEntry { withBootCounting = true; };
|
|
|
|
garbageCollectEntryWithBootCounting = garbage-collect-entry { withBootCounting = true; };
|
|
}
|