Skip to content

Commit

Permalink
nixos/systemd-boot: Add support for an XBOOTLDR partition
Browse files Browse the repository at this point in the history
  • Loading branch information
sdht0 committed Oct 26, 2023
1 parent 0ddfbd9 commit b9810e2
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 29 deletions.
2 changes: 2 additions & 0 deletions nixos/doc/manual/release-notes/rl-2311.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,8 @@ The module update takes care of the new config syntax and the data itself (user

- New `boot.bcache.enable` (default enabled) allows completely removing `bcache` mount support.

- New `boot.loader.systemd-boot.xbootldrMountPoint` allows setting up a separate [XBOOTLDR partition](https://uapi-group.org/specifications/specs/boot_loader_specification/).

- The module `services.mbpfan` now has the option `aggressive` enabled by default for better heat moderation. You can disable it for upstream defaults.

- `security.sudo` now provides two extra options, that do not change the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ def system_dir(profile: str | None, generation: int, specialisation: str | None)
options {kernel_params}
"""

LOADER_CONF = "@efiSysMountPoint@/loader/loader.conf" # Always stored on the ESP

def generation_conf_filename(profile: str | None, generation: int, specialisation: str | None) -> str:
pieces = [
"nixos",
Expand All @@ -59,7 +61,7 @@ def generation_conf_filename(profile: str | None, generation: int, specialisatio


def write_loader_conf(profile: str | None, generation: int, specialisation: str | None) -> None:
with open("@efiSysMountPoint@/loader/loader.conf.tmp", 'w') as f:
with open(f"{LOADER_CONF}.tmp", 'w') as f:
if "@timeout@" != "":
f.write("timeout @timeout@\n")
f.write("default %s\n" % generation_conf_filename(profile, generation, specialisation))
Expand All @@ -68,7 +70,7 @@ def write_loader_conf(profile: str | None, generation: int, specialisation: str
f.write("console-mode @consoleMode@\n")
f.flush()
os.fsync(f.fileno())
os.rename("@efiSysMountPoint@/loader/loader.conf.tmp", "@efiSysMountPoint@/loader/loader.conf")
os.rename(f"{LOADER_CONF}.tmp", LOADER_CONF)


def profile_path(profile: str | None, generation: int, specialisation: str | None, name: str) -> str:
Expand All @@ -79,9 +81,9 @@ def copy_from_profile(profile: str | None, generation: int, specialisation: str
store_file_path = profile_path(profile, generation, specialisation, name)
suffix = os.path.basename(store_file_path)
store_dir = os.path.basename(os.path.dirname(store_file_path))
efi_file_path = "/efi/nixos/%s-%s.efi" % (store_dir, suffix)
efi_file_path = "/EFI/nixos/%s-%s.efi" % (store_dir, suffix)
if not dry_run:
copy_if_not_exists(store_file_path, "@efiSysMountPoint@%s" % (efi_file_path))
copy_if_not_exists(store_file_path, "@bootMountPoint@%s" % (efi_file_path))
return efi_file_path


Expand Down Expand Up @@ -117,7 +119,7 @@ def write_entry(profile: str | None, generation: int, specialisation: str | None

try:
append_initrd_secrets = profile_path(profile, generation, specialisation, "append-initrd-secrets")
subprocess.check_call([append_initrd_secrets, "@efiSysMountPoint@%s" % (initrd)])
subprocess.check_call([append_initrd_secrets, "@bootMountPoint@%s" % (initrd)])
except FileNotFoundError:
pass
except subprocess.CalledProcessError:
Expand All @@ -129,7 +131,7 @@ def write_entry(profile: str | None, generation: int, specialisation: str | None
f'for "{title} - Configuration {generation}", an older generation', file=sys.stderr)
print("note: this is normal after having removed "
"or renamed a file in `boot.initrd.secrets`", file=sys.stderr)
entry_file = "@efiSysMountPoint@/loader/entries/%s" % (
entry_file = "@bootMountPoint@/loader/entries/%s" % (
generation_conf_filename(profile, generation, specialisation))
tmp_path = "%s.tmp" % (entry_file)
kernel_params = "init=%s " % profile_path(profile, generation, specialisation, "init")
Expand Down Expand Up @@ -182,13 +184,13 @@ def get_specialisations(profile: str | None, generation: int, _: str | None) ->


def remove_old_entries(gens: list[SystemIdentifier]) -> None:
rex_profile = re.compile(r"^@efiSysMountPoint@/loader/entries/nixos-(.*)-generation-.*\.conf$")
rex_generation = re.compile(r"^@efiSysMountPoint@/loader/entries/nixos.*-generation-([0-9]+)(-specialisation-.*)?\.conf$")
rex_profile = re.compile(r"^@bootMountPoint@/loader/entries/nixos-(.*)-generation-.*\.conf$")
rex_generation = re.compile(r"^@bootMountPoint@/loader/entries/nixos.*-generation-([0-9]+)(-specialisation-.*)?\.conf$")
known_paths = []
for gen in gens:
known_paths.append(copy_from_profile(*gen, "kernel", True))
known_paths.append(copy_from_profile(*gen, "initrd", True))
for path in glob.iglob("@efiSysMountPoint@/loader/entries/nixos*-generation-[1-9]*.conf"):
for path in glob.iglob("@bootMountPoint@/loader/entries/nixos*-generation-[1-9]*.conf"):
if rex_profile.match(path):
prof = rex_profile.sub(r"\1", path)
else:
Expand All @@ -199,11 +201,18 @@ def remove_old_entries(gens: list[SystemIdentifier]) -> None:
continue
if not (prof, gen_number, None) in gens:
os.unlink(path)
for path in glob.iglob("@efiSysMountPoint@/efi/nixos/*"):
for path in glob.iglob("@bootMountPoint@/EFI/nixos/*"):
if not path in known_paths and not os.path.isdir(path):
os.unlink(path)


def cleanup_esp() -> None:
for path in glob.iglob("@efiSysMountPoint@/loader/entries/nixos*"):
os.unlink(path)
if os.path.isdir("@efiSysMountPoint@/EFI/nixos"):
shutil.rmtree("@efiSysMountPoint@/EFI/nixos")


def get_profiles() -> list[str]:
if os.path.isdir("/nix/var/nix/profiles/system-profiles/"):
return [x
Expand Down Expand Up @@ -234,6 +243,9 @@ def install_bootloader(args: argparse.Namespace) -> None:
# flags to pass to bootctl install/update
bootctl_flags = []

if "@bootMountPoint@" != "@efiSysMountPoint@":
bootctl_flags.append("--boot-path=@bootMountPoint@")

if "@canTouchEfiVariables@" != "1":
bootctl_flags.append("--no-variables")

Expand All @@ -242,8 +254,8 @@ def install_bootloader(args: argparse.Namespace) -> None:

if os.getenv("NIXOS_INSTALL_BOOTLOADER") == "1":
# bootctl uses fopen() with modes "wxe" and fails if the file exists.
if os.path.exists("@efiSysMountPoint@/loader/loader.conf"):
os.unlink("@efiSysMountPoint@/loader/loader.conf")
if os.path.exists(LOADER_CONF):
os.unlink(LOADER_CONF)

subprocess.check_call(["@systemd@/bin/bootctl", "--esp-path=@efiSysMountPoint@"] + bootctl_flags + ["install"])
else:
Expand All @@ -270,8 +282,8 @@ def install_bootloader(args: argparse.Namespace) -> None:
print("updating systemd-boot from %s to %s" % (installed_version, available_version))
subprocess.check_call(["@systemd@/bin/bootctl", "--esp-path=@efiSysMountPoint@", "update"])

os.makedirs("@efiSysMountPoint@/efi/nixos", exist_ok=True)
os.makedirs("@efiSysMountPoint@/loader/entries", exist_ok=True)
os.makedirs("@bootMountPoint@/EFI/nixos", exist_ok=True)
os.makedirs("@bootMountPoint@/loader/entries", exist_ok=True)

gens = get_generations()
for profile in get_profiles():
Expand All @@ -293,9 +305,9 @@ def install_bootloader(args: argparse.Namespace) -> None:
else:
raise e

for root, _, files in os.walk('@efiSysMountPoint@/efi/nixos/.extra-files', topdown=False):
relative_root = root.removeprefix("@efiSysMountPoint@/efi/nixos/.extra-files").removeprefix("/")
actual_root = os.path.join("@efiSysMountPoint@", relative_root)
for root, _, files in os.walk('@bootMountPoint@/EFI/nixos/.extra-files', topdown=False):
relative_root = root.removeprefix("@bootMountPoint@/EFI/nixos/.extra-files").removeprefix("/")
actual_root = os.path.join("@bootMountPoint@", relative_root)

for file in files:
actual_file = os.path.join(actual_root, file)
Expand All @@ -308,7 +320,7 @@ def install_bootloader(args: argparse.Namespace) -> None:
os.rmdir(actual_root)
os.rmdir(root)

os.makedirs("@efiSysMountPoint@/efi/nixos/.extra-files", exist_ok=True)
os.makedirs("@bootMountPoint@/EFI/nixos/.extra-files", exist_ok=True)

subprocess.check_call("@copyExtraFiles@")

Expand All @@ -325,9 +337,18 @@ def main() -> None:
# it can leave the system in an unbootable state, when a crash/outage
# happens shortly after an update. To decrease the likelihood of this
# event sync the efi filesystem after each update.
rc = libc.syncfs(os.open("@efiSysMountPoint@", os.O_RDONLY))
rc = libc.syncfs(os.open("@bootMountPoint@", os.O_RDONLY))
if rc != 0:
print("could not sync @efiSysMountPoint@: {}".format(os.strerror(rc)), file=sys.stderr)
print("could not sync @bootMountPoint@: {}".format(os.strerror(rc)), file=sys.stderr)

if "@bootMountPoint@" != "@efiSysMountPoint@":
# Cleanup any entries in ESP if xbootldrMountPoint is set.
# If the user later unsets xbootldrMountPoint, entries in XBOOTLDR will not be cleaned up
# automatically, as we don't have information about the mount point anymore.
cleanup_esp()
rc = libc.syncfs(os.open("@efiSysMountPoint@", os.O_RDONLY))
if rc != 0:
print("could not sync @efiSysMountPoint@: {}".format(os.strerror(rc)), file=sys.stderr)


if __name__ == '__main__':
Expand Down
37 changes: 28 additions & 9 deletions nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ let

efi = config.boot.loader.efi;

systemdBootBuilder = pkgs.substituteAll {
systemdBootBuilder = pkgs.substituteAll rec {
src = ./systemd-boot-builder.py;

isExecutable = true;
Expand All @@ -28,6 +28,10 @@ let

inherit (efi) efiSysMountPoint canTouchEfiVariables;

bootMountPoint = if cfg.xbootldrMountPoint != null
then cfg.xbootldrMountPoint
else efi.efiSysMountPoint;

inherit (config.system.nixos) distroName;

memtest86 = optionalString cfg.memtest86.enable pkgs.memtest86plus;
Expand All @@ -38,13 +42,13 @@ let
empty_file=$(${pkgs.coreutils}/bin/mktemp)
${concatStrings (mapAttrsToList (n: v: ''
${pkgs.coreutils}/bin/install -Dp "${v}" "${efi.efiSysMountPoint}/"${escapeShellArg n}
${pkgs.coreutils}/bin/install -D $empty_file "${efi.efiSysMountPoint}/efi/nixos/.extra-files/"${escapeShellArg n}
${pkgs.coreutils}/bin/install -Dp "${v}" "${bootMountPoint}/"${escapeShellArg n}
${pkgs.coreutils}/bin/install -D $empty_file "${bootMountPoint}/EFI/nixos/.extra-files/"${escapeShellArg n}
'') cfg.extraFiles)}
${concatStrings (mapAttrsToList (n: v: ''
${pkgs.coreutils}/bin/install -Dp "${pkgs.writeText n v}" "${efi.efiSysMountPoint}/loader/entries/"${escapeShellArg n}
${pkgs.coreutils}/bin/install -D $empty_file "${efi.efiSysMountPoint}/efi/nixos/.extra-files/loader/entries/"${escapeShellArg n}
${pkgs.coreutils}/bin/install -Dp "${pkgs.writeText n v}" "${bootMountPoint}/loader/entries/"${escapeShellArg n}
${pkgs.coreutils}/bin/install -D $empty_file "${bootMountPoint}/EFI/nixos/.extra-files/loader/entries/"${escapeShellArg n}
'') cfg.extraEntries)}
'';
};
Expand Down Expand Up @@ -94,6 +98,17 @@ in {
'';
};

xbootldrMountPoint = mkOption {
default = null;
type = types.nullOr types.str;
description = lib.mdDoc ''
Where the XBOOTLOADER partition is mounted.
If set, this partition will be used as $BOOT to store boot loader entries and extra files
instead of the ESP.
'';
};

configurationLimit = mkOption {
default = null;
example = 120;
Expand All @@ -103,7 +118,7 @@ in {
Useful to prevent boot partition running out of disk space.
`null` means no limit i.e. all generations
that were not garbage collected yet.
that have not been garbage collected yet.
'';
};

Expand Down Expand Up @@ -195,7 +210,7 @@ in {
'';
description = lib.mdDoc ''
Any additional entries you want added to the `systemd-boot` menu.
These entries will be copied to {file}`/boot/loader/entries`.
These entries will be copied to {file}`$BOOT/loader/entries`.
Each attribute name denotes the destination file name,
and the corresponding attribute value is the contents of the entry.
Expand All @@ -212,9 +227,9 @@ in {
{ "efi/memtest86/memtest.efi" = "''${pkgs.memtest86plus}/memtest.efi"; }
'';
description = lib.mdDoc ''
A set of files to be copied to {file}`/boot`.
A set of files to be copied to {file}`$BOOT`.
Each attribute name denotes the destination file name in
{file}`/boot`, while the corresponding
{file}`$BOOT`, while the corresponding
attribute value specifies the source file.
'';
};
Expand All @@ -238,6 +253,10 @@ in {

config = mkIf cfg.enable {
assertions = [
{
assertion = cfg.xbootldrMountPoint != efi.efiSysMountPoint;
message = "The XBOOTLDR mount point '${cfg.xbootldrMountPoint}' cannot be the same as the ESP mount point '${efi.efiSysMountPoint}'";
}
{
assertion = (config.boot.kernelPackages.kernel.features or { efiBootStub = true; }) ? efiBootStub;
message = "This kernel does not support the EFI boot stub";
Expand Down
48 changes: 48 additions & 0 deletions nixos/tests/systemd-boot.nix
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ let
boot.loader.efi.canTouchEfiVariables = true;
environment.systemPackages = [ pkgs.efibootmgr ];
};

common_xbootldr = {
imports = [ common ];

boot.loader.efi.efiSysMountPoint = "/efi";
boot.loader.systemd-boot.xbootldrMountPoint = "/boot";
};
in
{
basic = makeTest {
Expand All @@ -39,6 +46,30 @@ in
'';
};

basic_xbootldr = makeTest {
name = "systemd-boot-xbootldr";
meta.maintainers = with pkgs.lib.maintainers; [ danielfullmer ];

nodes.machine = common_xbootldr;

testScript = ''
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")
# Ensure we actually booted using systemd-boot
# Magic number is the vendor UUID used by systemd-boot.
machine.succeed(
"test -e /sys/firmware/efi/efivars/LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
)
# "bootctl install" should have created an EFI entry
machine.succeed('efibootmgr | grep "Linux Boot Manager"')
'';
};

# Check that specialisations create corresponding boot entries.
specialisation = makeTest {
name = "systemd-boot-specialisation";
Expand Down Expand Up @@ -158,6 +189,23 @@ in
'';
};

entryFilename_xbootldr = makeTest {
name = "systemd-boot-entry-filename";
meta.maintainers = with pkgs.lib.maintainers; [ Enzime ];

nodes.machine = { pkgs, lib, ... }: {
imports = [ common_xbootldr ];
boot.loader.systemd-boot.memtest86.enable = true;
boot.loader.systemd-boot.memtest86.entryFilename = "apple.conf";
};

testScript = ''
machine.fail("test -e /boot/loader/entries/memtest86.conf")
machine.succeed("test -e /boot/loader/entries/apple.conf")
machine.succeed("test -e /boot/efi/memtest86/memtest.efi")
'';
};

extraEntries = makeTest {
name = "systemd-boot-extra-entries";
meta.maintainers = with pkgs.lib.maintainers; [ Enzime ];
Expand Down

0 comments on commit b9810e2

Please sign in to comment.