nixos/systemd-boot-builder: store boot loader configs using content hashing

Co-Authored-By: AkechiShiro <14914796+AkechiShiro@users.noreply.github.com>
This commit is contained in:
r-vdp
2026-06-02 12:20:51 +03:00
co-authored by AkechiShiro
parent 323ef6c123
commit b4c278c06b
5 changed files with 555 additions and 426 deletions
@@ -26,4 +26,6 @@
<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
- `boot.loader.systemd-boot` gained support for [Automatic Boot Assessment](https://systemd.io/AUTOMATIC_BOOT_ASSESSMENT/) via the new [`boot.loader.systemd-boot.bootCounting`](#opt-boot.loader.systemd-boot.bootCounting.enable) options, allowing automatic detection of and recovery from bad NixOS generations. As part of this change, boot loader entries on the ESP/XBOOTLDR partition are now named `nixos-<content-hash>.conf` instead of `nixos-generation-<n>.conf`; existing entries are migrated automatically on the next `nixos-rebuild boot`/`switch`.
- The `newuidmap` and `newgidmap` security wrappers are now installed with `cap_setuid`/`cap_setgid` file capabilities instead of the setuid-root bit, matching shadow's `--with-fcaps` install mode and other major distributions. Rootless containers (podman, docker-rootless, unprivileged user namespaces) are unaffected. The only behavioural change is that mapping host uid 0 via `/etc/subuid` (which NixOS never configures by default) additionally requires `cap_setfcap`; users who explicitly grant uid 0 in a subuid range can restore the previous behaviour with `security.wrappers.newuidmap.capabilities = lib.mkForce "cap_setuid,cap_setfcap+ep";`.
@@ -3,6 +3,7 @@ import argparse
import ctypes
import datetime
import errno
import hashlib
import os
import re
import shutil
@@ -11,8 +12,7 @@ import sys
import tempfile
import warnings
import json
import glob
from typing import NamedTuple, Any, Sequence, Type
from typing import NamedTuple, Any, Sequence
from dataclasses import dataclass
from pathlib import Path
@@ -32,8 +32,8 @@ NIX = "@nix@"
SYSTEMD = "@systemd@"
CONFIGURATION_LIMIT = int("@configurationLimit@")
REBOOT_FOR_BITLOCKER = bool("@rebootForBitlocker@")
CAN_TOUCH_EFI_VARIABLES = "@canTouchEfiVariables@"
GRACEFUL = "@graceful@"
CAN_TOUCH_EFI_VARIABLES = "@canTouchEfiVariables@" == "1"
GRACEFUL = "@graceful@" == "1"
COPY_EXTRA_FILES = "@copyExtraFiles@"
CHECK_MOUNTPOINTS = "@checkMountpoints@"
STORE_DIR = "@storeDir@"
@@ -41,7 +41,7 @@ BOOT_COUNTING_TRIES = "@bootCountingTries@"
BOOT_COUNTING = "@bootCounting@" == "True"
@dataclass
@dataclass(frozen=True)
class BootSpec:
init: Path
initrd: Path
@@ -56,44 +56,31 @@ class BootSpec:
initrdSecrets: str | None = None # noqa: N815
@dataclass
@dataclass(frozen=True)
class GcRoot:
prefix: Path | None
path: Path | None
@staticmethod
def from_prefix(prefix: Path) -> "GcRoot":
return GcRoot(prefix=prefix, path=None)
@staticmethod
def from_path(path: Path) -> "GcRoot":
return GcRoot(prefix=None, path=path)
@dataclass(frozen=True)
class Entry:
profile: str | None
generation_number: int
specialisation: str | None
@classmethod
def from_path(cls: Type["Entry"], path: Path) -> "Entry":
filename = path.name
# Matching nixos-$profile-generation-*.conf
rex_profile = re.compile(r"^nixos-(.*)-generation-.*\.conf$")
# Matching nixos*-generation-$number*.conf
rex_generation = re.compile(r"^nixos.*-generation-([0-9]+).*\.conf$")
# Matching nixos*-generation-$number-specialisation-$specialisation_name*.conf
rex_specialisation = re.compile(
r"^nixos.*-generation-([0-9]+)-specialisation-([a-zA-Z0-9]+).*\.conf$"
)
profile = (
rex_profile.sub(r"\1", filename) if rex_profile.match(filename) else None
)
specialisation = (
rex_specialisation.sub(r"\2", filename)
if rex_specialisation.match(filename)
else None
)
try:
generation_number = int(rex_generation.sub(r"\1", filename))
except ValueError:
raise
return cls(profile, generation_number, specialisation)
@dataclass
@dataclass(frozen=True)
class DiskEntry:
entry: Entry
default: bool
counters: str | None
title: str | None
description: str | None
kernel: Path
initrd: Path
@@ -102,87 +89,84 @@ class DiskEntry:
machine_id: str | None
sort_key: str
@classmethod
def from_path(cls: Type["DiskEntry"], path: Path) -> "DiskEntry":
entry = Entry.from_path(path)
data = path.read_text().splitlines()
if "" in data:
data.remove("")
entry_map = dict(lines.split(" ", 1) for lines in data)
assert "linux" in entry_map
assert "initrd" in entry_map
filename = path.name
# Matching nixos*-generation-*$counters.conf
rex_counters = re.compile(r"^nixos.*-generation-.*(\+\d(-\d)?)\.conf$")
counters = (
rex_counters.sub(r"\1", filename) if rex_counters.match(filename) else None
@property
def title(self) -> str:
return "{name}{profile}{specialisation}".format(
name=DISTRO_NAME,
profile=" [" + self.entry.profile + "]" if self.entry.profile else "",
specialisation=" (%s)" % self.entry.specialisation
if self.entry.specialisation
else "",
)
maybe_devicetree_path = entry_map.get("devicetree")
disk_entry = cls(
entry=entry,
default=(entry_map.get("sort-key") == "default"),
counters=counters,
title=entry_map.get("title"),
description=entry_map.get("version"),
kernel=Path(entry_map["linux"]),
initrd=Path(entry_map["initrd"]),
devicetree=Path(maybe_devicetree_path) if maybe_devicetree_path else None,
machine_id=entry_map.get("machine-id"),
sort_key=entry_map.get("sort_key", "nixos"),
kernel_params=entry_map.get("options"),
)
return disk_entry
def serialise(self) -> str:
boot_entry = [
f"title {self.title}",
f"version {self.description}" if self.description is not None else None,
f"linux /{self.kernel}",
f"initrd /{self.initrd}",
f"options {self.kernel_params}" if self.kernel_params is not None else None,
f"machine-id {self.machine_id}" if self.machine_id is not None else None,
f"devicetree /{self.devicetree}" if self.devicetree is not None else None,
f"sort-key {self.sort_key}",
]
return "\n".join(filter(None, boot_entry))
def write(self, sorted_first: str) -> None:
# TODO
# Compute a sort-key sorted before sorted_first
# This will compute something like: nixos -> nixor-default to make sure we come before other nixos entries,
# while allowing users users can pre-pend their own entries before.
default_sort_key = (
sorted_first[:-1] + chr(ord(sorted_first[-1]) - 1) + "-default"
)
def write(self) -> GcRoot:
# Check first if the file already exists
for e in os.scandir(path=BOOT_MOUNT_POINT / "loader" / "entries"):
match = re.fullmatch(
rf"{self.path_prefix}(\+[0-9]+(-[0-9]+)?)?\.conf", e.name
)
if match:
# Check that the contents match the hash
with open(e.path, "r") as f:
hash = hashlib.sha256(f.read().encode("utf-8")).hexdigest()
if hash == self.content_hash:
# The contents match, we are done, there is nothing to write
return GcRoot.from_prefix(
BOOT_MOUNT_POINT / "loader" / "entries" / self.path_prefix
)
# We didn't find a matching file, so we'll create one
tmp_path = self.path.with_suffix(".tmp")
with tmp_path.open("w") as f:
# We use "sort-key" to sort the default generation first.
# The "default" string is sorted before "non-default" (alphabetically)
boot_entry = [
f"title {self.title}" if self.title is not None else None,
f"version {self.description}" if self.description is not None else None,
f"linux {self.kernel}",
f"initrd {self.initrd}",
f"options {self.kernel_params}"
if self.kernel_params is not None
else None,
f"machine-id {self.machine_id}"
if self.machine_id is not None
else None,
f"devicetree /{self.devicetree}"
if self.devicetree is not None
else None,
f"sort-key {default_sort_key if self.default else self.sort_key}",
]
boot_entry = self.serialise()
f.write("\n".join(filter(None, boot_entry)))
f.write(boot_entry)
f.flush()
os.fsync(f.fileno())
tmp_path.rename(self.path)
return GcRoot.from_prefix(
BOOT_MOUNT_POINT / "loader" / "entries" / self.path_prefix
)
@property
def content_hash(self) -> str:
return hashlib.sha256(self.serialise().encode("utf-8")).hexdigest()
@property
def path_prefix(self) -> str:
return "-".join(
p
for p in [
"nixos",
self.content_hash,
]
if p
)
@property
def path(self) -> Path:
pieces = [
"nixos",
self.entry.profile or None,
"generation",
str(self.entry.generation_number),
f"specialisation-{self.entry.specialisation}"
if self.entry.specialisation
else None,
]
prefix = "-".join(p for p in pieces if p)
return Path(
f"{BOOT_MOUNT_POINT}/loader/entries/{prefix}{self.counters if self.counters else ''}.conf"
)
return BOOT_MOUNT_POINT / "loader" / "entries" / self.filename
@property
def filename(self) -> str:
return f"{self.path_prefix}{self.counters if self.counters else ''}.conf"
@property
def bootctl_id(self) -> str:
return f"{self.path_prefix}.conf"
libc = ctypes.CDLL("libc.so.6")
@@ -193,7 +177,7 @@ FILE = None | int
def run(
cmd: Sequence[str | Path], stdout: FILE = None
) -> subprocess.CompletedProcess[str]:
return subprocess.run(cmd, check=True, text=True, stdout=stdout)
return subprocess.run(cmd, check=True, text=True, stdout=stdout, stderr=sys.stderr)
class SystemIdentifier(NamedTuple):
@@ -231,14 +215,25 @@ def system_dir(
return d
def write_loader_conf(profile: str | None) -> None:
def write_loader_conf(default_entry_id: str | None) -> None:
tmp = LOADER_CONF.with_suffix(".tmp")
with tmp.open("x") as f:
f.write(f"timeout {TIMEOUT}\n")
if profile:
f.write("default nixos-%s-generation-*\n" % profile)
if default_entry_id is None:
# No generation matched the requested default config; fall back to
# the newest entry as determined by Boot Loader Spec sorting.
f.write("default nixos-*\n")
elif BOOT_COUNTING:
# `preferred` (systemd-boot >= 260) honours boot assessment, so a
# generation that exhausted its boot counter is skipped and we fall
# through to `default`. systemd-boot sorts entries with
# tries_left == 0 to the end of the list and resolves the `default`
# glob against that order, so `nixos-*` yields the newest entry that
# is not bad, or a bad one only if every nixos entry is bad.
f.write(f"preferred {default_entry_id}\n")
f.write("default nixos-*\n")
else:
f.write("default nixos-generation-*\n")
f.write(f"default {default_entry_id}\n")
if not EDITOR:
f.write("editor 0\n")
if REBOOT_FOR_BITLOCKER:
@@ -249,23 +244,6 @@ def write_loader_conf(profile: str | None) -> None:
os.rename(tmp, LOADER_CONF)
def scan_entries() -> list[DiskEntry]:
"""
Scan all entries in $ESP/loader/entries/*
Does not support Type 2 entries as we do not support them for now.
Returns a generator of Entry.
"""
entries = []
for path in Path(f"{EFI_SYS_MOUNT_POINT}/loader/entries/").glob(
"nixos*-generation-[1-9]*.conf"
):
try:
entries.append(DiskEntry.from_path(path))
except ValueError:
continue
return entries
def get_bootspec(profile: str | None, generation: int) -> BootSpec:
system_directory = system_dir(profile, generation, None)
boot_json_path = (system_directory / "boot.json").resolve()
@@ -316,7 +294,7 @@ def bootspec_from_json(bootspec_json: dict[str, Any]) -> BootSpec:
)
def copy_from_file(file: Path, dry_run: bool = False) -> Path:
def copy_from_file(file: Path) -> Path:
"""
Copy a file to the boot filesystem (XBOOTLDR if in use, otherwise ESP), basing the destination filename on the store path that's being copied from. Return the destination path, relative to the boot filesystem mountpoint.
"""
@@ -326,8 +304,7 @@ def copy_from_file(file: Path, dry_run: bool = False) -> Path:
efi_file_path = NIXOS_DIR / (
f"{suffix}.efi" if suffix == store_subdir else f"{store_subdir}-{suffix}.efi"
)
if not dry_run:
copy_if_not_exists(store_file_path, BOOT_MOUNT_POINT / efi_file_path)
copy_if_not_exists(store_file_path, BOOT_MOUNT_POINT / efi_file_path)
return efi_file_path
@@ -337,22 +314,41 @@ def write_entry(
specialisation: str | None,
machine_id: str | None,
bootspec: BootSpec,
entries: list[DiskEntry],
sorted_first: str,
current: bool,
) -> None:
) -> tuple[DiskEntry, set[GcRoot]]:
gc_roots = set()
if specialisation:
bootspec = bootspec.specialisations[specialisation]
kernel = copy_from_file(bootspec.kernel)
gc_roots.add(GcRoot.from_path(path=(BOOT_MOUNT_POINT / kernel)))
initrd = copy_from_file(bootspec.initrd)
gc_roots.add(GcRoot.from_path(path=(BOOT_MOUNT_POINT / initrd)))
devicetree = (
copy_from_file(bootspec.devicetree) if bootspec.devicetree is not None else None
)
if devicetree is not None:
gc_roots.add(GcRoot.from_path(path=(BOOT_MOUNT_POINT / devicetree)))
title = "{name}{profile}{specialisation}".format(
name=DISTRO_NAME,
profile=" [" + profile + "]" if profile else "",
specialisation=" (%s)" % specialisation if specialisation else "",
kernel_params = "init=%s " % bootspec.init
kernel_params = kernel_params + " ".join(bootspec.kernelParams)
build_time = int(system_dir(profile, generation, specialisation).stat().st_ctime)
build_date = datetime.datetime.fromtimestamp(build_time).strftime("%F")
counters = f"+{BOOT_COUNTING_TRIES}" if BOOT_COUNTING else None
entry = Entry(profile, generation, specialisation)
disk_entry = DiskEntry(
entry=entry,
kernel=kernel,
initrd=initrd,
devicetree=devicetree,
counters=counters,
kernel_params=kernel_params,
machine_id=machine_id,
description=f"Generation {generation} {bootspec.label}, built on {build_date}",
sort_key=bootspec.sortKey,
)
try:
@@ -365,7 +361,7 @@ def write_entry(
else:
print(
"warning: failed to create initrd secrets "
f'for "{title} - Configuration {generation}", an older generation',
f'for "{disk_entry.title} - Configuration {generation}", an older generation',
file=sys.stderr,
)
print(
@@ -373,35 +369,9 @@ def write_entry(
"or renamed a file in `boot.initrd.secrets`",
file=sys.stderr,
)
kernel_params = "init=%s " % bootspec.init
kernel_params = kernel_params + " ".join(bootspec.kernelParams)
build_time = int(system_dir(profile, generation, specialisation).stat().st_ctime)
build_date = datetime.datetime.fromtimestamp(build_time).strftime("%F")
counters = f"+{BOOT_COUNTING_TRIES}" if BOOT_COUNTING else ""
entry = Entry(profile, generation, specialisation)
# We check if the entry we are writing is already on disk
# and we update its "default entry" status
for entry_on_disk in entries:
if entry == entry_on_disk.entry:
entry_on_disk.default = current
entry_on_disk.write(sorted_first)
return
DiskEntry(
entry=entry,
title=title,
kernel=kernel,
initrd=initrd,
devicetree=devicetree,
counters=counters,
kernel_params=kernel_params,
machine_id=machine_id,
description=f"Generation {generation} {bootspec.label}, built on {build_date}",
sort_key=bootspec.sortKey,
default=current,
).write(sorted_first)
gc_roots.add(disk_entry.write())
return disk_entry, gc_roots
def get_generations(profile: str | None = None) -> list[SystemIdentifier]:
@@ -428,32 +398,8 @@ def get_generations(profile: str | None = None) -> list[SystemIdentifier]:
return configurations[-configurationLimit:]
def remove_old_entries(
gens: list[SystemIdentifier], disk_entries: list[DiskEntry]
) -> None:
known_paths = []
for gen in gens:
bootspec = get_bootspec(gen.profile, gen.generation)
known_paths.append(copy_from_file(bootspec.kernel, True).name)
known_paths.append(copy_from_file(bootspec.initrd, True).name)
if bootspec.devicetree is not None:
known_paths.append(copy_from_file(bootspec.devicetree, True).name)
for disk_entry in disk_entries:
if (
disk_entry.entry.profile,
disk_entry.entry.generation_number,
None,
) not in gens:
os.unlink(disk_entry.path)
for path_name in glob.iglob(f"{EFI_SYS_MOUNT_POINT}/efi/nixos/*"):
path = Path(path_name)
if path.name not in known_paths and not path.is_dir():
path.unlink()
def cleanup_esp() -> None:
for path in (EFI_SYS_MOUNT_POINT / "loader/entries").glob("nixos*"):
for path in (EFI_SYS_MOUNT_POINT / "loader" / "entries").glob("nixos*"):
path.unlink()
nixos_dir = EFI_SYS_MOUNT_POINT / NIXOS_DIR
if nixos_dir.is_dir():
@@ -492,10 +438,10 @@ def install_bootloader(args: argparse.Namespace) -> None:
if BOOT_MOUNT_POINT != EFI_SYS_MOUNT_POINT:
bootctl_flags.append(f"--boot-path={BOOT_MOUNT_POINT}")
if CAN_TOUCH_EFI_VARIABLES != "1":
if not CAN_TOUCH_EFI_VARIABLES:
bootctl_flags.append("--no-variables")
if GRACEFUL == "1":
if GRACEFUL:
bootctl_flags.append("--graceful")
if os.getenv("NIXOS_INSTALL_BOOTLOADER") == "1":
@@ -565,15 +511,34 @@ def install_bootloader(args: argparse.Namespace) -> None:
for profile in get_profiles():
gens += get_generations(profile)
entries = scan_entries()
remove_old_entries(gens, entries)
# Compute the sort-key that will be sorted first.
sorted_first = ""
gc_roots: set[GcRoot] = set()
default_config = Path(args.default_config)
default_entry_id: str | None = None
for gen in gens:
try:
bootspec = get_bootspec(gen.profile, gen.generation)
if bootspec.sortKey < sorted_first or sorted_first == "":
sorted_first = bootspec.sortKey
is_default = Path(bootspec.init).parent == default_config
disk_entry, new_gc_roots = write_entry(
*gen, machine_id, bootspec, current=is_default
)
gc_roots.update(new_gc_roots)
if is_default:
default_entry_id = disk_entry.bootctl_id
for specialisation_name, specialisation in bootspec.specialisations.items():
is_default = Path(specialisation.init).parent == default_config
disk_entry, new_gc_roots = write_entry(
gen.profile,
gen.generation,
specialisation_name,
machine_id,
bootspec,
current=is_default,
)
gc_roots.update(new_gc_roots)
if is_default:
default_entry_id = disk_entry.bootctl_id
except OSError as e:
# See https://github.com/NixOS/nixpkgs/issues/114552
if e.errno == errno.EINVAL:
@@ -589,44 +554,7 @@ def install_bootloader(args: argparse.Namespace) -> None:
else:
raise e
for gen in gens:
try:
bootspec = get_bootspec(gen.profile, gen.generation)
is_default = Path(bootspec.init).parent == Path(args.default_config)
write_entry(
*gen, machine_id, bootspec, entries, sorted_first, current=is_default
)
for specialisation in bootspec.specialisations.keys():
write_entry(
gen.profile,
gen.generation,
specialisation,
machine_id,
bootspec,
entries,
sorted_first,
current=(
is_default
and bootspec.specialisations[specialisation].sortKey
== bootspec.sortKey
),
)
if is_default:
write_loader_conf(gen.profile)
except OSError as e:
# See https://github.com/NixOS/nixpkgs/issues/114552
if e.errno == errno.EINVAL:
profile = (
f"profile '{gen.profile}'" if gen.profile else "default profile"
)
print(
"ignoring {} in the list of boot entries because of the following error:\n{}".format(
profile, e
),
file=sys.stderr,
)
else:
raise e
write_loader_conf(default_entry_id)
if BOOT_MOUNT_POINT != EFI_SYS_MOUNT_POINT:
# Cleanup any entries in ESP if xbootldrMountPoint is set.
@@ -634,6 +562,16 @@ def install_bootloader(args: argparse.Namespace) -> None:
# automatically, as we don't have information about the mount point anymore.
cleanup_esp()
# Garbage-collect stale kernels/initrds/entries before re-populating extra
# files, so that user-supplied extraEntries (which may also live under
# loader/entries and start with `nixos-`) are not removed again.
garbage_collect(gc_roots)
remove_extra_files()
run([COPY_EXTRA_FILES])
def remove_extra_files() -> None:
extra_files_dir = BOOT_MOUNT_POINT / NIXOS_DIR / ".extra-files"
for root, _, files in extra_files_dir.walk(top_down=False):
relative_root = root.relative_to(extra_files_dir)
@@ -650,7 +588,31 @@ def install_bootloader(args: argparse.Namespace) -> None:
extra_files_dir.mkdir(parents=True, exist_ok=True)
run([COPY_EXTRA_FILES])
def garbage_collect(gc_roots: set[GcRoot]) -> None:
# Check if a file is in the list of gc roots.
# For prefixes, we need to allow for the potential presence of boot counters.
def has_gc_root(p: Path) -> bool:
for root in gc_roots:
if root.path and root.path == p:
return True
elif root.prefix and re.fullmatch(
rf"{re.escape(str(root.prefix))}(\+[0-9]+(-[0-9]+)?)?\.conf", str(p)
):
return True
return False
def delete_path(e: os.DirEntry) -> None:
if e.is_file(follow_symlinks=True) and not has_gc_root(Path(e.path)):
os.remove(e.path)
for e in os.scandir(BOOT_MOUNT_POINT / NIXOS_DIR):
delete_path(e)
for e in os.scandir(BOOT_MOUNT_POINT / "loader" / "entries"):
match = re.fullmatch(r"nixos-.+\.conf", e.name)
if match:
delete_path(e)
def main() -> None:
@@ -421,11 +421,22 @@ in
};
bootCounting = {
enable = mkEnableOption "automatic boot assessment";
enable = mkEnableOption ''
[Automatic Boot Assessment](https://systemd.io/AUTOMATIC_BOOT_ASSESSMENT/).
New boot entries are written with a boot counter in the file name. On
each boot, systemd-boot decrements the counter; once the booted system
reaches `boot-complete.target`, `systemd-bless-boot.service` removes the
counter and marks the entry as good. An entry whose counter reaches zero
is considered bad and will be skipped in favour of an older generation
'';
tries = mkOption {
default = 3;
type = types.int;
description = "number of tries each entry should start with";
type = types.ints.positive;
description = ''
Number of boot attempts a freshly written entry is given before it is
considered bad.
'';
};
};
+4 -1
View File
@@ -1593,7 +1593,10 @@ in
systemd = runTest ./systemd.nix;
systemd-analyze = runTest ./systemd-analyze.nix;
systemd-binfmt = handleTestOn [ "x86_64-linux" ] ./systemd-binfmt.nix { };
systemd-boot = import ./systemd-boot.nix { inherit runTest runTestOn; };
systemd-boot = import ./systemd-boot.nix {
inherit runTest runTestOn;
inherit (pkgs) lib;
};
systemd-bpf = runTest ./systemd-bpf.nix;
systemd-capsules = runTest ./systemd-capsules.nix;
systemd-confinement = handleTest ./systemd-confinement { };
+337 -186
View File
@@ -1,10 +1,37 @@
{
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, ... }:
{
@@ -72,39 +99,41 @@ let
boot.loader.systemd-boot.xbootldrMountPoint = "/boot";
};
customDiskImage = nodes: ''
import os
import subprocess
import tempfile
customDiskImage =
nodes:
# python
''
import os
import subprocess
import tempfile
tmp_disk_image = tempfile.NamedTemporaryFile()
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,
])
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
'';
# 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 =
{
lib,
pkgs,
withBootCounting ? false,
...
}:
runTest {
name = "systemd-boot-default-entry" + lib.optionalString withBootCounting "-with-boot-counting";
meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];
meta.maintainers = with lib.maintainers; [ julienmalka ];
nodes = {
machine =
@@ -115,11 +144,13 @@ let
boot.loader.systemd-boot.bootCounting.enable = withBootCounting;
};
other_machine = {
imports = [ common ];
boot.loader.systemd-boot.bootCounting.enable = withBootCounting;
environment.systemPackages = [ pkgs.hello ];
};
other_machine =
{ pkgs, ... }:
{
imports = [ common ];
boot.loader.systemd-boot.bootCounting.enable = withBootCounting;
environment.systemPackages = [ pkgs.hello ];
};
};
testScript =
{ nodes, ... }:
@@ -127,13 +158,13 @@ let
orig = nodes.machine.system.build.toplevel;
other = nodes.other_machine.system.build.toplevel;
in
# python
''
${testScriptPreamble}
orig = "${orig}"
other = "${other}"
def check_current_system(system_path):
machine.succeed(f'test $(readlink -f /run/current-system) = "{system_path}"')
check_current_system(orig)
# Switch to other configuration
@@ -150,6 +181,7 @@ let
check_current_system(orig)
'';
};
garbage-collect-entry =
{
withBootCounting ? false,
@@ -170,6 +202,7 @@ let
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 = [
@@ -183,22 +216,33 @@ let
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("mv /boot/loader/entries/nixos-generation-1.conf /boot/loader/entries/nixos-generation-1+3.conf")
machine.succeed(f"mv {conf_file} {new_conf_file}")
''}
machine.succeed("${baseSystem}/bin/switch-to-configuration boot")
machine.fail("test -e /boot/loader/entries/nixos-generation-1*")
machine.succeed("test -e /boot/loader/entries/nixos-generation-2.conf")
machine.fail(
"grep --files-with-matches 'version Generation 1 NixOS' /boot/loader/entries/nixos-*.conf"
)
check_generation(2)
'';
}
);
in
{
inherit defaultEntry;
defaultEntry = defaultEntry { };
garbage-collect-entry = garbage-collect-entry { };
basic = runTest (
{ lib, ... }:
{
@@ -210,25 +254,26 @@ in
nodes.machine = common;
testScript = ''
machine.start()
machine.wait_for_unit("multi-user.target")
testScript = # python
''
${testScriptPreamble}
machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf")
out = machine.succeed("cat /boot/loader/entries/nixos-generation-1.conf")
print(out)
# our sort-key will uses r to sort before nixos
machine.succeed("grep 'sort-key nixor-default' /boot/loader/entries/nixos-generation-1.conf")
machine.start()
machine.wait_for_unit("multi-user.target")
# 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"
)
conf_file = check_generation(1)[0]
# "bootctl install" should have created an EFI entry
machine.succeed('efibootmgr | grep "Linux Boot Manager"')
'';
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"')
'';
}
);
@@ -251,6 +296,7 @@ in
let
efiArch = pkgs.stdenv.hostPlatform.efiArch;
in
#python
''
machine.start(allow_reboot=True)
machine.wait_for_unit("multi-user.target")
@@ -279,14 +325,17 @@ in
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")
machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf")
check_generation(1)
# Ensure we actually booted using systemd-boot
# Magic number is the vendor UUID used by systemd-boot.
@@ -334,25 +383,28 @@ in
};
};
testScript = ''
machine.start()
machine.wait_for_unit("multi-user.target")
testScript =
# python
''
${testScriptPreamble}
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.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)}"
)
''
}
'';
}
);
@@ -373,21 +425,25 @@ in
boot.loader.efi.canTouchEfiVariables = lib.mkForce false;
};
testScript = ''
machine.start()
machine.wait_for_unit("multi-user.target")
testScript =
# python
''
${testScriptPreamble}
machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf")
machine.start()
machine.wait_for_unit("multi-user.target")
# 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"
)
check_generation(1)
# "bootctl install" should _not_ have created an EFI entry
machine.fail('efibootmgr | grep "Linux Boot Manager"')
'';
# 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"')
'';
}
);
@@ -403,35 +459,37 @@ in
nodes.machine = common;
testScript = ''
machine.succeed("mount -o remount,rw /boot")
testScript =
# 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 000.0-1-notnixos ####/' '{}'
"""
)
return machine.succeed("/run/current-system/bin/switch-to-configuration boot 2>&1")
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 000.0-1-notnixos ####/' '{}'
"""
)
return machine.succeed("/run/current-system/bin/switch-to-configuration boot 2>&1")
output = switch()
assert "updating systemd-boot from 000.0-1-notnixos to " in output, "Couldn't find systemd-boot update message"
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"
output = switch()
assert "updating systemd-boot from 000.0-1-notnixos to " in output, "Couldn't find systemd-boot update message"
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 "updating systemd-boot from 000.0-1-notnixos to " in output, "Couldn't find systemd-boot update message"
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 "updating systemd-boot from 000.0-1-notnixos to " in output, "Couldn't find systemd-boot update message"
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"
'';
}
);
@@ -446,10 +504,12 @@ in
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 =
# python
''
machine.succeed("test -e /boot/loader/entries/memtest86.conf")
machine.succeed("test -e /boot/efi/memtest86/memtest.efi")
'';
}
);
@@ -464,10 +524,12 @@ in
boot.loader.systemd-boot.netbootxyz.enable = true;
};
testScript = ''
machine.succeed("test -e /boot/loader/entries/netbootxyz.conf")
machine.succeed("test -e /boot/efi/netbootxyz/netboot.xyz.efi")
'';
testScript =
# python
''
machine.succeed("test -e /boot/loader/entries/netbootxyz.conf")
machine.succeed("test -e /boot/efi/netbootxyz/netboot.xyz.efi")
'';
}
);
@@ -484,10 +546,12 @@ in
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")
'';
testScript =
# python
''
machine.succeed("test -e /boot/loader/entries/edk2-uefi-shell.conf")
machine.succeed("test -e /boot/efi/edk2-uefi-shell/shell.efi")
'';
}
);
@@ -515,29 +579,31 @@ in
};
};
testScript = ''
machine.succeed("test -e /boot/efi/edk2-uefi-shell/shell.efi")
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("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 '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 '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 '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')
'';
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')
'';
}
);
@@ -553,11 +619,13 @@ in
boot.loader.systemd-boot.memtest86.sortKey = "apple";
};
testScript = ''
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")
'';
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")
'';
}
);
@@ -575,6 +643,7 @@ in
testScript =
{ nodes, ... }:
# python
''
${customDiskImage nodes}
@@ -603,10 +672,12 @@ in
};
};
testScript = ''
machine.succeed("test -e /boot/loader/entries/banana.conf")
machine.succeed("test -e /boot/efi/nixos/.extra-files/loader/entries/banana.conf")
'';
testScript =
# python
''
machine.succeed("test -e /boot/loader/entries/banana.conf")
machine.succeed("test -e /boot/efi/nixos/.extra-files/loader/entries/banana.conf")
'';
}
);
@@ -625,10 +696,12 @@ in
};
};
testScript = ''
machine.succeed("test -e /boot/efi/fruits/tomato.efi")
machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.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")
'';
}
);
@@ -669,6 +742,7 @@ in
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")
@@ -709,10 +783,12 @@ in
boot.bootspec.enable = false;
};
testScript = ''
machine.start()
machine.wait_for_unit("multi-user.target")
'';
testScript =
# python
''
machine.start()
machine.wait_for_unit("multi-user.target")
'';
}
);
@@ -720,21 +796,34 @@ in
let
baseConfig = {
imports = [ common ];
boot.loader.systemd-boot.bootCounting.enable = true;
boot.loader.systemd-boot.bootCounting.trials = 2;
boot.loader.systemd-boot.bootCounting = {
enable = true;
tries = 2;
};
};
in
{ pkgs, ... }:
runTest {
name = "systemd-boot-counting";
meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];
meta.maintainers = with lib.maintainers; [ julienmalka ];
nodes = {
machine =
{ nodes, ... }:
{
imports = [ baseConfig ];
system.extraDependencies = [ nodes.bad_machine.system.build.toplevel ];
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 = {
@@ -753,13 +842,17 @@ in
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}"
def check_current_system(system_path):
machine.succeed(f'test $(readlink -f /run/current-system) = "{system_path}"')
machine.start(allow_reboot=True)
# Ensure we booted using an entry with counters enabled
machine.succeed(
@@ -768,46 +861,104 @@ in
# systemd-bless-boot should have already removed the "+2" suffix from the boot entry
machine.wait_for_unit("systemd-bless-boot.service")
machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf")
conf_file = check_generation(1)
check_current_system(orig)
# Switch to bad configuration
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 entry has initialized counter
machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf")
machine.succeed("test -e /boot/loader/entries/nixos-generation-2+2.conf")
machine.shutdown()
# Ensure new bootloader entries have initialized counters
check_generation(1)
check_generation(2, 2)
check_generation(3, 2)
machine.reboot()
machine.start()
machine.wait_for_unit("multi-user.target")
check_current_system(bad)
machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf")
machine.succeed("test -e /boot/loader/entries/nixos-generation-2+1-1.conf")
machine.shutdown()
check_generation(1)
check_generation(2, 1, 1)
check_generation(3, 2)
machine.reboot()
machine.start()
machine.wait_for_unit("multi-user.target")
check_current_system(bad)
machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf")
machine.succeed("test -e /boot/loader/entries/nixos-generation-2+0-2.conf")
machine.shutdown()
check_generation(1)
check_generation(2, 0, 2)
check_generation(3, 2)
machine.reboot()
# Should boot back into original configuration
machine.start()
check_current_system(orig)
machine.wait_for_unit("multi-user.target")
machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf")
machine.succeed("test -e /boot/loader/entries/nixos-generation-2+0-2.conf")
machine.shutdown()
# 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)
'';
};
defaultEntryWithBootCounting =
{ lib, pkgs }:
defaultEntry {
inherit lib pkgs;
withBootCounting = true;
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; };
}