nixos/boot: allow passing extra initrd archives to the bootloader

This is for instance useful on Asahi where an additional initrd archive
containing firmware blobs and per-device calibration files is placed
on the ESP and updated by the Asahi Linux Installer.
These need to be loaded alongside the NixOS initrd.

Implemented for systemd-boot and Limine. Grub is left out since its
install script does not use bootspec yet.

Co-authored-by: Florian Klink <flokli@flokli.de>
This commit is contained in:
r-vdp 2026-06-25 22:46:22 +02:00 committed by Florian Klink
commit 5f267f4dda
6 changed files with 157 additions and 13 deletions

View file

@ -1911,6 +1911,7 @@
./system/boot/clevis-luks-askpass.nix
./system/boot/clevis.nix
./system/boot/emergency-mode.nix
./system/boot/extra-initrd.nix
./system/boot/grow-partition.nix
./system/boot/initrd-network.nix
./system/boot/initrd-openvpn.nix

View file

@ -0,0 +1,21 @@
{ config, lib, ... }:
{
options.system.boot.extraInitrd = {
paths = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
List of paths relative to the ESP that are combined with the NixOS
main initrd before being passed to the kernel.
'';
};
};
config = {
boot.bootspec.extensions = lib.mkIf (config.system.boot.extraInitrd.paths != [ ]) {
"org.nixos.extra-initrd.v1" = {
inherit (config.system.boot.extraInitrd) paths;
};
};
};
}

View file

@ -36,6 +36,7 @@ class BootSpec:
label: str
toplevel: str
specialisations: Dict[str, "BootSpec"]
extraInitrdPaths: list[str]
xen: XenBootSpec | None
initrd: str | None = None
initrdSecrets: str | None = None
@ -173,10 +174,15 @@ def bootjson_to_bootspec(bootjson: dict) -> BootSpec:
xen = None
if "org.xenproject.bootspec.v2" in bootjson:
xen = bootjson["org.xenproject.bootspec.v2"]
extraInitrdExtension = bootjson.get("org.nixos.extra-initrd.v1", {})
extraInitrdPaths = extraInitrdExtension.get("paths", [])
return BootSpec(
**bootjson["org.nixos.bootspec.v1"],
specialisations=specialisations,
xen=xen,
extraInitrdPaths=extraInitrdPaths,
)
@ -299,6 +305,10 @@ def xen_config_entry(
if bootspec.initrd:
# the final module is the initrd
entry += "module_path: " + get_kernel_uri(bootspec.initrd) + "\n"
for p in bootspec.extraInitrdPaths:
entry += f"module_path: boot():/{p}\n"
return entry
@ -321,6 +331,9 @@ def config_entry(levels: int, bootspec: BootSpec, label: str, time: str) -> str:
if bootspec.initrd:
entry += f"module_path: " + get_kernel_uri(bootspec.initrd) + "\n"
for p in bootspec.extraInitrdPaths:
entry += f"module_path: boot():/{p}\n"
if bootspec.initrdSecrets:
base_path = str(limine_install_dir) + "/kernels/"
initrd_secrets_path = (

View file

@ -45,6 +45,7 @@ BOOT_COUNTING = "@bootCounting@" == "True"
class BootSpec:
init: Path
initrd: Path
extraInitrdPaths: list[Path]
kernel: Path
kernelParams: list[str] # noqa: N815
label: str
@ -306,9 +307,7 @@ def get_bootspec(profile: str | None, generation: int) -> BootSpec | None:
try:
bootspec_json = json.load(f)
except ValueError as e:
print(
f"error: Malformed Json: {e}, in {boot_json_path}", file=sys.stderr
)
print(f"error: Malformed Json: {e}, in {boot_json_path}", file=sys.stderr)
sys.exit(1)
return bootspec_from_json(bootspec_json)
@ -320,6 +319,11 @@ def bootspec_from_json(bootspec_json: dict[str, Any]) -> BootSpec:
sortKey = systemdBootExtension.get("sortKey", "nixos")
devicetree = systemdBootExtension.get("devicetree")
extraInitrdExtension = bootspec_json.get("org.nixos.extra-initrd.v1", {})
extraInitrdPaths = list(
map(lambda path: Path(path), extraInitrdExtension.get("paths", []))
)
if devicetree:
devicetree = Path(devicetree)
@ -332,6 +336,7 @@ def bootspec_from_json(bootspec_json: dict[str, Any]) -> BootSpec:
specialisations=specialisations,
sortKey=sortKey,
devicetree=devicetree,
extraInitrdPaths=extraInitrdPaths,
)
@ -374,16 +379,21 @@ def boot_file(
specialisation=" (%s)" % specialisation if specialisation else "",
)
description = f"Generation {generation} {bootspec.label}, built on {build_date}"
boot_entry = [
f"title {title}",
f"version {description}",
f"linux /{str(kernel.path)}",
f"initrd /{str(initrd.path)}",
f"options {kernel_params}",
f"machine-id {machine_id}" if machine_id is not None else None,
f"devicetree /{str(devicetree.path)}" if devicetree is not None else None,
f"sort-key {bootspec.sortKey}",
]
boot_entry = (
[
f"title {title}",
f"version {description}",
f"linux /{str(kernel.path)}",
f"initrd /{str(initrd.path)}",
]
+ list(map(lambda initrd: f"initrd /{initrd}", bootspec.extraInitrdPaths))
+ [
f"options {kernel_params}",
f"machine-id {machine_id}" if machine_id is not None else None,
f"devicetree /{str(devicetree.path)}" if devicetree is not None else None,
f"sort-key {bootspec.sortKey}",
]
)
contents = "\n".join(filter(None, boot_entry))
entry, bootctl_id = BootFile.from_entry(contents.encode("utf-8"))
return (list(filter(None, [kernel, initrd, devicetree, entry])), bootctl_id)

View file

@ -554,6 +554,9 @@ in
etebase-server = runTest ./etebase-server.nix;
etesync-dav = runTest ./etesync-dav.nix;
evcc = runTest ./evcc.nix;
extra-initrd = import ./extra-initrd.nix {
inherit runTest pkgs;
};
facter = runTest ./facter;
fail2ban = runTest ./fail2ban.nix;
fakeroute = runTest ./fakeroute.nix;

View file

@ -0,0 +1,96 @@
{
runTest,
...
}:
let
common =
{ config, pkgs, ... }:
{
virtualisation.useBootLoader = true;
virtualisation.useEFIBoot = true;
system.boot.extraInitrd.paths = [
extraInitrdPath
];
boot.loader.timeout = 2;
boot.initrd.systemd.mounts = [
{
what = "/canary.txt";
where = "/sysroot/run/canary.txt";
type = "none";
options = "bind";
unitConfig = {
DefaultDependencies = false;
};
requiredBy = [ "initrd-fs.target" ];
before = [ "initrd-fs.target" ];
}
];
};
extraInitrdPath = "custom.cpio";
canaryCpio =
pkgs:
pkgs.runCommand "canary.cpio"
{
nativeBuildInputs = [ pkgs.cpio ];
}
''
echo canary > canary.txt
find . -print0 | cpio --null -o --format=newc > $out
'';
testScript =
# python
''
machine.wait_for_unit("multi-user.target")
# Check that the extra cpio archive is on the ESP
machine.succeed("test -e /boot/custom.cpio")
# Check that the initrd that we booted with contained the file from
# the extra initrd and our initrd mount unit bound it into sysroot
assert machine.succeed("cat /run/canary.txt").strip() == "canary"
'';
in
{
systemd-boot = runTest (
{ pkgs, ... }: {
name = "systemd-boot-extra-initrd";
nodes.machine = { config, ... }: {
imports = [ common ];
boot.loader.systemd-boot = {
enable = true;
extraInstallCommands = ''
cp ${canaryCpio pkgs} ${config.boot.loader.efi.efiSysMountPoint}/${extraInitrdPath}
'';
};
};
inherit testScript;
}
);
limine = runTest {
name = "limine-extra-initrd";
nodes.machine = { pkgs, config, ... }: {
imports = [ common ];
boot.loader.limine = {
enable = true;
efiSupport = true;
enableEditor = true;
extraInstallCommands = ''
cp ${canaryCpio pkgs} ${config.boot.loader.efi.efiSysMountPoint}/${extraInitrdPath}
'';
};
};
inherit testScript;
};
}