From b3a73f17baedc5a555f0ff0b01d25766a03988e3 Mon Sep 17 00:00:00 2001 From: r-vdp Date: Mon, 22 Jun 2026 11:51:56 +0200 Subject: [PATCH 1/4] nixos/limine: format the install script --- .../boot/loader/limine/limine-install.py | 638 +++++++++++------- 1 file changed, 393 insertions(+), 245 deletions(-) diff --git a/nixos/modules/system/boot/loader/limine/limine-install.py b/nixos/modules/system/boot/loader/limine/limine-install.py index be4522299c5b..629a2fef6c76 100644 --- a/nixos/modules/system/boot/loader/limine/limine-install.py +++ b/nixos/modules/system/boot/loader/limine/limine-install.py @@ -1,7 +1,7 @@ #!@python3@/bin/python3 -B from dataclasses import dataclass -from typing import Any, Callable, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple import datetime import hashlib @@ -16,6 +16,7 @@ import sys import tempfile import textwrap + @dataclass class XenBootSpec: """Represent the bootspec extension for Xen dom0 kernels""" @@ -25,6 +26,7 @@ class XenBootSpec: params: List[str] version: str + @dataclass class BootSpec: system: str @@ -38,13 +40,15 @@ class BootSpec: initrd: str | None = None initrdSecrets: str | None = None -install_config = json.load(open('@configPath@', 'r')) + +install_config = json.load(open("@configPath@", "r")) libc = CDLL("libc.so.6") limine_install_dir: Optional[str] = None can_use_direct_paths = False paths: Dict[str, bool] = {} + def config(*path: str) -> Optional[Any]: result = install_config for component in path: @@ -53,60 +57,69 @@ def config(*path: str) -> Optional[Any]: def bool_to_yes_no(value: bool) -> str: - return 'yes' if value else 'no' + return "yes" if value else "no" -def get_system_path(profile: str = 'system', gen: Optional[str] = None, spec: Optional[str] = None) -> str: - basename = f'{profile}-{gen}-link' if gen is not None else profile - profiles_dir = '/nix/var/nix/profiles' - if profile == 'system': +def get_system_path( + profile: str = "system", gen: Optional[str] = None, spec: Optional[str] = None +) -> str: + basename = f"{profile}-{gen}-link" if gen is not None else profile + profiles_dir = "/nix/var/nix/profiles" + if profile == "system": result = os.path.join(profiles_dir, basename) else: - result = os.path.join(profiles_dir, 'system-profiles', basename) + result = os.path.join(profiles_dir, "system-profiles", basename) if spec is not None: - result = os.path.join(result, 'specialisation', spec) + result = os.path.join(result, "specialisation", spec) return result def get_profiles() -> List[str]: - profiles_dir = '/nix/var/nix/profiles/system-profiles/' + profiles_dir = "/nix/var/nix/profiles/system-profiles/" dirs = os.listdir(profiles_dir) if os.path.isdir(profiles_dir) else [] - return [path for path in dirs if not path.endswith('-link')] + return [path for path in dirs if not path.endswith("-link")] -def get_gens(profile: str = 'system') -> List[Tuple[int, List[str]]]: - nix_env = os.path.join(str(config('nixPath')), 'bin', 'nix-env') - output = subprocess.check_output([ - nix_env, '--list-generations', - '-p', get_system_path(profile), - '--option', 'build-users-group', '', - ], universal_newlines=True) +def get_gens(profile: str = "system") -> List[Tuple[int, List[str]]]: + nix_env = os.path.join(str(config("nixPath")), "bin", "nix-env") + output = subprocess.check_output( + [ + nix_env, + "--list-generations", + "-p", + get_system_path(profile), + "--option", + "build-users-group", + "", + ], + universal_newlines=True, + ) gen_lines = output.splitlines() gen_nums = [int(line.split()[0]) for line in gen_lines] - return [gen for gen in gen_nums][-config('maxGenerations'):] + return [gen for gen in gen_nums][-config("maxGenerations") :] def is_encrypted(device: str) -> bool: - for name in config('luksDevices'): - if os.readlink(os.path.join('/dev/mapper', name)) == os.readlink(device): + for name in config("luksDevices"): + if os.readlink(os.path.join("/dev/mapper", name)) == os.readlink(device): return True return False def is_fs_type_supported(fs_type: str) -> bool: - return fs_type.startswith('vfat') + return fs_type.startswith("vfat") def get_dest_file(path: str) -> str: package_id = os.path.basename(os.path.dirname(path)) suffix = os.path.basename(path) - return f'{package_id}-{suffix}' + return f"{package_id}-{suffix}" def get_dest_path(path: str, target: str) -> str: @@ -115,7 +128,7 @@ def get_dest_path(path: str, target: str) -> str: def get_copied_path_uri(path: str, target: str) -> str: - result = '' + result = "" dest_file = get_dest_file(path) dest_path = get_dest_path(path, target) @@ -125,15 +138,15 @@ def get_copied_path_uri(path: str, target: str) -> str: else: paths[dest_path] = True - path_with_prefix = os.path.join('/limine', target, dest_file) - result = f'boot():{path_with_prefix}' + path_with_prefix = os.path.join("/limine", target, dest_file) + result = f"boot():{path_with_prefix}" - if config('validateChecksums'): - with open(path, 'rb') as file: + if config("validateChecksums"): + with open(path, "rb") as file: b2sum = hashlib.blake2b() b2sum.update(file.read()) - result += f'#{b2sum.hexdigest()}' + result += f"#{b2sum.hexdigest()}" return result @@ -142,7 +155,9 @@ def get_path_uri(path: str) -> str: return get_copied_path_uri(path, "") -def get_file_uri(profile: str, gen: Optional[str], spec: Optional[str], name: str) -> str: +def get_file_uri( + profile: str, gen: Optional[str], spec: Optional[str], name: str +) -> str: gen_path = get_system_path(profile, gen, spec) path_in_store = os.path.realpath(os.path.join(gen_path, name)) return get_path_uri(path_in_store) @@ -151,22 +166,21 @@ def get_file_uri(profile: str, gen: Optional[str], spec: Optional[str], name: st def get_kernel_uri(kernel_path: str) -> str: return get_copied_path_uri(kernel_path, "kernels") + def bootjson_to_bootspec(bootjson: dict) -> BootSpec: - specialisations = bootjson['org.nixos.specialisation.v1'] + specialisations = bootjson["org.nixos.specialisation.v1"] specialisations = {k: bootjson_to_bootspec(v) for k, v in specialisations.items()} xen = None - if 'org.xenproject.bootspec.v2' in bootjson: - xen = bootjson['org.xenproject.bootspec.v2'] + if "org.xenproject.bootspec.v2" in bootjson: + xen = bootjson["org.xenproject.bootspec.v2"] return BootSpec( - **bootjson['org.nixos.bootspec.v1'], + **bootjson["org.nixos.bootspec.v1"], specialisations=specialisations, xen=xen, ) -def generate_xen_efi_files( - bootspec: BootSpec, - gen: str - ) -> str: + +def generate_xen_efi_files(bootspec: BootSpec, gen: str) -> str: """Generate a Xen EFI xen.cfg file, and copy required files in place. Assumes the bootspec has already been validated as having the requried @@ -179,43 +193,43 @@ def generate_xen_efi_files( Returns the path to the Xen EFI binary """ - xen_efi_boot_path = get_copied_path_uri(bootspec.xen['efiPath'], f'xen/{gen}') - xen_efi_path = get_dest_path(bootspec.xen['efiPath'], f'xen/{gen}') + xen_efi_boot_path = get_copied_path_uri(bootspec.xen["efiPath"], f"xen/{gen}") + xen_efi_path = get_dest_path(bootspec.xen["efiPath"], f"xen/{gen}") xen_efi_cfg_dir = os.path.dirname(xen_efi_path) - xen_efi_cfg_path = xen_efi_path[:-4] + '.cfg' + xen_efi_cfg_path = xen_efi_path[:-4] + ".cfg" if not os.path.exists(xen_efi_cfg_dir): os.makedirs(xen_efi_cfg_dir) - xen_efi_cfg = ( - f'default=nixos{gen}\n\n' + - f'[nixos{gen}]\n' - ) + xen_efi_cfg = f"default=nixos{gen}\n\n" + f"[nixos{gen}]\n" # set xen dom0 parameters - if 'params' in bootspec.xen and len(bootspec.xen['params']) > 0: - xen_efi_cfg += 'options=' + ' '.join(bootspec.xen['params']).strip() + '\n' + if "params" in bootspec.xen and len(bootspec.xen["params"]) > 0: + xen_efi_cfg += "options=" + " ".join(bootspec.xen["params"]).strip() + "\n" # set kernel and copy in-place - xen_efi_kernel_path = get_dest_path(bootspec.kernel, f'xen/{gen}') + xen_efi_kernel_path = get_dest_path(bootspec.kernel, f"xen/{gen}") copy_file(bootspec.kernel, xen_efi_kernel_path) xen_efi_cfg += ( - 'kernel=' + os.path.basename(xen_efi_kernel_path) + ' ' - + ' '.join(['init=' + bootspec.init] + bootspec.kernelParams).strip() - + '\n' + "kernel=" + + os.path.basename(xen_efi_kernel_path) + + " " + + " ".join(["init=" + bootspec.init] + bootspec.kernelParams).strip() + + "\n" ) # set ramdisk and copy initrd in-place if bootspec.initrd: - xen_efi_initrd_path = get_dest_path(bootspec.initrd, f'xen/{gen}') + xen_efi_initrd_path = get_dest_path(bootspec.initrd, f"xen/{gen}") copy_file(bootspec.initrd, xen_efi_initrd_path) - xen_efi_cfg += 'ramdisk=' + os.path.basename(xen_efi_initrd_path) + '\n' + xen_efi_cfg += "ramdisk=" + os.path.basename(xen_efi_initrd_path) + "\n" - with open(xen_efi_cfg_path, 'w') as xen_efi_cfg_file: + with open(xen_efi_cfg_path, "w") as xen_efi_cfg_file: xen_efi_cfg_file.write(xen_efi_cfg) return xen_efi_boot_path + def xen_config_entry( levels: int, bootspec: BootSpec, xenVersion: str, gen: str, time: str, efi: bool ) -> str: @@ -230,15 +244,19 @@ def xen_config_entry( efi -- True if EFI protocol should be used for this entry """ # generate Xen menu label for the current generation - entry = '/' * levels + f'Generation {gen} with Xen {xenVersion}' + (' EFI\n' if efi else '\n') - entry += f'comment: Xen {xenVersion} {bootspec.label}, built on {time}\n' + entry = ( + "/" * levels + + f"Generation {gen} with Xen {xenVersion}" + + (" EFI\n" if efi else "\n") + ) + entry += f"comment: Xen {xenVersion} {bootspec.label}, built on {time}\n" # load Xen dom0 as the executable, using multiboot for EFI & BIOS if ( - efi and - 'multibootPath' in bootspec.xen and - len(bootspec.xen['multibootPath']) > 0 and - os.path.exists(bootspec.xen['multibootPath']) - ): + efi + and "multibootPath" in bootspec.xen + and len(bootspec.xen["multibootPath"]) > 0 + and os.path.exists(bootspec.xen["multibootPath"]) + ): # Use the EFI protocol and generate Xen EFI configuration # files and directories which are loaded by Xen's EFI binary # directly. @@ -247,84 +265,100 @@ def xen_config_entry( # an entry-point in Xen's multiboot binary, and multiboot1 # doesn't work under EFI. # Upstream Limine issue #482 - entry += 'protocol: efi\n' - entry += ( - 'path: ' + generate_xen_efi_files(bootspec, gen) + '\n' - ) + entry += "protocol: efi\n" + entry += "path: " + generate_xen_efi_files(bootspec, gen) + "\n" elif ( - 'multibootPath' in bootspec.xen and - len(bootspec.xen['multibootPath']) > 0 and - os.path.exists(bootspec.xen['multibootPath']) - ): + "multibootPath" in bootspec.xen + and len(bootspec.xen["multibootPath"]) > 0 + and os.path.exists(bootspec.xen["multibootPath"]) + ): # Use multiboot1 if not generating an EFI entry, as multiboot2 # doesn't work under Limine for booting Xen. # Upstream Limine issue #483 - entry += 'protocol: multiboot\n' + entry += "protocol: multiboot\n" entry += ( - 'path: ' + get_copied_path_uri(bootspec.xen['multibootPath'], f'xen/{gen}') + '\n' + "path: " + + get_copied_path_uri(bootspec.xen["multibootPath"], f"xen/{gen}") + + "\n" ) # set params as the multiboot executable's parameters - if 'params' in bootspec.xen and len(bootspec.xen['params']) > 0: + if "params" in bootspec.xen and len(bootspec.xen["params"]) > 0: # TODO: Understand why the first argument is ignored below? # --- to work around first argument being ignored - entry += ( - 'cmdline: -- ' + ' '.join(bootspec.xen['params']).strip() + '\n' - ) + entry += "cmdline: -- " + " ".join(bootspec.xen["params"]).strip() + "\n" # load the linux kernel as the second module - entry += 'module_path: ' + get_kernel_uri(bootspec.kernel) + '\n' + entry += "module_path: " + get_kernel_uri(bootspec.kernel) + "\n" # set kernel parameters as the parameters to the first module # TODO: Understand why the first argument is ignored below? # --- to work around first argument being ignored entry += ( - 'module_string: -- ' - + ' '.join(['init=' + bootspec.init] + bootspec.kernelParams).strip() - + '\n' + "module_string: -- " + + " ".join(["init=" + bootspec.init] + bootspec.kernelParams).strip() + + "\n" ) if bootspec.initrd: # the final module is the initrd - entry += 'module_path: ' + get_kernel_uri(bootspec.initrd) + '\n' + entry += "module_path: " + get_kernel_uri(bootspec.initrd) + "\n" return entry + def config_entry(levels: int, bootspec: BootSpec, label: str, time: str) -> str: - entry = '/' * levels + label + '\n' - entry += 'protocol: linux\n' - entry += f'comment: {bootspec.label}, built on {time}\n' - entry += 'kernel_path: ' + get_kernel_uri(bootspec.kernel) + '\n' - entry += 'cmdline: ' + ' '.join(['init=' + bootspec.init] + bootspec.kernelParams).strip() + '\n' + entry = "/" * levels + label + "\n" + entry += "protocol: linux\n" + entry += f"comment: {bootspec.label}, built on {time}\n" + entry += "kernel_path: " + get_kernel_uri(bootspec.kernel) + "\n" + entry += ( + "cmdline: " + + " ".join(["init=" + bootspec.init] + bootspec.kernelParams).strip() + + "\n" + ) # Set framebuffer resolution for Linux boot entries if configured - resolution = config('resolution') + resolution = config("resolution") if resolution is not None: - entry += f'resolution: {resolution}\n' + entry += f"resolution: {resolution}\n" if bootspec.initrd: - entry += f'module_path: ' + get_kernel_uri(bootspec.initrd) + '\n' + entry += f"module_path: " + get_kernel_uri(bootspec.initrd) + "\n" if bootspec.initrdSecrets: - base_path = str(limine_install_dir) + '/kernels/' - initrd_secrets_path = base_path + os.path.basename(bootspec.toplevel) + '-secrets' + base_path = str(limine_install_dir) + "/kernels/" + initrd_secrets_path = ( + base_path + os.path.basename(bootspec.toplevel) + "-secrets" + ) if not os.path.exists(base_path): os.makedirs(base_path) old_umask = os.umask(0o137) - initrd_secrets_path_temp = tempfile.mktemp(os.path.basename(bootspec.toplevel) + '-secrets') + initrd_secrets_path_temp = tempfile.mktemp( + os.path.basename(bootspec.toplevel) + "-secrets" + ) if os.system(bootspec.initrdSecrets + " " + initrd_secrets_path_temp) != 0: - print(f'warning: failed to create initrd secrets for "{label}"', file=sys.stderr) - print(f'note: if this is an older generation there is nothing to worry about') + print( + f'warning: failed to create initrd secrets for "{label}"', + file=sys.stderr, + ) + print( + f"note: if this is an older generation there is nothing to worry about" + ) if os.path.exists(initrd_secrets_path_temp): copy_file(initrd_secrets_path_temp, initrd_secrets_path) os.unlink(initrd_secrets_path_temp) - entry += 'module_path: ' + get_kernel_uri(initrd_secrets_path) + '\n' + entry += "module_path: " + get_kernel_uri(initrd_secrets_path) + "\n" os.umask(old_umask) return entry def generate_config_entry(profile: str, gen: str, special: bool) -> str: - time = datetime.datetime.fromtimestamp(os.stat(get_system_path(profile,gen), follow_symlinks=False).st_mtime).strftime("%F %H:%M:%S") - boot_json = json.load(open(os.path.join(get_system_path(profile, gen), 'boot.json'), 'r')) + time = datetime.datetime.fromtimestamp( + os.stat(get_system_path(profile, gen), follow_symlinks=False).st_mtime + ).strftime("%F %H:%M:%S") + boot_json = json.load( + open(os.path.join(get_system_path(profile, gen), "boot.json"), "r") + ) boot_spec = bootjson_to_bootspec(boot_json) specialisation_list = boot_spec.specialisations.items() @@ -332,37 +366,37 @@ def generate_config_entry(profile: str, gen: str, special: bool) -> str: entry = "" # Xen, if configured, should be listed first for each generation - if boot_spec.xen and 'version' in boot_spec.xen: - xen_version = boot_spec.xen['version'] - if config('efiSupport'): + if boot_spec.xen and "version" in boot_spec.xen: + xen_version = boot_spec.xen["version"] + if config("efiSupport"): entry += xen_config_entry(2, boot_spec, xen_version, gen, time, True) entry += xen_config_entry(2, boot_spec, xen_version, gen, time, False) if len(specialisation_list) > 0: depth += 1 - entry += '/' * (depth-1) + entry += "/" * (depth - 1) if special: - entry += '+' + entry += "+" - entry += f'Generation {gen}' + '\n' - entry += config_entry(depth, boot_spec, f'Default', str(time)) + entry += f"Generation {gen}" + "\n" + entry += config_entry(depth, boot_spec, f"Default", str(time)) else: - entry += config_entry(depth, boot_spec, f'Generation {gen}', str(time)) + entry += config_entry(depth, boot_spec, f"Generation {gen}", str(time)) for spec, spec_boot_spec in specialisation_list: - entry += config_entry(depth, spec_boot_spec, f'{spec}', str(time)) + entry += config_entry(depth, spec_boot_spec, f"{spec}", str(time)) return entry def find_disk_device(part: str) -> str: part = os.path.realpath(part) - part = part.removeprefix('/dev/') - disk = os.path.realpath(os.path.join('/sys', 'class', 'block', part)) + part = part.removeprefix("/dev/") + disk = os.path.realpath(os.path.join("/sys", "class", "block", part)) disk = os.path.dirname(disk) - return os.path.join('/dev', os.path.basename(disk)) + return os.path.join("/dev", os.path.basename(disk)) def find_mounted_device(path: str) -> str: @@ -403,34 +437,48 @@ def install_bootloader() -> None: boot_fs = None - for mount_point, fs in config('fileSystems').items(): - if mount_point == '/boot': + for mount_point, fs in config("fileSystems").items(): + if mount_point == "/boot": boot_fs = fs - if config('efiSupport'): - limine_install_dir = os.path.join(str(config('efiMountPoint')), 'limine') - elif boot_fs and is_fs_type_supported(boot_fs['fsType']) and not is_encrypted(boot_fs['device']): - limine_install_dir = '/boot/limine' + if config("efiSupport"): + limine_install_dir = os.path.join(str(config("efiMountPoint")), "limine") + elif ( + boot_fs + and is_fs_type_supported(boot_fs["fsType"]) + and not is_encrypted(boot_fs["device"]) + ): + limine_install_dir = "/boot/limine" else: possible_causes = [] if not boot_fs: - possible_causes.append(f'/limine on the boot partition (not present)') + possible_causes.append(f"/limine on the boot partition (not present)") else: - is_boot_fs_type_ok = is_fs_type_supported(boot_fs['fsType']) - is_boot_fs_encrypted = is_encrypted(boot_fs['device']) - possible_causes.append(f'/limine on the boot partition ({is_boot_fs_type_ok=} {is_boot_fs_encrypted=})') + is_boot_fs_type_ok = is_fs_type_supported(boot_fs["fsType"]) + is_boot_fs_encrypted = is_encrypted(boot_fs["device"]) + possible_causes.append( + f"/limine on the boot partition ({is_boot_fs_type_ok=} {is_boot_fs_encrypted=})" + ) - causes_str = textwrap.indent('\n'.join(possible_causes), ' - ') + causes_str = textwrap.indent("\n".join(possible_causes), " - ") - raise Exception(textwrap.dedent(''' + raise Exception( + textwrap.dedent(""" Could not find a valid place for Limine configuration files!' Possible candidates that were ruled out: - ''') + causes_str + textwrap.dedent(''' + """) + + causes_str + + textwrap.dedent(""" Limine cannot be installed on a system without an unencrypted partition formatted as FAT. - ''')) + """) + ) - if config('secureBoot', 'enable') and not config('secureBoot', 'autoGenerateKeys') and not os.path.exists("/var/lib/sbctl"): + if ( + config("secureBoot", "enable") + and not config("secureBoot", "autoGenerateKeys") + and not os.path.exists("/var/lib/sbctl") + ): print("There are no sbctl secure boot keys present. Please generate some.") sys.exit(1) @@ -441,65 +489,103 @@ def install_bootloader() -> None: for file in files: paths[os.path.join(dir, file)] = False - limine_xen_dir = os.path.join(limine_install_dir, 'xen') + limine_xen_dir = os.path.join(limine_install_dir, "xen") if os.path.exists(limine_xen_dir): - print(f'cleaning {limine_xen_dir}') + print(f"cleaning {limine_xen_dir}") shutil.rmtree(limine_xen_dir) os.makedirs(limine_xen_dir) - profiles = [('system', get_gens())] + profiles = [("system", get_gens())] for profile in get_profiles(): profiles += [(profile, get_gens(profile))] - timeout = config('timeout') - editor_enabled = bool_to_yes_no(config('enableEditor')) - hash_mismatch_panic = bool_to_yes_no(config('panicOnChecksumMismatch')) + timeout = config("timeout") + editor_enabled = bool_to_yes_no(config("enableEditor")) + hash_mismatch_panic = bool_to_yes_no(config("panicOnChecksumMismatch")) last_gen = get_gens()[-1] - last_gen_json = json.load(open(os.path.join(get_system_path('system', last_gen), 'boot.json'), 'r')) + last_gen_json = json.load( + open(os.path.join(get_system_path("system", last_gen), "boot.json"), "r") + ) last_gen_boot_spec = bootjson_to_bootspec(last_gen_json) - config_file = str(config('extraConfig')) + '\n' - config_file += textwrap.dedent(f''' + config_file = str(config("extraConfig")) + "\n" + config_file += textwrap.dedent(f""" timeout: {timeout} editor_enabled: {editor_enabled} hash_mismatch_panic: {hash_mismatch_panic} graphics: yes default_entry: {3 if len(last_gen_boot_spec.specialisations.items()) > 0 else 2} - ''') + """) - for wallpaper in config('style', 'wallpapers'): - config_file += f'''wallpaper: {get_copied_path_uri(wallpaper, 'wallpapers')}\n''' + for wallpaper in config("style", "wallpapers"): + config_file += ( + f"""wallpaper: {get_copied_path_uri(wallpaper, "wallpapers")}\n""" + ) - config_file += option_from_config('wallpaper_style', ['style', 'wallpaperStyle']) - config_file += option_from_config('backdrop', ['style', 'backdrop']) + config_file += option_from_config("wallpaper_style", ["style", "wallpaperStyle"]) + config_file += option_from_config("backdrop", ["style", "backdrop"]) - config_file += option_from_config('interface_resolution', ['style', 'interface', 'resolution']) - config_file += option_from_config('interface_branding', ['style', 'interface', 'branding']) - config_file += option_from_config('interface_branding_colour', ['style', 'interface', 'brandingColor']) - config_file += option_from_config('interface_help_colour', ['style', 'interface', 'helpColor']) - config_file += option_from_config('interface_help_colour_bright', ['style', 'interface', 'helpColorBright']) - config_file += option_from_config('interface_help_hidden', ['style', 'interface', 'helpHidden']) - config_file += option_from_config('term_font_scale', ['style', 'graphicalTerminal', 'font', 'scale']) - config_file += option_from_config('term_font_spacing', ['style', 'graphicalTerminal', 'font', 'spacing']) - config_file += option_from_config('term_palette', ['style', 'graphicalTerminal', 'palette']) - config_file += option_from_config('term_palette_bright', ['style', 'graphicalTerminal', 'brightPalette']) - config_file += option_from_config('term_foreground', ['style', 'graphicalTerminal', 'foreground']) - config_file += option_from_config('term_background', ['style', 'graphicalTerminal', 'background']) - config_file += option_from_config('term_foreground_bright', ['style', 'graphicalTerminal', 'brightForeground']) - config_file += option_from_config('term_background_bright', ['style', 'graphicalTerminal', 'brightBackground']) - config_file += option_from_config('term_margin', ['style', 'graphicalTerminal', 'margin']) - config_file += option_from_config('term_margin_gradient', ['style', 'graphicalTerminal', 'marginGradient']) + config_file += option_from_config( + "interface_resolution", ["style", "interface", "resolution"] + ) + config_file += option_from_config( + "interface_branding", ["style", "interface", "branding"] + ) + config_file += option_from_config( + "interface_branding_colour", ["style", "interface", "brandingColor"] + ) + config_file += option_from_config( + "interface_help_colour", ["style", "interface", "helpColor"] + ) + config_file += option_from_config( + "interface_help_colour_bright", ["style", "interface", "helpColorBright"] + ) + config_file += option_from_config( + "interface_help_hidden", ["style", "interface", "helpHidden"] + ) + config_file += option_from_config( + "term_font_scale", ["style", "graphicalTerminal", "font", "scale"] + ) + config_file += option_from_config( + "term_font_spacing", ["style", "graphicalTerminal", "font", "spacing"] + ) + config_file += option_from_config( + "term_palette", ["style", "graphicalTerminal", "palette"] + ) + config_file += option_from_config( + "term_palette_bright", ["style", "graphicalTerminal", "brightPalette"] + ) + config_file += option_from_config( + "term_foreground", ["style", "graphicalTerminal", "foreground"] + ) + config_file += option_from_config( + "term_background", ["style", "graphicalTerminal", "background"] + ) + config_file += option_from_config( + "term_foreground_bright", ["style", "graphicalTerminal", "brightForeground"] + ) + config_file += option_from_config( + "term_background_bright", ["style", "graphicalTerminal", "brightBackground"] + ) + config_file += option_from_config( + "term_margin", ["style", "graphicalTerminal", "margin"] + ) + config_file += option_from_config( + "term_margin_gradient", ["style", "graphicalTerminal", "marginGradient"] + ) - config_file += textwrap.dedent(f''' - # {config('distroName')} boot entries start here - ''') + config_file += textwrap.dedent(f""" + # {config("distroName")} boot entries start here + """) - for (profile, gens) in profiles: - group_name = 'default profile' if profile == 'system' else f"profile '{profile}'" - config_file += f'/+{config('distroName')} {group_name}\n' + for profile, gens in profiles: + group_name = ( + "default profile" if profile == "system" else f"profile '{profile}'" + ) + config_file += f"/+{config('distroName')} {group_name}\n" isFirst = True @@ -507,12 +593,12 @@ def install_bootloader() -> None: config_file += generate_config_entry(profile, gen, isFirst) isFirst = False - config_file_path = os.path.join(limine_install_dir, 'limine.conf') - config_file += f'\n# {config('distroName')} boot entries end here\n\n' + config_file_path = os.path.join(limine_install_dir, "limine.conf") + config_file += f"\n# {config('distroName')} boot entries end here\n\n" - config_file += str(config('extraEntries')) + config_file += str(config("extraEntries")) - with open(f"{config_file_path}.tmp", 'w') as file: + with open(f"{config_file_path}.tmp", "w") as file: file.truncate() file.write(config_file.strip()) file.flush() @@ -521,150 +607,208 @@ def install_bootloader() -> None: paths[config_file_path] = True - for dest_path, source_path in config('additionalFiles').items(): + for dest_path, source_path in config("additionalFiles").items(): dest_path = os.path.join(limine_install_dir, dest_path) copy_file(source_path, dest_path) - limine_binary = os.path.join(str(config('liminePath')), 'bin', 'limine') - cpu_family = config('hostArchitecture', 'family') - if config('efiSupport'): + limine_binary = os.path.join(str(config("liminePath")), "bin", "limine") + cpu_family = config("hostArchitecture", "family") + if config("efiSupport"): boot_file = "" - if cpu_family == 'x86': - if config('hostArchitecture', 'bits') == 32: - boot_file = 'BOOTIA32.EFI' - elif config('hostArchitecture', 'bits') == 64: - boot_file = 'BOOTX64.EFI' - elif cpu_family == 'arm': - if config('hostArchitecture', 'arch') == 'armv8-a' and config('hostArchitecture', 'bits') == 64: - boot_file = 'BOOTAA64.EFI' + if cpu_family == "x86": + if config("hostArchitecture", "bits") == 32: + boot_file = "BOOTIA32.EFI" + elif config("hostArchitecture", "bits") == 64: + boot_file = "BOOTX64.EFI" + elif cpu_family == "arm": + if ( + config("hostArchitecture", "arch") == "armv8-a" + and config("hostArchitecture", "bits") == 64 + ): + boot_file = "BOOTAA64.EFI" else: - raise Exception(f'Unsupported CPU arch: {config("hostArchitecture", "arch")}') + raise Exception( + f"Unsupported CPU arch: {config('hostArchitecture', 'arch')}" + ) else: - raise Exception(f'Unsupported CPU family: {cpu_family}') + raise Exception(f"Unsupported CPU family: {cpu_family}") - efi_path = os.path.join(str(config('liminePath')), 'share', 'limine', boot_file) - dest_path = os.path.join(str(config('efiMountPoint')), 'efi', 'boot' if config('efiRemovable') else 'limine', boot_file) + efi_path = os.path.join(str(config("liminePath")), "share", "limine", boot_file) + dest_path = os.path.join( + str(config("efiMountPoint")), + "efi", + "boot" if config("efiRemovable") else "limine", + boot_file, + ) copy_file(efi_path, dest_path) - if config('enrollConfig'): + if config("enrollConfig"): b2sum = hashlib.blake2b() b2sum.update(config_file.strip().encode()) try: - subprocess.run([limine_binary, 'enroll-config', dest_path, b2sum.hexdigest()]) + subprocess.run( + [limine_binary, "enroll-config", dest_path, b2sum.hexdigest()] + ) except: - print('error: failed to enroll limine config.', file=sys.stderr) + print("error: failed to enroll limine config.", file=sys.stderr) sys.exit(1) - if config('secureBoot', 'enable'): - sbctl = os.path.join(str(config('secureBoot', 'sbctl')), 'bin', 'sbctl') - if not os.path.exists("/var/lib/sbctl/keys") and config('secureBoot', 'autoGenerateKeys'): - print('auto generating keys') + if config("secureBoot", "enable"): + sbctl = os.path.join(str(config("secureBoot", "sbctl")), "bin", "sbctl") + if not os.path.exists("/var/lib/sbctl/keys") and config( + "secureBoot", "autoGenerateKeys" + ): + print("auto generating keys") try: - subprocess.run([sbctl, 'create-keys']) + subprocess.run([sbctl, "create-keys"]) except: - print('error: failed to create keys', file=sys.stderr) + print("error: failed to create keys", file=sys.stderr) sys.exit(1) - if config('secureBoot', 'autoEnrollKeys', 'enable'): + if config("secureBoot", "autoEnrollKeys", "enable"): try: - command = [sbctl, 'enroll-keys'] - command.extend(config('secureBoot', 'autoEnrollKeys', 'extraArgs')) + command = [sbctl, "enroll-keys"] + command.extend( + config("secureBoot", "autoEnrollKeys", "extraArgs") + ) subprocess.run(command) except: - print('error: failed to enroll keys', file=sys.stderr) + print("error: failed to enroll keys", file=sys.stderr) sys.exit(1) - print('signing limine...') + print("signing limine...") try: - subprocess.run([sbctl, 'sign', dest_path]) + subprocess.run([sbctl, "sign", dest_path]) except: - print('error: failed to sign limine', file=sys.stderr) + print("error: failed to sign limine", file=sys.stderr) sys.exit(1) - if not config('efiRemovable') and not config('canTouchEfiVariables'): - print('warning: boot.loader.efi.canTouchEfiVariables is set to false while boot.loader.limine.efiInstallAsRemovable.\n This may render the system unbootable.') + if not config("efiRemovable") and not config("canTouchEfiVariables"): + print( + "warning: boot.loader.efi.canTouchEfiVariables is set to false while boot.loader.limine.efiInstallAsRemovable.\n This may render the system unbootable." + ) - if config('canTouchEfiVariables'): - if config('efiRemovable'): - print('note: boot.loader.limine.efiInstallAsRemovable is true, no need to add EFI entry.') + if config("canTouchEfiVariables"): + if config("efiRemovable"): + print( + "note: boot.loader.limine.efiInstallAsRemovable is true, no need to add EFI entry." + ) else: - efibootmgr = os.path.join(str(config('efiBootMgrPath')), 'bin', 'efibootmgr') - efi_partition = find_mounted_device(str(config('efiMountPoint'))) + efibootmgr = os.path.join( + str(config("efiBootMgrPath")), "bin", "efibootmgr" + ) + efi_partition = find_mounted_device(str(config("efiMountPoint"))) efi_disk = find_disk_device(efi_partition) - efibootmgr_output = subprocess.check_output([efibootmgr], stderr=subprocess.STDOUT, universal_newlines=True) + efibootmgr_output = subprocess.check_output( + [efibootmgr], stderr=subprocess.STDOUT, universal_newlines=True + ) # Check the output of `efibootmgr` to find if limine is already installed and present in the boot record limine_boot_entry = None - if matches := re.findall(r'Boot([0-9a-fA-F]{4})\*? Limine', efibootmgr_output): + if matches := re.findall( + r"Boot([0-9a-fA-F]{4})\*? Limine", efibootmgr_output + ): limine_boot_entry = matches[0] # If there's already a Limine entry, replace it if limine_boot_entry: - boot_order = re.findall(r'BootOrder: ((?:[0-9a-fA-F]{4},?)*)', efibootmgr_output)[0] + boot_order = re.findall( + r"BootOrder: ((?:[0-9a-fA-F]{4},?)*)", efibootmgr_output + )[0] - efibootmgr_output = subprocess.check_output([ - efibootmgr, - '-b', limine_boot_entry, - '-B', - ], stderr=subprocess.STDOUT, universal_newlines=True) + efibootmgr_output = subprocess.check_output( + [ + efibootmgr, + "-b", + limine_boot_entry, + "-B", + ], + stderr=subprocess.STDOUT, + universal_newlines=True, + ) - efibootmgr_output = subprocess.check_output([ - efibootmgr, - '-c', - '-b', limine_boot_entry, - '-d', efi_disk, - '-p', efi_partition.removeprefix(efi_disk).removeprefix('p'), - '-l', f'\\efi\\limine\\{boot_file}', - '-L', 'Limine', - '-o', boot_order, - ], stderr=subprocess.STDOUT, universal_newlines=True) + efibootmgr_output = subprocess.check_output( + [ + efibootmgr, + "-c", + "-b", + limine_boot_entry, + "-d", + efi_disk, + "-p", + efi_partition.removeprefix(efi_disk).removeprefix("p"), + "-l", + f"\\efi\\limine\\{boot_file}", + "-L", + "Limine", + "-o", + boot_order, + ], + stderr=subprocess.STDOUT, + universal_newlines=True, + ) else: - efibootmgr_output = subprocess.check_output([ - efibootmgr, - '-c', - '-d', efi_disk, - '-p', efi_partition.removeprefix(efi_disk).removeprefix('p'), - '-l', f'\\efi\\limine\\{boot_file}', - '-L', 'Limine', - ], stderr=subprocess.STDOUT, universal_newlines=True) + efibootmgr_output = subprocess.check_output( + [ + efibootmgr, + "-c", + "-d", + efi_disk, + "-p", + efi_partition.removeprefix(efi_disk).removeprefix("p"), + "-l", + f"\\efi\\limine\\{boot_file}", + "-L", + "Limine", + ], + stderr=subprocess.STDOUT, + universal_newlines=True, + ) - if config('biosSupport'): - if cpu_family != 'x86': - raise Exception(f'Unsupported CPU family for BIOS install: {cpu_family}') + if config("biosSupport"): + if cpu_family != "x86": + raise Exception(f"Unsupported CPU family for BIOS install: {cpu_family}") - limine_sys = os.path.join(str(config('liminePath')), 'share', 'limine', 'limine-bios.sys') - limine_sys_dest = os.path.join(limine_install_dir, 'limine-bios.sys') + limine_sys = os.path.join( + str(config("liminePath")), "share", "limine", "limine-bios.sys" + ) + limine_sys_dest = os.path.join(limine_install_dir, "limine-bios.sys") copy_file(limine_sys, limine_sys_dest) - device = str(config('biosDevice')) + device = str(config("biosDevice")) - if device == 'nodev': - print("note: boot.loader.limine.biosSupport is set, but device is set to nodev, only the stage 2 bootloader will be installed.", file=sys.stderr) + if device == "nodev": + print( + "note: boot.loader.limine.biosSupport is set, but device is set to nodev, only the stage 2 bootloader will be installed.", + file=sys.stderr, + ) return - limine_deploy_args: List[str] = [limine_binary, 'bios-install', device] + limine_deploy_args: List[str] = [limine_binary, "bios-install", device] - if config('partitionIndex'): - limine_deploy_args.append(str(config('partitionIndex'))) + if config("partitionIndex"): + limine_deploy_args.append(str(config("partitionIndex"))) - if config('force'): - limine_deploy_args.append('--force') + if config("force"): + limine_deploy_args.append("--force") try: subprocess.run(limine_deploy_args) except: raise Exception( - 'Failed to deploy BIOS stage 1 Limine bootloader!\n' + - 'You might want to try enabling the `boot.loader.limine.force` option.') + "Failed to deploy BIOS stage 1 Limine bootloader!\n" + + "You might want to try enabling the `boot.loader.limine.force` option." + ) print("removing unused boot files...") for path in paths: if not paths[path] and os.path.exists(path): os.remove(path) + def main() -> None: try: install_bootloader() @@ -675,7 +819,11 @@ def main() -> None: # event sync the efi filesystem after each update. rc = libc.syncfs(os.open(f"{str(config('efiMountPoint'))}", os.O_RDONLY)) if rc != 0: - print(f"could not sync {str(config('efiMountPoint'))}: {os.strerror(rc)}", file=sys.stderr) + print( + f"could not sync {str(config('efiMountPoint'))}: {os.strerror(rc)}", + file=sys.stderr, + ) -if __name__ == '__main__': + +if __name__ == "__main__": main() From e68249a28b64fb920d9756d21a1ae89619b3f696 Mon Sep 17 00:00:00 2001 From: r-vdp Date: Mon, 22 Jun 2026 16:04:57 +0200 Subject: [PATCH 2/4] limine: align concatenated initrd modules to 4 bytes The kernel's initramfs unpacker only accepts a raw cpio header at a 4-byte aligned offset, so multiple module_path entries need padding between them. Without this an uncompressed cpio after the main compressed initrd fails with "invalid magic at start of compressed archive". Patch merged upstream as Limine PR #599, remove on next release. --- pkgs/by-name/li/limine/package.nix | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pkgs/by-name/li/limine/package.nix b/pkgs/by-name/li/limine/package.nix index 6296234c02df..2f1f8fe3e5f1 100644 --- a/pkgs/by-name/li/limine/package.nix +++ b/pkgs/by-name/li/limine/package.nix @@ -1,5 +1,6 @@ # Derivation containing the Limine host tool and the compiled bootloader { + fetchpatch, fetchurl, lib, llvmPackages, @@ -57,6 +58,16 @@ stdenv.mkDerivation (finalAttrs: { hash = "sha256-8aUp2lzVClyje6WHMTOnuOclhLEn1zMf6U5VTl5gEvc="; }; + patches = [ + # Merged upstream in https://github.com/Limine-Bootloader/Limine/pull/599, + # remove on the next release. + (fetchpatch { + name = "align-linux-modules.patch"; + url = "https://github.com/Limine-Bootloader/Limine/commit/6635ae4ff2321f9a3c85116a57a00e3b6edc100b.patch"; + hash = "sha256-poAcZND5xwS8npHCytS9lwcQi9oCEcc4bR+6HKVXJ78="; + }) + ]; + enableParallelBuilding = true; hardeningDisable = lib.optionals missingZerocallusedregs [ From 5640e1f935ce7b602e0e27b7f371901c754afe21 Mon Sep 17 00:00:00 2001 From: r-vdp Date: Thu, 25 Jun 2026 22:46:15 +0200 Subject: [PATCH 3/4] nixos/limine: add extraInstallCommands option Allows running additional shell commands after the limine install script generates menu entries, mirroring the systemd-boot option of the same name. Co-authored-by: Florian Klink --- .../system/boot/loader/limine/limine.nix | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/nixos/modules/system/boot/loader/limine/limine.nix b/nixos/modules/system/boot/loader/limine/limine.nix index c9725a6fee00..956fa78800ef 100644 --- a/nixos/modules/system/boot/loader/limine/limine.nix +++ b/nixos/modules/system/boot/loader/limine/limine.nix @@ -205,6 +205,17 @@ in ''; }; + extraInstallCommands = lib.mkOption { + default = ""; + type = lib.types.lines; + description = '' + Additional shell commands inserted in the bootloader installer + script after generating menu entries. It can be used to expand + on extra boot entries that cannot incorporate certain pieces of + information (such as the resulting `init=` kernel parameter). + ''; + }; + secureBoot = { enable = lib.mkEnableOption null // { description = '' @@ -443,14 +454,25 @@ in system = { boot.loader.id = "limine"; - build.installBootLoader = pkgs.replaceVarsWith { - src = ./limine-install.py; - isExecutable = true; - replacements = { - python3 = pkgs.python3.withPackages (python-packages: [ python-packages.psutil ]); - configPath = limineInstallConfig; - }; - }; + build.installBootLoader = + let + install = pkgs.replaceVarsWith { + src = ./limine-install.py; + isExecutable = true; + replacements = { + python3 = pkgs.python3.withPackages (python-packages: [ python-packages.psutil ]); + configPath = limineInstallConfig; + }; + }; + + final = pkgs.writeScript "limine-install.sh" '' + #!${pkgs.runtimeShell} + set -euo pipefail + ${install} "$@" + ${cfg.extraInstallCommands} + ''; + in + final; }; }) (lib.mkIf (cfg.enable && cfg.secureBoot.enable) { From 5f267f4ddab4e1e461cdb00cf1f41c8041afa2d7 Mon Sep 17 00:00:00 2001 From: r-vdp Date: Thu, 25 Jun 2026 22:46:22 +0200 Subject: [PATCH 4/4] 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 --- nixos/modules/module-list.nix | 1 + nixos/modules/system/boot/extra-initrd.nix | 21 ++++ .../boot/loader/limine/limine-install.py | 13 +++ .../systemd-boot/systemd-boot-builder.py | 36 ++++--- nixos/tests/all-tests.nix | 3 + nixos/tests/extra-initrd.nix | 96 +++++++++++++++++++ 6 files changed, 157 insertions(+), 13 deletions(-) create mode 100644 nixos/modules/system/boot/extra-initrd.nix create mode 100644 nixos/tests/extra-initrd.nix diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 0fd01c232583..334ebc3de609 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -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 diff --git a/nixos/modules/system/boot/extra-initrd.nix b/nixos/modules/system/boot/extra-initrd.nix new file mode 100644 index 000000000000..22451c64da7d --- /dev/null +++ b/nixos/modules/system/boot/extra-initrd.nix @@ -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; + }; + }; + }; +} diff --git a/nixos/modules/system/boot/loader/limine/limine-install.py b/nixos/modules/system/boot/loader/limine/limine-install.py index 629a2fef6c76..0ddbd8a9f29b 100644 --- a/nixos/modules/system/boot/loader/limine/limine-install.py +++ b/nixos/modules/system/boot/loader/limine/limine-install.py @@ -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 = ( diff --git a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py index 06c15ee6674b..534efa8877e2 100644 --- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py +++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py @@ -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) diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 24b3d1216af7..9651010181ca 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -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; diff --git a/nixos/tests/extra-initrd.nix b/nixos/tests/extra-initrd.nix new file mode 100644 index 000000000000..20da3ad8d11b --- /dev/null +++ b/nixos/tests/extra-initrd.nix @@ -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; + }; +}