diff options
Diffstat (limited to 'nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py')
-rw-r--r-- | nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py | 118 |
1 files changed, 79 insertions, 39 deletions
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 694d34d1c059a..6db0ee7a4e475 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 @@ -12,7 +12,7 @@ import subprocess import sys import warnings import json -from typing import NamedTuple, Dict, List +from typing import NamedTuple, Any from dataclasses import dataclass # These values will be replaced with actual values during the package build @@ -21,13 +21,14 @@ BOOT_MOUNT_POINT = "@bootMountPoint@" LOADER_CONF = f"{EFI_SYS_MOUNT_POINT}/loader/loader.conf" # Always stored on the ESP NIXOS_DIR = "@nixosDir@" TIMEOUT = "@timeout@" -EDITOR = "@editor@" == "1" +EDITOR = "@editor@" == "1" # noqa: PLR0133 CONSOLE_MODE = "@consoleMode@" BOOTSPEC_TOOLS = "@bootspecTools@" DISTRO_NAME = "@distroName@" NIX = "@nix@" SYSTEMD = "@systemd@" CONFIGURATION_LIMIT = int("@configurationLimit@") +REBOOT_FOR_BITLOCKER = bool("@rebootForBitlocker@") CAN_TOUCH_EFI_VARIABLES = "@canTouchEfiVariables@" GRACEFUL = "@graceful@" COPY_EXTRA_FILES = "@copyExtraFiles@" @@ -38,17 +39,23 @@ class BootSpec: init: str initrd: str kernel: str - kernelParams: List[str] + kernelParams: list[str] # noqa: N815 label: str system: str toplevel: str - specialisations: Dict[str, "BootSpec"] - sortKey: str - initrdSecrets: str | None = None + specialisations: dict[str, "BootSpec"] + sortKey: str # noqa: N815 + devicetree: str | None = None # noqa: N815 + initrdSecrets: str | None = None # noqa: N815 libc = ctypes.CDLL("libc.so.6") +FILE = None | int + +def run(cmd: list[str], stdout: FILE = None) -> subprocess.CompletedProcess[str]: + return subprocess.run(cmd, check=True, text=True, stdout=stdout) + class SystemIdentifier(NamedTuple): profile: str | None generation: int @@ -94,11 +101,12 @@ def generation_conf_filename(profile: str | None, generation: int, specialisatio def write_loader_conf(profile: str | None, generation: int, specialisation: str | None) -> None: with open(f"{LOADER_CONF}.tmp", 'w') as f: - if TIMEOUT != "": - f.write(f"timeout {TIMEOUT}\n") + f.write(f"timeout {TIMEOUT}\n") f.write("default %s\n" % generation_conf_filename(profile, generation, specialisation)) if not EDITOR: f.write("editor 0\n") + if REBOOT_FOR_BITLOCKER: + f.write("reboot-for-bitlocker yes\n"); f.write(f"console-mode {CONSOLE_MODE}\n") f.flush() os.fsync(f.fileno()) @@ -112,25 +120,30 @@ def get_bootspec(profile: str | None, generation: int) -> BootSpec: boot_json_f = open(boot_json_path, 'r') bootspec_json = json.load(boot_json_f) else: - boot_json_str = subprocess.check_output([ - f"{BOOTSPEC_TOOLS}/bin/synthesize", - "--version", - "1", - system_directory, - "/dev/stdout"], - universal_newlines=True) + boot_json_str = run( + [ + f"{BOOTSPEC_TOOLS}/bin/synthesize", + "--version", + "1", + system_directory, + "/dev/stdout", + ], + stdout=subprocess.PIPE, + ).stdout bootspec_json = json.loads(boot_json_str) return bootspec_from_json(bootspec_json) -def bootspec_from_json(bootspec_json: Dict) -> BootSpec: +def bootspec_from_json(bootspec_json: dict[str, Any]) -> BootSpec: specialisations = bootspec_json['org.nixos.specialisation.v1'] specialisations = {k: bootspec_from_json(v) for k, v in specialisations.items()} systemdBootExtension = bootspec_json.get('org.nixos.systemd-boot', {}) sortKey = systemdBootExtension.get('sortKey', 'nixos') + devicetree = systemdBootExtension.get('devicetree') return BootSpec( **bootspec_json['org.nixos.bootspec.v1'], specialisations=specialisations, - sortKey=sortKey + sortKey=sortKey, + devicetree=devicetree, ) @@ -149,6 +162,7 @@ def write_entry(profile: str | None, generation: int, specialisation: str | None bootspec = bootspec.specialisations[specialisation] kernel = copy_from_file(bootspec.kernel) initrd = copy_from_file(bootspec.initrd) + devicetree = copy_from_file(bootspec.devicetree) if bootspec.devicetree is not None else None title = "{name}{profile}{specialisation}".format( name=DISTRO_NAME, @@ -157,7 +171,7 @@ def write_entry(profile: str | None, generation: int, specialisation: str | None try: if bootspec.initrdSecrets is not None: - subprocess.check_call([bootspec.initrdSecrets, f"{BOOT_MOUNT_POINT}%s" % (initrd)]) + run([bootspec.initrdSecrets, f"{BOOT_MOUNT_POINT}%s" % (initrd)]) except subprocess.CalledProcessError: if current: print("failed to create initrd secrets!", file=sys.stderr) @@ -186,19 +200,25 @@ def write_entry(profile: str | None, generation: int, specialisation: str | None description=f"{bootspec.label}, built on {build_date}")) if machine_id is not None: f.write("machine-id %s\n" % machine_id) + if devicetree is not None: + f.write("devicetree %s\n" % devicetree) f.flush() os.fsync(f.fileno()) os.rename(tmp_path, entry_file) def get_generations(profile: str | None = None) -> list[SystemIdentifier]: - gen_list = subprocess.check_output([ - f"{NIX}/bin/nix-env", - "--list-generations", - "-p", - "/nix/var/nix/profiles/%s" % ("system-profiles/" + profile if profile else "system")], - universal_newlines=True) - gen_lines = gen_list.split('\n') + gen_list = run( + [ + f"{NIX}/bin/nix-env", + "--list-generations", + "-p", + "/nix/var/nix/profiles/%s" + % ("system-profiles/" + profile if profile else "system"), + ], + stdout=subprocess.PIPE, + ).stdout + gen_lines = gen_list.split("\n") gen_lines.pop() configurationLimit = CONFIGURATION_LIMIT @@ -214,8 +234,8 @@ def get_generations(profile: str | None = None) -> list[SystemIdentifier]: def remove_old_entries(gens: list[SystemIdentifier]) -> None: - rex_profile = re.compile(r"^" + re.escape(BOOT_MOUNT_POINT) + "/loader/entries/nixos-(.*)-generation-.*\.conf$") - rex_generation = re.compile(r"^" + re.escape(BOOT_MOUNT_POINT) + "/loader/entries/nixos.*-generation-([0-9]+)(-specialisation-.*)?\.conf$") + rex_profile = re.compile(r"^" + re.escape(BOOT_MOUNT_POINT) + r"/loader/entries/nixos-(.*)-generation-.*\.conf$") + rex_generation = re.compile(r"^" + re.escape(BOOT_MOUNT_POINT) + r"/loader/entries/nixos.*-generation-([0-9]+)(-specialisation-.*)?\.conf$") known_paths = [] for gen in gens: bootspec = get_bootspec(gen.profile, gen.generation) @@ -230,10 +250,10 @@ def remove_old_entries(gens: list[SystemIdentifier]) -> None: gen_number = int(rex_generation.sub(r"\1", path)) except ValueError: continue - if not (prof, gen_number, None) in gens: + if (prof, gen_number, None) not in gens: os.unlink(path) for path in glob.iglob(f"{BOOT_MOUNT_POINT}/{NIXOS_DIR}/*"): - if not path in known_paths and not os.path.isdir(path): + if path not in known_paths and not os.path.isdir(path): os.unlink(path) @@ -263,9 +283,7 @@ def install_bootloader(args: argparse.Namespace) -> None: # be there on newly installed systems, so let's generate one so that # bootctl can find it and we can also pass it to write_entry() later. cmd = [f"{SYSTEMD}/bin/systemd-machine-id-setup", "--print"] - machine_id = subprocess.run( - cmd, text=True, check=True, stdout=subprocess.PIPE - ).stdout.rstrip() + machine_id = run(cmd, stdout=subprocess.PIPE).stdout.rstrip() if os.getenv("NIXOS_INSTALL_GRUB") == "1": warnings.warn("NIXOS_INSTALL_GRUB env var deprecated, use NIXOS_INSTALL_BOOTLOADER", DeprecationWarning) @@ -288,14 +306,32 @@ def install_bootloader(args: argparse.Namespace) -> None: if os.path.exists(LOADER_CONF): os.unlink(LOADER_CONF) - subprocess.check_call([f"{SYSTEMD}/bin/bootctl", f"--esp-path={EFI_SYS_MOUNT_POINT}"] + bootctl_flags + ["install"]) + run( + [f"{SYSTEMD}/bin/bootctl", f"--esp-path={EFI_SYS_MOUNT_POINT}"] + + bootctl_flags + + ["install"] + ) else: # Update bootloader to latest if needed - available_out = subprocess.check_output([f"{SYSTEMD}/bin/bootctl", "--version"], universal_newlines=True).split()[2] - installed_out = subprocess.check_output([f"{SYSTEMD}/bin/bootctl", f"--esp-path={EFI_SYS_MOUNT_POINT}", "status"], universal_newlines=True) + available_out = run( + [f"{SYSTEMD}/bin/bootctl", "--version"], stdout=subprocess.PIPE + ).stdout.split()[2] + installed_out = run( + [f"{SYSTEMD}/bin/bootctl", f"--esp-path={EFI_SYS_MOUNT_POINT}", "status"], + stdout=subprocess.PIPE, + ).stdout # See status_binaries() in systemd bootctl.c for code which generates this - installed_match = re.search(r"^\W+File:.*/EFI/(?:BOOT|systemd)/.*\.efi \(systemd-boot ([\d.]+[^)]*)\)$", + # Matches + # Available Boot Loaders on ESP: + # ESP: /boot (/dev/disk/by-partuuid/9b39b4c4-c48b-4ebf-bfea-a56b2395b7e0) + # File: └─/EFI/systemd/systemd-bootx64.efi (systemd-boot 255.2) + # But also: + # Available Boot Loaders on ESP: + # ESP: /boot (/dev/disk/by-partuuid/9b39b4c4-c48b-4ebf-bfea-a56b2395b7e0) + # File: ├─/EFI/systemd/HashTool.efi + # └─/EFI/systemd/systemd-bootx64.efi (systemd-boot 255.2) + installed_match = re.search(r"^\W+.*/EFI/(?:BOOT|systemd)/.*\.efi \(systemd-boot ([\d.]+[^)]*)\)$", installed_out, re.IGNORECASE | re.MULTILINE) available_match = re.search(r"^\((.*)\)$", available_out) @@ -311,7 +347,11 @@ def install_bootloader(args: argparse.Namespace) -> None: if installed_version < available_version: print("updating systemd-boot from %s to %s" % (installed_version, available_version)) - subprocess.check_call([f"{SYSTEMD}/bin/bootctl", f"--esp-path={EFI_SYS_MOUNT_POINT}"] + bootctl_flags + ["update"]) + run( + [f"{SYSTEMD}/bin/bootctl", f"--esp-path={EFI_SYS_MOUNT_POINT}"] + + bootctl_flags + + ["update"] + ) os.makedirs(f"{BOOT_MOUNT_POINT}/{NIXOS_DIR}", exist_ok=True) os.makedirs(f"{BOOT_MOUNT_POINT}/loader/entries", exist_ok=True) @@ -362,7 +402,7 @@ def install_bootloader(args: argparse.Namespace) -> None: os.makedirs(f"{BOOT_MOUNT_POINT}/{NIXOS_DIR}/.extra-files", exist_ok=True) - subprocess.check_call(COPY_EXTRA_FILES) + run([COPY_EXTRA_FILES]) def main() -> None: @@ -370,7 +410,7 @@ def main() -> None: parser.add_argument('default_config', metavar='DEFAULT-CONFIG', help=f"The default {DISTRO_NAME} config to boot") args = parser.parse_args() - subprocess.check_call(CHECK_MOUNTPOINTS) + run([CHECK_MOUNTPOINTS]) try: install_bootloader(args) |