Merge pull request #285401 from sdht0/systemd-boot-xbootldr

nixos/systemd-boot: Add support for an XBOOTLDR partition
This commit is contained in:
Will Fancher 2024-02-27 20:35:03 -05:00 committed by GitHub
commit a587a6a357
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 263 additions and 45 deletions

View File

@ -17174,6 +17174,12 @@
fingerprint = "E173 237A C782 296D 98F5 ADAC E13D FD4B 4712 7951";
}];
};
sdht0 = {
email = "nixpkgs@sdht.in";
github = "sdht0";
githubId = 867424;
name = "Siddhartha Sahu";
};
sdier = {
email = "scott@dier.name";
matrix = "@sdier:matrix.org";

View File

@ -283,6 +283,10 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m
- Cinnamon has been updated to 6.0. Please beware that the [Wayland session](https://blog.linuxmint.com/?p=4591) is still experimental in this release.
- New `boot.loader.systemd-boot.xbootldrMountPoint` allows setting up a separate [XBOOTLDR partition](https://uapi-group.org/specifications/specs/boot_loader_specification/) to store boot files. Useful on systems with a small EFI System partition that cannot be easily repartitioned.
- `boot.loader.systemd-boot` will now verify that `efiSysMountPoint` (and `xbootldrMountPoint` if configured) are mounted partitions.
- `services.postgresql.extraPlugins` changed its type from just a list of packages to also a function that returns such a list.
For example a config line like ``services.postgresql.extraPlugins = with pkgs.postgresql_11.pkgs; [ postgis ];`` is recommended to be changed to ``services.postgresql.extraPlugins = ps: with ps; [ postgis ];``;

View File

@ -56,6 +56,14 @@ This partition table type uses GPT and:
- creates an FAT32 ESP partition from 8MiB to specified `bootSize` parameter (256MiB by default), set it bootable ;
- creates an primary ext4 partition starting after the boot partition and extending to the full disk image
#### `efixbootldr`
This partition table type uses GPT and:
- creates an FAT32 ESP partition from 8MiB to 100MiB, set it bootable ;
- creates an FAT32 BOOT partition from 100MiB to specified `bootSize` parameter (256MiB by default), set `bls_boot` flag ;
- creates an primary ext4 partition starting after the boot partition and extending to the full disk image
#### `hybrid`
This partition table type uses GPT and:
@ -111,19 +119,7 @@ To solve this, you can run `fdisk -l $image` and generate `dd if=$image of=$imag
# When setting one of `user' or `group', the other needs to be set too.
contents ? []
, # Type of partition table to use; either "legacy", "efi", or "none".
# For "efi" images, the GPT partition table is used and a mandatory ESP
# partition of reasonable size is created in addition to the root partition.
# For "legacy", the msdos partition table is used and a single large root
# partition is created.
# For "legacy+gpt", the GPT partition table is used, a 1MiB no-fs partition for
# use by the bootloader is created, and a single large root partition is
# created.
# For "hybrid", the GPT partition table is used and a mandatory ESP
# partition of reasonable size is created in addition to the root partition.
# Also a legacy MBR will be present.
# For "none", no partition table is created. Enabling `installBootLoader`
# most likely fails as GRUB will probably refuse to install.
, # Type of partition table to use; described in the `Image Partitioning` section above.
partitionTableType ? "legacy"
, # Whether to invoke `switch-to-configuration boot` during image creation
@ -193,11 +189,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, efixbootldr, or legacy+gpt.");
# 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
@ -225,6 +221,7 @@ let format' = format; in let
legacy = "1";
"legacy+gpt" = "2";
efi = "2";
efixbootldr = "3";
hybrid = "3";
}.${partitionTableType};
@ -266,6 +263,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:1C06F03B-704E-4657-B9CD-681A087A2FDC \
--partition-guid=2:970C694F-AFD0-4B99-B750-CDB7A329AB6F \
--partition-guid=3:${rootGPUID} \
$diskImage
''}
'';
hybrid = ''
parted --script $diskImage -- \
mklabel gpt \
@ -436,7 +450,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
@ -570,6 +584,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

View File

@ -17,6 +17,9 @@ from dataclasses import dataclass
# These values will be replaced with actual values during the package build
EFI_SYS_MOUNT_POINT = "@efiSysMountPoint@"
BOOT_MOUNT_POINT = "@bootMountPoint@"
LOADER_CONF = f"{EFI_SYS_MOUNT_POINT}/loader/loader.conf" # Always stored on the ESP
NIXOS_DIR = "@nixosDir@"
TIMEOUT = "@timeout@"
EDITOR = "@editor@" == "1"
CONSOLE_MODE = "@consoleMode@"
@ -28,6 +31,7 @@ CONFIGURATION_LIMIT = int("@configurationLimit@")
CAN_TOUCH_EFI_VARIABLES = "@canTouchEfiVariables@"
GRACEFUL = "@graceful@"
COPY_EXTRA_FILES = "@copyExtraFiles@"
CHECK_MOUNTPOINTS = "@checkMountpoints@"
@dataclass
class BootSpec:
@ -87,7 +91,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(f"{EFI_SYS_MOUNT_POINT}/loader/loader.conf.tmp", 'w') as f:
with open(f"{LOADER_CONF}.tmp", 'w') as f:
if TIMEOUT != "":
f.write(f"timeout {TIMEOUT}\n")
f.write("default %s\n" % generation_conf_filename(profile, generation, specialisation))
@ -96,7 +100,7 @@ def write_loader_conf(profile: str | None, generation: int, specialisation: str
f.write(f"console-mode {CONSOLE_MODE}\n")
f.flush()
os.fsync(f.fileno())
os.rename(f"{EFI_SYS_MOUNT_POINT}/loader/loader.conf.tmp", f"{EFI_SYS_MOUNT_POINT}/loader/loader.conf")
os.rename(f"{LOADER_CONF}.tmp", LOADER_CONF)
def get_bootspec(profile: str | None, generation: int) -> BootSpec:
@ -126,9 +130,9 @@ def copy_from_file(file: str, dry_run: bool = False) -> str:
store_file_path = os.path.realpath(file)
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 = f"{NIXOS_DIR}/{store_dir}-{suffix}.efi"
if not dry_run:
copy_if_not_exists(store_file_path, f"{EFI_SYS_MOUNT_POINT}%s" % (efi_file_path))
copy_if_not_exists(store_file_path, f"{BOOT_MOUNT_POINT}{efi_file_path}")
return efi_file_path
def write_entry(profile: str | None, generation: int, specialisation: str | None,
@ -145,7 +149,7 @@ def write_entry(profile: str | None, generation: int, specialisation: str | None
try:
if bootspec.initrdSecrets is not None:
subprocess.check_call([bootspec.initrdSecrets, f"{EFI_SYS_MOUNT_POINT}%s" % (initrd)])
subprocess.check_call([bootspec.initrdSecrets, f"{BOOT_MOUNT_POINT}%s" % (initrd)])
except subprocess.CalledProcessError:
if current:
print("failed to create initrd secrets!", file=sys.stderr)
@ -155,7 +159,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 = f"{EFI_SYS_MOUNT_POINT}/loader/entries/%s" % (
entry_file = f"{BOOT_MOUNT_POINT}/loader/entries/%s" % (
generation_conf_filename(profile, generation, specialisation))
tmp_path = "%s.tmp" % (entry_file)
kernel_params = "init=%s " % bootspec.init
@ -202,14 +206,14 @@ def get_generations(profile: str | None = None) -> list[SystemIdentifier]:
def remove_old_entries(gens: list[SystemIdentifier]) -> None:
rex_profile = re.compile(r"^" + re.escape(EFI_SYS_MOUNT_POINT) + "/loader/entries/nixos-(.*)-generation-.*\.conf$")
rex_generation = re.compile(r"^" + re.escape(EFI_SYS_MOUNT_POINT) + "/loader/entries/nixos.*-generation-([0-9]+)(-specialisation-.*)?\.conf$")
rex_profile = re.compile(r"^" + re.escape(BOOT_MOUNT_POINT) + "/loader/entries/nixos-(.*)-generation-.*\.conf$")
rex_generation = re.compile(r"^" + re.escape(BOOT_MOUNT_POINT) + "/loader/entries/nixos.*-generation-([0-9]+)(-specialisation-.*)?\.conf$")
known_paths = []
for gen in gens:
bootspec = get_bootspec(gen.profile, gen.generation)
known_paths.append(copy_from_file(bootspec.kernel, True))
known_paths.append(copy_from_file(bootspec.initrd, True))
for path in glob.iglob(f"{EFI_SYS_MOUNT_POINT}/loader/entries/nixos*-generation-[1-9]*.conf"):
for path in glob.iglob(f"{BOOT_MOUNT_POINT}/loader/entries/nixos*-generation-[1-9]*.conf"):
if rex_profile.match(path):
prof = rex_profile.sub(r"\1", path)
else:
@ -220,11 +224,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(f"{EFI_SYS_MOUNT_POINT}/efi/nixos/*"):
for path in glob.iglob(f"{BOOT_MOUNT_POINT}/{NIXOS_DIR}/*"):
if not path in known_paths and not os.path.isdir(path):
os.unlink(path)
def cleanup_esp() -> None:
for path in glob.iglob(f"{EFI_SYS_MOUNT_POINT}/loader/entries/nixos*"):
os.unlink(path)
if os.path.isdir(f"{EFI_SYS_MOUNT_POINT}/{NIXOS_DIR}"):
shutil.rmtree(f"{EFI_SYS_MOUNT_POINT}/{NIXOS_DIR}")
def get_profiles() -> list[str]:
if os.path.isdir("/nix/var/nix/profiles/system-profiles/"):
return [x
@ -255,6 +266,9 @@ def install_bootloader(args: argparse.Namespace) -> None:
# flags to pass to bootctl install/update
bootctl_flags = []
if BOOT_MOUNT_POINT != EFI_SYS_MOUNT_POINT:
bootctl_flags.append(f"--boot-path={BOOT_MOUNT_POINT}")
if CAN_TOUCH_EFI_VARIABLES != "1":
bootctl_flags.append("--no-variables")
@ -263,8 +277,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(f"{EFI_SYS_MOUNT_POINT}/loader/loader.conf"):
os.unlink(f"{EFI_SYS_MOUNT_POINT}/loader/loader.conf")
if os.path.exists(LOADER_CONF):
os.unlink(LOADER_CONF)
subprocess.check_call([f"{SYSTEMD}/bin/bootctl", f"--esp-path={EFI_SYS_MOUNT_POINT}"] + bootctl_flags + ["install"])
else:
@ -291,13 +305,15 @@ def install_bootloader(args: argparse.Namespace) -> None:
print("updating systemd-boot from %s to %s" % (installed_version, available_version))
subprocess.check_call([f"{SYSTEMD}/bin/bootctl", f"--esp-path={EFI_SYS_MOUNT_POINT}"] + bootctl_flags + ["update"])
os.makedirs(f"{EFI_SYS_MOUNT_POINT}/efi/nixos", exist_ok=True)
os.makedirs(f"{EFI_SYS_MOUNT_POINT}/loader/entries", exist_ok=True)
os.makedirs(f"{BOOT_MOUNT_POINT}/{NIXOS_DIR}", exist_ok=True)
os.makedirs(f"{BOOT_MOUNT_POINT}/loader/entries", exist_ok=True)
gens = get_generations()
for profile in get_profiles():
gens += get_generations(profile)
remove_old_entries(gens)
for gen in gens:
try:
bootspec = get_bootspec(gen.profile, gen.generation)
@ -315,9 +331,15 @@ def install_bootloader(args: argparse.Namespace) -> None:
else:
raise e
for root, _, files in os.walk(f"{EFI_SYS_MOUNT_POINT}/efi/nixos/.extra-files", topdown=False):
relative_root = root.removeprefix(f"{EFI_SYS_MOUNT_POINT}/efi/nixos/.extra-files").removeprefix("/")
actual_root = os.path.join(f"{EFI_SYS_MOUNT_POINT}", relative_root)
if BOOT_MOUNT_POINT != EFI_SYS_MOUNT_POINT:
# 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()
for root, _, files in os.walk(f"{BOOT_MOUNT_POINT}/{NIXOS_DIR}/.extra-files", topdown=False):
relative_root = root.removeprefix(f"{BOOT_MOUNT_POINT}/{NIXOS_DIR}/.extra-files").removeprefix("/")
actual_root = os.path.join(f"{BOOT_MOUNT_POINT}", relative_root)
for file in files:
actual_file = os.path.join(actual_root, file)
@ -330,7 +352,7 @@ def install_bootloader(args: argparse.Namespace) -> None:
os.rmdir(actual_root)
os.rmdir(root)
os.makedirs(f"{EFI_SYS_MOUNT_POINT}/efi/nixos/.extra-files", exist_ok=True)
os.makedirs(f"{BOOT_MOUNT_POINT}/{NIXOS_DIR}/.extra-files", exist_ok=True)
subprocess.check_call(COPY_EXTRA_FILES)
@ -340,6 +362,8 @@ def main() -> None:
parser.add_argument('default_config', metavar='DEFAULT-CONFIG', help=f"The default {DISTRO_NAME} config to boot")
args = parser.parse_args()
subprocess.check_call(CHECK_MOUNTPOINTS)
try:
install_bootloader(args)
finally:
@ -347,9 +371,14 @@ 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(f"{EFI_SYS_MOUNT_POINT}", os.O_RDONLY))
rc = libc.syncfs(os.open(f"{BOOT_MOUNT_POINT}", os.O_RDONLY))
if rc != 0:
print(f"could not sync {EFI_SYS_MOUNT_POINT}: {os.strerror(rc)}", file=sys.stderr)
print(f"could not sync {BOOT_MOUNT_POINT}: {os.strerror(rc)}", file=sys.stderr)
if BOOT_MOUNT_POINT != EFI_SYS_MOUNT_POINT:
rc = libc.syncfs(os.open(EFI_SYS_MOUNT_POINT, os.O_RDONLY))
if rc != 0:
print(f"could not sync {EFI_SYS_MOUNT_POINT}: {os.strerror(rc)}", file=sys.stderr)
if __name__ == '__main__':

View File

@ -7,7 +7,7 @@ let
efi = config.boot.loader.efi;
systemdBootBuilder = pkgs.substituteAll {
systemdBootBuilder = pkgs.substituteAll rec {
src = ./systemd-boot-builder.py;
isExecutable = true;
@ -28,23 +28,40 @@ let
inherit (efi) efiSysMountPoint canTouchEfiVariables;
bootMountPoint = if cfg.xbootldrMountPoint != null
then cfg.xbootldrMountPoint
else efi.efiSysMountPoint;
nixosDir = "/EFI/nixos";
inherit (config.system.nixos) distroName;
memtest86 = optionalString cfg.memtest86.enable pkgs.memtest86plus;
netbootxyz = optionalString cfg.netbootxyz.enable pkgs.netbootxyz-efi;
checkMountpoints = pkgs.writeShellScript "check-mountpoints" ''
fail() {
echo "$1 = '$2' is not a mounted partition. Is the path configured correctly?" >&2
exit 1
}
${pkgs.util-linuxMinimal}/bin/findmnt ${efiSysMountPoint} > /dev/null || fail efiSysMountPoint ${efiSysMountPoint}
${lib.optionalString
(cfg.xbootldrMountPoint != null)
"${pkgs.util-linuxMinimal}/bin/findmnt ${cfg.xbootldrMountPoint} > /dev/null || fail xbootldrMountPoint ${cfg.xbootldrMountPoint}"}
'';
copyExtraFiles = pkgs.writeShellScript "copy-extra-files" ''
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}/${nixosDir}/.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}/${nixosDir}/.extra-files/loader/entries/"${escapeShellArg n}
'') cfg.extraEntries)}
'';
};
@ -99,6 +116,18 @@ in {
'';
};
xbootldrMountPoint = mkOption {
default = null;
type = types.nullOr types.str;
description = lib.mdDoc ''
Where the XBOOTLDR partition is mounted.
If set, this partition will be used as $BOOT to store boot loader entries and extra files
instead of the EFI partition. As per the bootloader specification, it is recommended that
the EFI and XBOOTLDR partitions be mounted at `/efi` and `/boot`, respectively.
'';
};
configurationLimit = mkOption {
default = null;
example = 120;
@ -108,7 +137,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.
'';
};
@ -200,7 +229,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.
@ -217,9 +246,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.
'';
};
@ -243,6 +272,18 @@ in {
config = mkIf cfg.enable {
assertions = [
{
assertion = (hasPrefix "/" efi.efiSysMountPoint);
message = "The ESP mount point '${efi.efiSysMountPoint}' must be an absolute path";
}
{
assertion = cfg.xbootldrMountPoint == null || (hasPrefix "/" cfg.xbootldrMountPoint);
message = "The XBOOTLDR mount point '${cfg.xbootldrMountPoint}' must be an absolute path";
}
{
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";

View File

@ -14,6 +14,72 @@ let
boot.loader.efi.canTouchEfiVariables = true;
environment.systemPackages = [ pkgs.efibootmgr ];
};
commonXbootldr = { config, lib, pkgs, ... }:
let
diskImage = import ../lib/make-disk-image.nix {
inherit config lib pkgs;
label = "nixos";
format = "qcow2";
partitionTableType = "efixbootldr";
touchEFIVars = true;
installBootLoader = true;
};
in
{
imports = [ common ];
virtualisation.useBootLoader = lib.mkForce false; # Only way to tell qemu-vm not to create the default system image
virtualisation.directBoot.enable = false; # But don't direct boot either because we're testing systemd-boot
system.build.diskImage = diskImage; # Use custom disk image with an XBOOTLDR partition
virtualisation.efi.variables = "${diskImage}/efi-vars.fd";
virtualisation.useDefaultFilesystems = false; # Needs custom setup for `diskImage`
virtualisation.bootPartition = null;
virtualisation.fileSystems = {
"/" = {
device = "/dev/vda3";
fsType = "ext4";
};
"/boot" = {
device = "/dev/vda2";
fsType = "vfat";
noCheck = true;
};
"/efi" = {
device = "/dev/vda1";
fsType = "vfat";
noCheck = true;
};
};
boot.loader.systemd-boot.enable = true;
boot.loader.efi.efiSysMountPoint = "/efi";
boot.loader.systemd-boot.xbootldrMountPoint = "/boot";
};
customDiskImage = nodes: ''
import os
import subprocess
import tempfile
tmp_disk_image = tempfile.NamedTemporaryFile()
subprocess.run([
"${nodes.machine.virtualisation.qemu.package}/bin/qemu-img",
"create",
"-f",
"qcow2",
"-b",
"${nodes.machine.system.build.diskImage}/nixos.qcow2",
"-F",
"qcow2",
tmp_disk_image.name,
])
# Set NIX_DISK_IMAGE so that the qemu script finds the right disk image.
os.environ['NIX_DISK_IMAGE'] = tmp_disk_image.name
'';
in
{
basic = makeTest {
@ -65,6 +131,32 @@ in
'';
};
basicXbootldr = makeTest {
name = "systemd-boot-xbootldr";
meta.maintainers = with pkgs.lib.maintainers; [ sdht0 ];
nodes.machine = commonXbootldr;
testScript = { nodes, ... }: ''
${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")
# 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";
@ -184,6 +276,29 @@ in
'';
};
entryFilenameXbootldr = makeTest {
name = "systemd-boot-entry-filename-xbootldr";
meta.maintainers = with pkgs.lib.maintainers; [ sdht0 ];
nodes.machine = { pkgs, lib, ... }: {
imports = [ commonXbootldr ];
boot.loader.systemd-boot.memtest86.enable = true;
boot.loader.systemd-boot.memtest86.entryFilename = "apple.conf";
};
testScript = { nodes, ... }: ''
${customDiskImage nodes}
machine.start()
machine.wait_for_unit("multi-user.target")
machine.succeed("test -e /efi/EFI/systemd/systemd-bootx64.efi")
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 julienmalka ];