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:
@@ -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.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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; };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user