Skip to content

Commit

Permalink
systemd: Add support for XBOOTLDR partition in systemd-boot
Browse files Browse the repository at this point in the history
  • Loading branch information
sdht0 committed Oct 10, 2023
1 parent 8a6c901 commit df24dfd
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 35 deletions.
33 changes: 30 additions & 3 deletions nixos/lib/make-disk-image.nix
Original file line number Diff line number Diff line change
Expand Up @@ -193,11 +193,11 @@ To solve this, you can run `fdisk -l $image` and generate `dd if=$image of=$imag
additionalPaths ? []
}:

assert (lib.assertOneOf "partitionTableType" partitionTableType [ "legacy" "legacy+gpt" "efi" "hybrid" "none" ]);
assert (lib.assertOneOf "partitionTableType" partitionTableType [ "legacy" "legacy+gpt" "efi" "efixbootldr" "hybrid" "none" ]);
assert (lib.assertMsg (fsType == "ext4" && deterministic -> rootFSUID != null) "In deterministic mode with a ext4 partition, rootFSUID must be non-null, by default, it is equal to rootGPUID.");
# We use -E offset=X below, which is only supported by e2fsprogs
assert (lib.assertMsg (partitionTableType != "none" -> fsType == "ext4") "to produce a partition table, we need to use -E offset flag which is support only for fsType = ext4");
assert (lib.assertMsg (touchEFIVars -> partitionTableType == "hybrid" || partitionTableType == "efi" || partitionTableType == "legacy+gpt") "EFI variables can be used only with a partition table of type: hybrid, efi or legacy+gpt.");
assert (lib.assertMsg (touchEFIVars -> partitionTableType == "hybrid" || partitionTableType == "efi" || partitionTableType == "efixbootldr" || partitionTableType == "legacy+gpt") "EFI variables can be used only with a partition table of type: hybrid, efi or legacy+gpt, but sent: ${partitionTableType}.");
# If only Nix store image, then: contents must be empty, configFile must be unset, and we should no install bootloader.
assert (lib.assertMsg (onlyNixStore -> contents == [] && configFile == null && !installBootLoader) "In a only Nix store image, the contents must be empty, no configuration must be provided and no bootloader should be installed.");
# Either both or none of {user,group} need to be set
Expand Down Expand Up @@ -225,6 +225,7 @@ let format' = format; in let
legacy = "1";
"legacy+gpt" = "2";
efi = "2";
efixbootldr = "3";
hybrid = "3";
}.${partitionTableType};

Expand Down Expand Up @@ -266,6 +267,23 @@ let format' = format; in let
$diskImage
''}
'';
efixbootldr = ''
parted --script $diskImage -- \
mklabel gpt \
mkpart ESP fat32 8MiB 100MiB \
set 1 boot on \
mkpart BOOT fat32 100MiB ${bootSize} \
set 2 bls_boot on \
mkpart ROOT ext4 ${bootSize} -1
${optionalString deterministic ''
sgdisk \
--disk-guid=97FD5997-D90B-4AA3-8D16-C1723AEA73C \
--partition-guid=1:C12A7328-F81F-11D2-BA4B-00A0C93EC93B \
--partition-guid=2:BC13C2FF-59E6-4262-A352-B275FD6F7172 \
--partition-guid=3:${rootGPUID} \
$diskImage
''}
'';
hybrid = ''
parted --script $diskImage -- \
mklabel gpt \
Expand Down Expand Up @@ -436,7 +454,7 @@ let format' = format; in let
diskImage=nixos.raw
${if diskSize == "auto" then ''
${if partitionTableType == "efi" || partitionTableType == "hybrid" then ''
${if partitionTableType == "efi" || partitionTableType == "efixbootldr" || partitionTableType == "hybrid" then ''
# Add the GPT at the end
gptSpace=$(( 512 * 34 * 1 ))
# Normally we'd need to account for alignment and things, if bootSize
Expand Down Expand Up @@ -562,6 +580,15 @@ let format' = format; in let
${optionalString touchEFIVars "mount -t efivarfs efivarfs /sys/firmware/efi/efivars"}
''}
${optionalString (partitionTableType == "efixbootldr") ''
mkdir -p /mnt/{boot,efi}
mkfs.vfat -n ESP /dev/vda1
mkfs.vfat -n BOOT /dev/vda2
mount /dev/vda1 /mnt/efi
mount /dev/vda2 /mnt/boot
${optionalString touchEFIVars "mount -t efivarfs efivarfs /sys/firmware/efi/efivars"}
''}
# Install a configuration.nix
mkdir -p /mnt/etc/nixos
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,6 +337,15 @@ 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("@bootMountPoint@", os.O_RDONLY))
if rc != 0:
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)
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
20 changes: 15 additions & 5 deletions nixos/modules/virtualisation/qemu-vm.nix
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ let

};

selectPartitionTableLayout = { useEFIBoot, useDefaultFilesystems }:
selectPartitionTableLayout = { useEFIBoot, useDefaultFilesystems, setupXbootldr }:
if useDefaultFilesystems then
if useEFIBoot then "efi" else "legacy"
if useEFIBoot then (if setupXbootldr then "efixbootldr" else "efi") else "legacy"
else "none";

driveCmdline = idx: { file, driveExtraOpts, deviceExtraOpts, ... }:
Expand Down Expand Up @@ -229,8 +229,11 @@ let
regInfo = hostPkgs.closureInfo { rootPaths = config.virtualisation.additionalPaths; };

# Use well-defined and persistent filesystem labels to identify block devices.
setupXbootldr = config.boot ? loader && config.boot.loader.systemd-boot.xbootldrMountPoint != null;
rootFilesystemLabel = "nixos";
espFilesystemLabel = "ESP"; # Hard-coded by make-disk-image.nix
xbootldrFilesystemLabel = "BOOT"; # Hard-coded by make-disk-image.nix
bootLabel = if setupXbootldr then xbootldrFilesystemLabel else espFilesystemLabel;
nixStoreFilesystemLabel = "nix-store";

# The root drive is a raw disk which does not necessarily contain a
Expand All @@ -249,7 +252,7 @@ let
format = "qcow2";
onlyNixStore = false;
label = rootFilesystemLabel;
partitionTableType = selectPartitionTableLayout { inherit (cfg) useDefaultFilesystems useEFIBoot; };
partitionTableType = selectPartitionTableLayout { inherit (cfg) useDefaultFilesystems useEFIBoot; inherit setupXbootldr; };
# Bootloader should be installed on the system image only if we are booting through bootloaders.
# Though, if a user is not using our default filesystems, it is possible to not have any ESP
# or a strange partition table that's incompatible with GRUB configuration.
Expand Down Expand Up @@ -358,8 +361,8 @@ in
virtualisation.bootPartition =
mkOption {
type = types.nullOr types.path;
default = if cfg.useEFIBoot then "/dev/disk/by-label/${espFilesystemLabel}" else null;
defaultText = literalExpression ''if cfg.useEFIBoot then "/dev/disk/by-label/${espFilesystemLabel}" else null'';
default = if cfg.useEFIBoot then "/dev/disk/by-label/${bootLabel}" else null;
defaultText = literalExpression ''if cfg.useEFIBoot then "/dev/disk/by-label/${bootLabel}" else null'';
example = "/dev/disk/by-label/esp";
description =
lib.mdDoc ''
Expand Down Expand Up @@ -1175,6 +1178,13 @@ in
noCheck = true; # fsck fails on a r/o filesystem
};
}
(lib.optionalAttrs setupXbootldr {
"/efi" = {
device = "/dev/disk/by-label/${espFilesystemLabel}";
fsType = "vfat";
noCheck = true; # fsck fails on a r/o filesystem
};
})
];

boot.initrd.systemd = lib.mkIf (config.boot.initrd.systemd.enable && cfg.writableStore) {
Expand Down
Loading

0 comments on commit df24dfd

Please sign in to comment.