Merge pull request #150408 from Enzime/systemd-boot-extra-entries

nixos/systemd-boot: Add `extraEntries` and `extraFiles` options

pennae 466cb747 1141814a

Changed files
+318 -28
nixos
modules
installer
system
boot
tests
pkgs
tools
misc
netbootxyz-efi
top-level
+2
nixos/modules/installer/tools/nixos-enter.sh
···
chroot "$mountPoint" systemd-tmpfiles --create --remove --exclude-prefix=/dev 1>&2 || true
)
exec chroot "$mountPoint" "${command[@]}"
···
chroot "$mountPoint" systemd-tmpfiles --create --remove --exclude-prefix=/dev 1>&2 || true
)
+
unset TMPDIR
+
exec chroot "$mountPoint" "${command[@]}"
+17 -26
nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py
···
options {kernel_params}
"""
-
# The boot loader entry for memtest86.
-
#
-
# TODO: This is hard-coded to use the 64-bit EFI app, but it could probably
-
# be updated to use the 32-bit EFI app on 32-bit systems. The 32-bit EFI
-
# app filename is BOOTIA32.efi.
-
MEMTEST_BOOT_ENTRY = """title MemTest86
-
efi /efi/memtest86/BOOTX64.efi
-
"""
-
-
def generation_conf_filename(profile: Optional[str], generation: int, specialisation: Optional[str]) -> str:
pieces = [
"nixos",
···
except OSError as e:
print("ignoring profile '{}' in the list of boot entries because of the following error:\n{}".format(profile, e), file=sys.stderr)
-
memtest_entry_file = "@efiSysMountPoint@/loader/entries/memtest86.conf"
-
if os.path.exists(memtest_entry_file):
-
os.unlink(memtest_entry_file)
-
shutil.rmtree("@efiSysMountPoint@/efi/memtest86", ignore_errors=True)
-
if "@memtest86@" != "":
-
mkdir_p("@efiSysMountPoint@/efi/memtest86")
-
for path in glob.iglob("@memtest86@/*"):
-
if os.path.isdir(path):
-
shutil.copytree(path, os.path.join("@efiSysMountPoint@/efi/memtest86", os.path.basename(path)))
-
else:
-
shutil.copy(path, "@efiSysMountPoint@/efi/memtest86/")
-
memtest_entry_file = "@efiSysMountPoint@/loader/entries/memtest86.conf"
-
memtest_entry_file_tmp_path = "%s.tmp" % memtest_entry_file
-
with open(memtest_entry_file_tmp_path, 'w') as f:
-
f.write(MEMTEST_BOOT_ENTRY)
-
os.rename(memtest_entry_file_tmp_path, memtest_entry_file)
# Since fat32 provides little recovery facilities after a crash,
# it can leave the system in an unbootable state, when a crash/outage
···
options {kernel_params}
"""
def generation_conf_filename(profile: Optional[str], generation: int, specialisation: Optional[str]) -> str:
pieces = [
"nixos",
···
except OSError as e:
print("ignoring profile '{}' in the list of boot entries because of the following error:\n{}".format(profile, e), file=sys.stderr)
+
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 file in files:
+
actual_file = os.path.join(actual_root, file)
+
+
if os.path.exists(actual_file):
+
os.unlink(actual_file)
+
os.unlink(os.path.join(root, file))
+
+
if not len(os.listdir(actual_root)):
+
os.rmdir(actual_root)
+
os.rmdir(root)
+
+
mkdir_p("@efiSysMountPoint@/efi/nixos/.extra-files")
+
subprocess.check_call("@copyExtraFiles@")
# Since fat32 provides little recovery facilities after a crash,
# it can leave the system in an unbootable state, when a crash/outage
+135 -2
nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix
···
inherit (efi) efiSysMountPoint canTouchEfiVariables;
memtest86 = if cfg.memtest86.enable then pkgs.memtest86-efi else "";
};
checkedSystemdBootBuilder = pkgs.runCommand "systemd-boot" {
···
<literal>true</literal>.
'';
};
};
graceful = mkOption {
···
assertions = [
{
assertion = (config.boot.kernelPackages.kernel.features or { efiBootStub = true; }) ? efiBootStub;
-
message = "This kernel does not support the EFI boot stub";
}
-
];
boot.loader.grub.enable = mkDefault false;
boot.loader.supportsInitrdSecrets = true;
system = {
build.installBootLoader = checkedSystemdBootBuilder;
···
inherit (efi) efiSysMountPoint canTouchEfiVariables;
memtest86 = if cfg.memtest86.enable then pkgs.memtest86-efi else "";
+
+
netbootxyz = if cfg.netbootxyz.enable then pkgs.netbootxyz-efi else "";
+
+
copyExtraFiles = pkgs.writeShellScript "copy-extra-files" ''
+
empty_file=$(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}
+
'') 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}
+
'') cfg.extraEntries)}
+
'';
};
checkedSystemdBootBuilder = pkgs.runCommand "systemd-boot" {
···
<literal>true</literal>.
'';
};
+
+
entryFilename = mkOption {
+
default = "memtest86.conf";
+
type = types.str;
+
description = ''
+
<literal>systemd-boot</literal> orders the menu entries by the config file names,
+
so if you want something to appear after all the NixOS entries,
+
it should start with <filename>o</filename> or onwards.
+
'';
+
};
+
};
+
+
netbootxyz = {
+
enable = mkOption {
+
default = false;
+
type = types.bool;
+
description = ''
+
Make <literal>netboot.xyz</literal> available from the
+
<literal>systemd-boot</literal> menu. <literal>netboot.xyz</literal>
+
is a menu system that allows you to boot OS installers and
+
utilities over the network.
+
'';
+
};
+
+
entryFilename = mkOption {
+
default = "o_netbootxyz.conf";
+
type = types.str;
+
description = ''
+
<literal>systemd-boot</literal> orders the menu entries by the config file names,
+
so if you want something to appear after all the NixOS entries,
+
it should start with <filename>o</filename> or onwards.
+
'';
+
};
+
};
+
+
extraEntries = mkOption {
+
type = types.attrsOf types.lines;
+
default = {};
+
example = literalExpression ''
+
{ "memtest86.conf" = '''
+
title MemTest86
+
efi /efi/memtest86/memtest86.efi
+
'''; }
+
'';
+
description = ''
+
Any additional entries you want added to the <literal>systemd-boot</literal> menu.
+
These entries will be copied to <filename>/boot/loader/entries</filename>.
+
Each attribute name denotes the destination file name,
+
and the corresponding attribute value is the contents of the entry.
+
+
<literal>systemd-boot</literal> orders the menu entries by the config file names,
+
so if you want something to appear after all the NixOS entries,
+
it should start with <filename>o</filename> or onwards.
+
'';
+
};
+
+
extraFiles = mkOption {
+
type = types.attrsOf types.path;
+
default = {};
+
example = literalExpression ''
+
{ "efi/memtest86/memtest86.efi" = "''${pkgs.memtest86-efi}/BOOTX64.efi"; }
+
'';
+
description = ''
+
A set of files to be copied to <filename>/boot</filename>.
+
Each attribute name denotes the destination file name in
+
<filename>/boot</filename>, while the corresponding
+
attribute value specifies the source file.
+
'';
};
graceful = mkOption {
···
assertions = [
{
assertion = (config.boot.kernelPackages.kernel.features or { efiBootStub = true; }) ? efiBootStub;
message = "This kernel does not support the EFI boot stub";
}
+
] ++ concatMap (filename: [
+
{
+
assertion = !(hasInfix "/" filename);
+
message = "boot.loader.systemd-boot.extraEntries.${lib.strings.escapeNixIdentifier filename} is invalid: entries within folders are not supported";
+
}
+
{
+
assertion = hasSuffix ".conf" filename;
+
message = "boot.loader.systemd-boot.extraEntries.${lib.strings.escapeNixIdentifier filename} is invalid: entries must have a .conf file extension";
+
}
+
]) (builtins.attrNames cfg.extraEntries)
+
++ concatMap (filename: [
+
{
+
assertion = !(hasPrefix "/" filename);
+
message = "boot.loader.systemd-boot.extraFiles.${lib.strings.escapeNixIdentifier filename} is invalid: paths must not begin with a slash";
+
}
+
{
+
assertion = !(hasInfix ".." filename);
+
message = "boot.loader.systemd-boot.extraFiles.${lib.strings.escapeNixIdentifier filename} is invalid: paths must not reference the parent directory";
+
}
+
{
+
assertion = !(hasInfix "nixos/.extra-files" (toLower filename));
+
message = "boot.loader.systemd-boot.extraFiles.${lib.strings.escapeNixIdentifier filename} is invalid: files cannot be placed in the nixos/.extra-files directory";
+
}
+
]) (builtins.attrNames cfg.extraFiles);
boot.loader.grub.enable = mkDefault false;
boot.loader.supportsInitrdSecrets = true;
+
+
boot.loader.systemd-boot.extraFiles = mkMerge [
+
# TODO: This is hard-coded to use the 64-bit EFI app, but it could probably
+
# be updated to use the 32-bit EFI app on 32-bit systems. The 32-bit EFI
+
# app filename is BOOTIA32.efi.
+
(mkIf cfg.memtest86.enable {
+
"efi/memtest86/BOOTX64.efi" = "${pkgs.memtest86-efi}/BOOTX64.efi";
+
})
+
(mkIf cfg.netbootxyz.enable {
+
"efi/netbootxyz/netboot.xyz.efi" = "${pkgs.netbootxyz-efi}";
+
})
+
];
+
+
boot.loader.systemd-boot.extraEntries = mkMerge [
+
(mkIf cfg.memtest86.enable {
+
"${cfg.memtest86.entryFilename}" = ''
+
title MemTest86
+
efi /efi/memtest86/BOOTX64.efi
+
'';
+
})
+
(mkIf cfg.netbootxyz.enable {
+
"${cfg.netbootxyz.entryFilename}" = ''
+
title netboot.xyz
+
efi /efi/netbootxyz/netboot.xyz.efi
+
'';
+
})
+
];
system = {
build.installBootLoader = checkedSystemdBootBuilder;
+141
nixos/tests/systemd-boot.nix
···
assert "updating systemd-boot from (000.0-1-notnixos) to " in output
'';
};
}
···
assert "updating systemd-boot from (000.0-1-notnixos) to " in output
'';
};
+
+
memtest86 = makeTest {
+
name = "systemd-boot-memtest86";
+
meta.maintainers = with pkgs.lib.maintainers; [ Enzime ];
+
+
machine = { pkgs, lib, ... }: {
+
imports = [ common ];
+
boot.loader.systemd-boot.memtest86.enable = true;
+
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [
+
"memtest86-efi"
+
];
+
};
+
+
testScript = ''
+
machine.succeed("test -e /boot/loader/entries/memtest86.conf")
+
machine.succeed("test -e /boot/efi/memtest86/BOOTX64.efi")
+
'';
+
};
+
+
netbootxyz = makeTest {
+
name = "systemd-boot-netbootxyz";
+
meta.maintainers = with pkgs.lib.maintainers; [ Enzime ];
+
+
machine = { pkgs, lib, ... }: {
+
imports = [ common ];
+
boot.loader.systemd-boot.netbootxyz.enable = true;
+
};
+
+
testScript = ''
+
machine.succeed("test -e /boot/loader/entries/o_netbootxyz.conf")
+
machine.succeed("test -e /boot/efi/netbootxyz/netboot.xyz.efi")
+
'';
+
};
+
+
entryFilename = makeTest {
+
name = "systemd-boot-entry-filename";
+
meta.maintainers = with pkgs.lib.maintainers; [ Enzime ];
+
+
machine = { pkgs, lib, ... }: {
+
imports = [ common ];
+
boot.loader.systemd-boot.memtest86.enable = true;
+
boot.loader.systemd-boot.memtest86.entryFilename = "apple.conf";
+
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [
+
"memtest86-efi"
+
];
+
};
+
+
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/BOOTX64.efi")
+
'';
+
};
+
+
extraEntries = makeTest {
+
name = "systemd-boot-extra-entries";
+
meta.maintainers = with pkgs.lib.maintainers; [ Enzime ];
+
+
machine = { pkgs, lib, ... }: {
+
imports = [ common ];
+
boot.loader.systemd-boot.extraEntries = {
+
"banana.conf" = ''
+
title banana
+
'';
+
};
+
};
+
+
testScript = ''
+
machine.succeed("test -e /boot/loader/entries/banana.conf")
+
machine.succeed("test -e /boot/efi/nixos/.extra-files/loader/entries/banana.conf")
+
'';
+
};
+
+
extraFiles = makeTest {
+
name = "systemd-boot-extra-files";
+
meta.maintainers = with pkgs.lib.maintainers; [ Enzime ];
+
+
machine = { pkgs, lib, ... }: {
+
imports = [ common ];
+
boot.loader.systemd-boot.extraFiles = {
+
"efi/fruits/tomato.efi" = pkgs.netbootxyz-efi;
+
};
+
};
+
+
testScript = ''
+
machine.succeed("test -e /boot/efi/fruits/tomato.efi")
+
machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi")
+
'';
+
};
+
+
switch-test = makeTest {
+
name = "systemd-boot-switch-test";
+
meta.maintainers = with pkgs.lib.maintainers; [ Enzime ];
+
+
nodes = {
+
inherit common;
+
+
machine = { pkgs, ... }: {
+
imports = [ common ];
+
boot.loader.systemd-boot.extraFiles = {
+
"efi/fruits/tomato.efi" = pkgs.netbootxyz-efi;
+
};
+
};
+
+
with_netbootxyz = { pkgs, ... }: {
+
imports = [ common ];
+
boot.loader.systemd-boot.netbootxyz.enable = true;
+
};
+
};
+
+
testScript = { nodes, ... }: let
+
originalSystem = nodes.machine.config.system.build.toplevel;
+
baseSystem = nodes.common.config.system.build.toplevel;
+
finalSystem = nodes.with_netbootxyz.config.system.build.toplevel;
+
in ''
+
machine.succeed("test -e /boot/efi/fruits/tomato.efi")
+
machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi")
+
+
with subtest("remove files when no longer needed"):
+
machine.succeed("${baseSystem}/bin/switch-to-configuration boot")
+
machine.fail("test -e /boot/efi/fruits/tomato.efi")
+
machine.fail("test -d /boot/efi/fruits")
+
machine.succeed("test -d /boot/efi/nixos/.extra-files")
+
machine.fail("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi")
+
machine.fail("test -d /boot/efi/nixos/.extra-files/efi/fruits")
+
+
with subtest("files are added back when needed again"):
+
machine.succeed("${originalSystem}/bin/switch-to-configuration boot")
+
machine.succeed("test -e /boot/efi/fruits/tomato.efi")
+
machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi")
+
+
with subtest("simultaneously removing and adding files works"):
+
machine.succeed("${finalSystem}/bin/switch-to-configuration boot")
+
machine.fail("test -e /boot/efi/fruits/tomato.efi")
+
machine.fail("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi")
+
machine.succeed("test -e /boot/loader/entries/o_netbootxyz.conf")
+
machine.succeed("test -e /boot/efi/netbootxyz/netboot.xyz.efi")
+
machine.succeed("test -e /boot/efi/nixos/.extra-files/loader/entries/o_netbootxyz.conf")
+
machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/netbootxyz/netboot.xyz.efi")
+
'';
+
};
}
+21
pkgs/tools/misc/netbootxyz-efi/default.nix
···
···
+
{ lib
+
, fetchurl
+
}:
+
+
let
+
pname = "netboot.xyz-efi";
+
version = "2.0.53";
+
in fetchurl {
+
name = "${pname}-${version}";
+
+
url = "https://github.com/netbootxyz/netboot.xyz/releases/download/${version}/netboot.xyz.efi";
+
sha256 = "sha256-v7XqrhG94BLTpDHDazTiowQUXu/ITEcgVMmhlqgmSQE=";
+
+
meta = with lib; {
+
homepage = "https://netboot.xyz/";
+
description = "A tool to boot OS installers and utilities over the network, to be run from a bootloader";
+
license = licenses.asl20;
+
maintainers = with maintainers; [ Enzime ];
+
platforms = platforms.linux;
+
};
+
}
+2
pkgs/top-level/all-packages.nix
···
netboot = callPackage ../tools/networking/netboot {};
netcat = libressl.nc;
netcat-gnu = callPackage ../tools/networking/netcat { };
···
netboot = callPackage ../tools/networking/netboot {};
+
netbootxyz-efi = callPackage ../tools/misc/netbootxyz-efi { };
+
netcat = libressl.nc;
netcat-gnu = callPackage ../tools/networking/netcat { };