diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 0f420ffecbd3..284fe268cb36 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1914,6 +1914,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 be4522299c5b..0ddbd8a9f29b 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 @@ -34,17 +36,20 @@ class BootSpec: label: str toplevel: str specialisations: Dict[str, "BootSpec"] + extraInitrdPaths: list[str] xen: XenBootSpec | None 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 +58,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 +129,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 +139,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 +156,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 +167,26 @@ 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"] + + extraInitrdExtension = bootjson.get("org.nixos.extra-initrd.v1", {}) + extraInitrdPaths = extraInitrdExtension.get("paths", []) + return BootSpec( - **bootjson['org.nixos.bootspec.v1'], + **bootjson["org.nixos.bootspec.v1"], specialisations=specialisations, xen=xen, + extraInitrdPaths=extraInitrdPaths, ) -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 +199,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 +250,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 +271,107 @@ 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" + + for p in bootspec.extraInitrdPaths: + entry += f"module_path: boot():/{p}\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" + + 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 = 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 +379,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 +450,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 +502,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 +606,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 +620,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 +832,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() 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) { 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 20db8a108573..aa1aa8933a87 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; + }; +} 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 [