at master 26 kB view raw
1#!@python3@/bin/python3 -B 2 3from dataclasses import dataclass 4from typing import Any, Callable, Dict, List, Optional, Tuple 5 6import datetime 7import hashlib 8import json 9from ctypes import CDLL 10import os 11import psutil 12import re 13import shutil 14import subprocess 15import sys 16import tempfile 17import textwrap 18 19@dataclass 20class XenBootSpec: 21 """Represent the bootspec extension for Xen dom0 kernels""" 22 23 efiPath: str 24 multibootPath: str 25 params: List[str] 26 version: str 27 28@dataclass 29class BootSpec: 30 system: str 31 init: str 32 kernel: str 33 kernelParams: List[str] 34 label: str 35 toplevel: str 36 specialisations: Dict[str, "BootSpec"] 37 xen: XenBootSpec | None 38 initrd: str | None = None 39 initrdSecrets: str | None = None 40 41install_config = json.load(open('@configPath@', 'r')) 42libc = CDLL("libc.so.6") 43 44limine_install_dir: Optional[str] = None 45can_use_direct_paths = False 46paths: Dict[str, bool] = {} 47 48def config(*path: str) -> Optional[Any]: 49 result = install_config 50 for component in path: 51 result = result[component] 52 return result 53 54def get_system_path(profile: str = 'system', gen: Optional[str] = None, spec: Optional[str] = None) -> str: 55 basename = f'{profile}-{gen}-link' if gen is not None else profile 56 profiles_dir = '/nix/var/nix/profiles' 57 if profile == 'system': 58 result = os.path.join(profiles_dir, basename) 59 else: 60 result = os.path.join(profiles_dir, 'system-profiles', basename) 61 62 if spec is not None: 63 result = os.path.join(result, 'specialisation', spec) 64 65 return result 66 67 68def get_profiles() -> List[str]: 69 profiles_dir = '/nix/var/nix/profiles/system-profiles/' 70 dirs = os.listdir(profiles_dir) if os.path.isdir(profiles_dir) else [] 71 72 return [path for path in dirs if not path.endswith('-link')] 73 74 75def get_gens(profile: str = 'system') -> List[Tuple[int, List[str]]]: 76 nix_env = os.path.join(str(config('nixPath')), 'bin', 'nix-env') 77 output = subprocess.check_output([ 78 nix_env, '--list-generations', 79 '-p', get_system_path(profile), 80 '--option', 'build-users-group', '', 81 ], universal_newlines=True) 82 83 gen_lines = output.splitlines() 84 gen_nums = [int(line.split()[0]) for line in gen_lines] 85 86 return [gen for gen in gen_nums][-config('maxGenerations'):] 87 88 89def is_encrypted(device: str) -> bool: 90 for name in config('luksDevices'): 91 if os.readlink(os.path.join('/dev/mapper', name)) == os.readlink(device): 92 return True 93 94 return False 95 96 97def is_fs_type_supported(fs_type: str) -> bool: 98 return fs_type.startswith('vfat') 99 100 101def get_dest_file(path: str) -> str: 102 package_id = os.path.basename(os.path.dirname(path)) 103 suffix = os.path.basename(path) 104 return f'{package_id}-{suffix}' 105 106 107def get_dest_path(path: str, target: str) -> str: 108 dest_file = get_dest_file(path) 109 return os.path.join(str(limine_install_dir), target, dest_file) 110 111 112def get_copied_path_uri(path: str, target: str) -> str: 113 result = '' 114 115 dest_file = get_dest_file(path) 116 dest_path = get_dest_path(path, target) 117 118 if not os.path.exists(dest_path): 119 copy_file(path, dest_path) 120 else: 121 paths[dest_path] = True 122 123 path_with_prefix = os.path.join('/limine', target, dest_file) 124 result = f'boot():{path_with_prefix}' 125 126 if config('validateChecksums'): 127 with open(path, 'rb') as file: 128 b2sum = hashlib.blake2b() 129 b2sum.update(file.read()) 130 131 result += f'#{b2sum.hexdigest()}' 132 133 return result 134 135 136def get_path_uri(path: str) -> str: 137 return get_copied_path_uri(path, "") 138 139 140def get_file_uri(profile: str, gen: Optional[str], spec: Optional[str], name: str) -> str: 141 gen_path = get_system_path(profile, gen, spec) 142 path_in_store = os.path.realpath(os.path.join(gen_path, name)) 143 return get_path_uri(path_in_store) 144 145 146def get_kernel_uri(kernel_path: str) -> str: 147 return get_copied_path_uri(kernel_path, "kernels") 148 149def bootjson_to_bootspec(bootjson: dict) -> BootSpec: 150 specialisations = bootjson['org.nixos.specialisation.v1'] 151 specialisations = {k: bootjson_to_bootspec(v) for k, v in specialisations.items()} 152 xen = None 153 if 'org.xenproject.bootspec.v2' in bootjson: 154 xen = bootjson['org.xenproject.bootspec.v2'] 155 return BootSpec( 156 **bootjson['org.nixos.bootspec.v1'], 157 specialisations=specialisations, 158 xen=xen, 159 ) 160 161def generate_xen_efi_files( 162 bootspec: BootSpec, 163 gen: str 164 ) -> str: 165 """Generate a Xen EFI xen.cfg file, and copy required files in place. 166 167 Assumes the bootspec has already been validated as having the requried 168 Xen keys. 169 170 Arguments: 171 bootspec -- the NixOS BootSpec requiring Xen EFI configuration 172 gen -- The system generation requiring Xen EFI configuration 173 174 Returns the path to the Xen EFI binary 175 """ 176 177 xen_efi_boot_path = get_copied_path_uri(bootspec.xen['efiPath'], f'xen/{gen}') 178 xen_efi_path = get_dest_path(bootspec.xen['efiPath'], f'xen/{gen}') 179 180 xen_efi_cfg_dir = os.path.dirname(xen_efi_path) 181 xen_efi_cfg_path = xen_efi_path[:-4] + '.cfg' 182 183 if not os.path.exists(xen_efi_cfg_dir): 184 os.makedirs(xen_efi_cfg_dir) 185 186 xen_efi_cfg = ( 187 f'default=nixos{gen}\n\n' + 188 f'[nixos{gen}]\n' 189 ) 190 # set xen dom0 parameters 191 if 'params' in bootspec.xen and len(bootspec.xen['params']) > 0: 192 xen_efi_cfg += 'options=' + ' '.join(bootspec.xen['params']).strip() + '\n' 193 194 # set kernel and copy in-place 195 xen_efi_kernel_path = get_dest_path(bootspec.kernel, f'xen/{gen}') 196 copy_file(bootspec.kernel, xen_efi_kernel_path) 197 xen_efi_cfg += ( 198 'kernel=' + os.path.basename(xen_efi_kernel_path) + ' ' 199 + ' '.join(['init=' + bootspec.init] + bootspec.kernelParams).strip() 200 + '\n' 201 ) 202 203 # set ramdisk and copy initrd in-place 204 if bootspec.initrd: 205 xen_efi_initrd_path = get_dest_path(bootspec.initrd, f'xen/{gen}') 206 copy_file(bootspec.initrd, xen_efi_initrd_path) 207 xen_efi_cfg += 'ramdisk=' + os.path.basename(xen_efi_initrd_path) + '\n' 208 209 with open(xen_efi_cfg_path, 'w') as xen_efi_cfg_file: 210 xen_efi_cfg_file.write(xen_efi_cfg) 211 212 return xen_efi_boot_path 213 214def xen_config_entry( 215 levels: int, bootspec: BootSpec, xenVersion: str, gen: str, time: str, efi: bool 216) -> str: 217 """Generate EFI and BIOS entries for Xen dom0 kernels. 218 219 Arguments: 220 levels -- The number of Limine menu levels for entries 221 bootspec -- The NixOS BootSpec used for generating this Limine configuration 222 xenVersion -- The version of Xen the entry is generated for, from the boot extension 223 gen -- The system generation these entries are generated for 224 time -- The build time for the configuration 225 efi -- True if EFI protocol should be used for this entry 226 """ 227 # generate Xen menu label for the current generation 228 entry = '/' * levels + f'Generation {gen} with Xen {xenVersion}' + (' EFI\n' if efi else '\n') 229 entry += f'comment: Xen {xenVersion} {bootspec.label}, built on {time}\n' 230 # load Xen dom0 as the executable, using multiboot for EFI & BIOS 231 if ( 232 efi and 233 'multibootPath' in bootspec.xen and 234 len(bootspec.xen['multibootPath']) > 0 and 235 os.path.exists(bootspec.xen['multibootPath']) 236 ): 237 # Use the EFI protocol and generate Xen EFI configuration 238 # files and directories which are loaded by Xen's EFI binary 239 # directly. 240 # Ideally both EFI and BIOS booting would use multiboot2, 241 # however Limine's multiboot2 module has trouble finding 242 # an entry-point in Xen's multiboot binary, and multiboot1 243 # doesn't work under EFI. 244 # Upstream Limine issue #482 245 entry += 'protocol: efi\n' 246 entry += ( 247 'path: ' + generate_xen_efi_files(bootspec, gen) + '\n' 248 ) 249 elif ( 250 'multibootPath' in bootspec.xen and 251 len(bootspec.xen['multibootPath']) > 0 and 252 os.path.exists(bootspec.xen['multibootPath']) 253 ): 254 # Use multiboot1 if not generating an EFI entry, as multiboot2 255 # doesn't work under Limine for booting Xen. 256 # Upstream Limine issue #483 257 entry += 'protocol: multiboot\n' 258 entry += ( 259 'path: ' + get_copied_path_uri(bootspec.xen['multibootPath'], f'xen/{gen}') + '\n' 260 ) 261 # set params as the multiboot executable's parameters 262 if 'params' in bootspec.xen and len(bootspec.xen['params']) > 0: 263 # TODO: Understand why the first argument is ignored below? 264 # --- to work around first argument being ignored 265 entry += ( 266 'cmdline: -- ' + ' '.join(bootspec.xen['params']).strip() + '\n' 267 ) 268 # load the linux kernel as the second module 269 entry += 'module_path: ' + get_kernel_uri(bootspec.kernel) + '\n' 270 # set kernel parameters as the parameters to the first module 271 # TODO: Understand why the first argument is ignored below? 272 # --- to work around first argument being ignored 273 entry += ( 274 'module_string: -- ' 275 + ' '.join(['init=' + bootspec.init] + bootspec.kernelParams).strip() 276 + '\n' 277 ) 278 if bootspec.initrd: 279 # the final module is the initrd 280 entry += 'module_path: ' + get_kernel_uri(bootspec.initrd) + '\n' 281 return entry 282 283def config_entry(levels: int, bootspec: BootSpec, label: str, time: str) -> str: 284 entry = '/' * levels + label + '\n' 285 entry += 'protocol: linux\n' 286 entry += f'comment: {bootspec.label}, built on {time}\n' 287 entry += 'kernel_path: ' + get_kernel_uri(bootspec.kernel) + '\n' 288 entry += 'cmdline: ' + ' '.join(['init=' + bootspec.init] + bootspec.kernelParams).strip() + '\n' 289 if bootspec.initrd: 290 entry += f'module_path: ' + get_kernel_uri(bootspec.initrd) + '\n' 291 292 if bootspec.initrdSecrets: 293 base_path = str(limine_install_dir) + '/kernels/' 294 initrd_secrets_path = base_path + os.path.basename(bootspec.toplevel) + '-secrets' 295 if not os.path.exists(base_path): 296 os.makedirs(base_path) 297 298 old_umask = os.umask(0o137) 299 initrd_secrets_path_temp = tempfile.mktemp(os.path.basename(bootspec.toplevel) + '-secrets') 300 301 if os.system(bootspec.initrdSecrets + " " + initrd_secrets_path_temp) != 0: 302 print(f'warning: failed to create initrd secrets for "{label}"', file=sys.stderr) 303 print(f'note: if this is an older generation there is nothing to worry about') 304 305 if os.path.exists(initrd_secrets_path_temp): 306 copy_file(initrd_secrets_path_temp, initrd_secrets_path) 307 os.unlink(initrd_secrets_path_temp) 308 entry += 'module_path: ' + get_kernel_uri(initrd_secrets_path) + '\n' 309 310 os.umask(old_umask) 311 return entry 312 313 314def generate_config_entry(profile: str, gen: str, special: bool) -> str: 315 time = datetime.datetime.fromtimestamp(os.stat(get_system_path(profile,gen), follow_symlinks=False).st_mtime).strftime("%F %H:%M:%S") 316 boot_json = json.load(open(os.path.join(get_system_path(profile, gen), 'boot.json'), 'r')) 317 boot_spec = bootjson_to_bootspec(boot_json) 318 319 specialisation_list = boot_spec.specialisations.items() 320 depth = 2 321 entry = "" 322 323 # Xen, if configured, should be listed first for each generation 324 if boot_spec.xen and 'version' in boot_spec.xen: 325 xen_version = boot_spec.xen['version'] 326 if config('efiSupport'): 327 entry += xen_config_entry(2, boot_spec, xen_version, gen, time, True) 328 entry += xen_config_entry(2, boot_spec, xen_version, gen, time, False) 329 330 if len(specialisation_list) > 0: 331 depth += 1 332 entry += '/' * (depth-1) 333 334 if special: 335 entry += '+' 336 337 entry += f'Generation {gen}' + '\n' 338 entry += config_entry(depth, boot_spec, f'Default', str(time)) 339 else: 340 entry += config_entry(depth, boot_spec, f'Generation {gen}', str(time)) 341 342 for spec, spec_boot_spec in specialisation_list: 343 entry += config_entry(depth, spec_boot_spec, f'{spec}', str(time)) 344 345 return entry 346 347 348def find_disk_device(part: str) -> str: 349 part = os.path.realpath(part) 350 part = part.removeprefix('/dev/') 351 disk = os.path.realpath(os.path.join('/sys', 'class', 'block', part)) 352 disk = os.path.dirname(disk) 353 354 return os.path.join('/dev', os.path.basename(disk)) 355 356 357def find_mounted_device(path: str) -> str: 358 path = os.path.abspath(path) 359 360 while not os.path.ismount(path): 361 path = os.path.dirname(path) 362 363 devices = [x for x in psutil.disk_partitions() if x.mountpoint == path] 364 365 assert len(devices) == 1 366 return devices[0].device 367 368 369def copy_file(from_path: str, to_path: str): 370 dirname = os.path.dirname(to_path) 371 372 if not os.path.exists(dirname): 373 os.makedirs(dirname) 374 375 shutil.copyfile(from_path, to_path + ".tmp") 376 os.rename(to_path + ".tmp", to_path) 377 378 paths[to_path] = True 379 380def option_from_config(name: str, config_path: List[str], conversion: Callable[[str], str] | None = None) -> str: 381 if config(*config_path): 382 return f'{name}: {conversion(config(*config_path)) if conversion else config(*config_path)}\n' 383 return '' 384 385 386def install_bootloader() -> None: 387 global limine_install_dir 388 389 boot_fs = None 390 391 for mount_point, fs in config('fileSystems').items(): 392 if mount_point == '/boot': 393 boot_fs = fs 394 395 if config('efiSupport'): 396 limine_install_dir = os.path.join(str(config('efiMountPoint')), 'limine') 397 elif boot_fs and is_fs_type_supported(boot_fs['fsType']) and not is_encrypted(boot_fs['device']): 398 limine_install_dir = '/boot/limine' 399 else: 400 possible_causes = [] 401 if not boot_fs: 402 possible_causes.append(f'/limine on the boot partition (not present)') 403 else: 404 is_boot_fs_type_ok = is_fs_type_supported(boot_fs['fsType']) 405 is_boot_fs_encrypted = is_encrypted(boot_fs['device']) 406 possible_causes.append(f'/limine on the boot partition ({is_boot_fs_type_ok=} {is_boot_fs_encrypted=})') 407 408 causes_str = textwrap.indent('\n'.join(possible_causes), ' - ') 409 410 raise Exception(textwrap.dedent(''' 411 Could not find a valid place for Limine configuration files!' 412 Possible candidates that were ruled out: 413 ''') + causes_str + textwrap.dedent(''' 414 Limine cannot be installed on a system without an unencrypted 415 partition formatted as FAT. 416 ''')) 417 418 if config('secureBoot', 'enable') and not config('secureBoot', 'createAndEnrollKeys') and not os.path.exists("/var/lib/sbctl"): 419 print("There are no sbctl secure boot keys present. Please generate some.") 420 sys.exit(1) 421 422 if not os.path.exists(limine_install_dir): 423 os.makedirs(limine_install_dir) 424 else: 425 for dir, dirs, files in os.walk(limine_install_dir, topdown=True): 426 for file in files: 427 paths[os.path.join(dir, file)] = False 428 429 limine_xen_dir = os.path.join(limine_install_dir, 'xen') 430 if os.path.exists(limine_xen_dir): 431 print(f'cleaning {limine_xen_dir}') 432 shutil.rmtree(limine_xen_dir) 433 434 os.makedirs(limine_xen_dir) 435 436 profiles = [('system', get_gens())] 437 438 for profile in get_profiles(): 439 profiles += [(profile, get_gens(profile))] 440 441 timeout = config('timeout') 442 editor_enabled = 'yes' if config('enableEditor') else 'no' 443 hash_mismatch_panic = 'yes' if config('panicOnChecksumMismatch') else 'no' 444 445 last_gen = get_gens()[-1] 446 last_gen_json = json.load(open(os.path.join(get_system_path('system', last_gen), 'boot.json'), 'r')) 447 last_gen_boot_spec = bootjson_to_bootspec(last_gen_json) 448 449 config_file = str(config('extraConfig')) + '\n' 450 config_file += textwrap.dedent(f''' 451 timeout: {timeout} 452 editor_enabled: {editor_enabled} 453 hash_mismatch_panic: {hash_mismatch_panic} 454 graphics: yes 455 default_entry: {3 if len(last_gen_boot_spec.specialisations.items()) > 0 else 2} 456 ''') 457 458 for wallpaper in config('style', 'wallpapers'): 459 config_file += f'''wallpaper: {get_copied_path_uri(wallpaper, 'wallpapers')}\n''' 460 461 config_file += option_from_config('wallpaper_style', ['style', 'wallpaperStyle']) 462 config_file += option_from_config('backdrop', ['style', 'backdrop']) 463 464 config_file += option_from_config('interface_resolution', ['style', 'interface', 'resolution']) 465 config_file += option_from_config('interface_branding', ['style', 'interface', 'branding']) 466 config_file += option_from_config('interface_branding_colour', ['style', 'interface', 'brandingColor']) 467 config_file += option_from_config('interface_help_hidden', ['style', 'interface', 'helpHidden']) 468 config_file += option_from_config('term_font_scale', ['style', 'graphicalTerminal', 'font', 'scale']) 469 config_file += option_from_config('term_font_spacing', ['style', 'graphicalTerminal', 'font', 'spacing']) 470 config_file += option_from_config('term_palette', ['style', 'graphicalTerminal', 'palette']) 471 config_file += option_from_config('term_palette_bright', ['style', 'graphicalTerminal', 'brightPalette']) 472 config_file += option_from_config('term_foreground', ['style', 'graphicalTerminal', 'foreground']) 473 config_file += option_from_config('term_background', ['style', 'graphicalTerminal', 'background']) 474 config_file += option_from_config('term_foreground_bright', ['style', 'graphicalTerminal', 'brightForeground']) 475 config_file += option_from_config('term_background_bright', ['style', 'graphicalTerminal', 'brightBackground']) 476 config_file += option_from_config('term_margin', ['style', 'graphicalTerminal', 'margin']) 477 config_file += option_from_config('term_margin_gradient', ['style', 'graphicalTerminal', 'marginGradient']) 478 479 config_file += textwrap.dedent(''' 480 # NixOS boot entries start here 481 ''') 482 483 for (profile, gens) in profiles: 484 group_name = 'default profile' if profile == 'system' else f"profile '{profile}'" 485 config_file += f'/+NixOS {group_name}\n' 486 487 isFirst = True 488 489 for gen in sorted(gens, key=lambda x: x, reverse=True): 490 config_file += generate_config_entry(profile, gen, isFirst) 491 isFirst = False 492 493 config_file_path = os.path.join(limine_install_dir, 'limine.conf') 494 config_file += '\n# NixOS boot entries end here\n\n' 495 496 config_file += str(config('extraEntries')) 497 498 with open(f"{config_file_path}.tmp", 'w') as file: 499 file.truncate() 500 file.write(config_file.strip()) 501 file.flush() 502 os.fsync(file.fileno()) 503 os.rename(f"{config_file_path}.tmp", config_file_path) 504 505 paths[config_file_path] = True 506 507 for dest_path, source_path in config('additionalFiles').items(): 508 dest_path = os.path.join(limine_install_dir, dest_path) 509 510 copy_file(source_path, dest_path) 511 512 limine_binary = os.path.join(str(config('liminePath')), 'bin', 'limine') 513 cpu_family = config('hostArchitecture', 'family') 514 if config('efiSupport'): 515 boot_file = "" 516 if cpu_family == 'x86': 517 if config('hostArchitecture', 'bits') == 32: 518 boot_file = 'BOOTIA32.EFI' 519 elif config('hostArchitecture', 'bits') == 64: 520 boot_file = 'BOOTX64.EFI' 521 elif cpu_family == 'arm': 522 if config('hostArchitecture', 'arch') == 'armv8-a' and config('hostArchitecture', 'bits') == 64: 523 boot_file = 'BOOTAA64.EFI' 524 else: 525 raise Exception(f'Unsupported CPU arch: {config("hostArchitecture", "arch")}') 526 else: 527 raise Exception(f'Unsupported CPU family: {cpu_family}') 528 529 efi_path = os.path.join(str(config('liminePath')), 'share', 'limine', boot_file) 530 dest_path = os.path.join(str(config('efiMountPoint')), 'efi', 'boot' if config('efiRemovable') else 'limine', boot_file) 531 532 copy_file(efi_path, dest_path) 533 534 if config('enrollConfig'): 535 b2sum = hashlib.blake2b() 536 b2sum.update(config_file.strip().encode()) 537 try: 538 subprocess.run([limine_binary, 'enroll-config', dest_path, b2sum.hexdigest()]) 539 except: 540 print('error: failed to enroll limine config.', file=sys.stderr) 541 sys.exit(1) 542 543 if config('secureBoot', 'enable'): 544 sbctl = os.path.join(str(config('secureBoot', 'sbctl')), 'bin', 'sbctl') 545 if config('secureBoot', 'createAndEnrollKeys'): 546 print("TEST MODE: creating and enrolling keys") 547 try: 548 subprocess.run([sbctl, 'create-keys']) 549 except: 550 print('error: failed to create keys', file=sys.stderr) 551 sys.exit(1) 552 try: 553 subprocess.run([sbctl, 'enroll-keys', '--yes-this-might-brick-my-machine']) 554 except: 555 print('error: failed to enroll keys', file=sys.stderr) 556 sys.exit(1) 557 558 print('signing limine...') 559 try: 560 subprocess.run([sbctl, 'sign', dest_path]) 561 except: 562 print('error: failed to sign limine', file=sys.stderr) 563 sys.exit(1) 564 565 if not config('efiRemovable') and not config('canTouchEfiVariables'): 566 print('warning: boot.loader.efi.canTouchEfiVariables is set to false while boot.loader.limine.efiInstallAsRemovable.\n This may render the system unbootable.') 567 568 if config('canTouchEfiVariables'): 569 if config('efiRemovable'): 570 print('note: boot.loader.limine.efiInstallAsRemovable is true, no need to add EFI entry.') 571 else: 572 efibootmgr = os.path.join(str(config('efiBootMgrPath')), 'bin', 'efibootmgr') 573 efi_partition = find_mounted_device(str(config('efiMountPoint'))) 574 efi_disk = find_disk_device(efi_partition) 575 576 efibootmgr_output = subprocess.check_output([efibootmgr], stderr=subprocess.STDOUT, universal_newlines=True) 577 578 # Check the output of `efibootmgr` to find if limine is already installed and present in the boot record 579 limine_boot_entry = None 580 if matches := re.findall(r'Boot([0-9a-fA-F]{4})\*? Limine', efibootmgr_output): 581 limine_boot_entry = matches[0] 582 583 # If there's already a Limine entry, replace it 584 if limine_boot_entry: 585 boot_order = re.findall(r'BootOrder: ((?:[0-9a-fA-F]{4},?)*)', efibootmgr_output)[0] 586 587 efibootmgr_output = subprocess.check_output([ 588 efibootmgr, 589 '-b', limine_boot_entry, 590 '-B', 591 ], stderr=subprocess.STDOUT, universal_newlines=True) 592 593 efibootmgr_output = subprocess.check_output([ 594 efibootmgr, 595 '-c', 596 '-b', limine_boot_entry, 597 '-d', efi_disk, 598 '-p', efi_partition.removeprefix(efi_disk).removeprefix('p'), 599 '-l', f'\\efi\\limine\\{boot_file}', 600 '-L', 'Limine', 601 '-o', boot_order, 602 ], stderr=subprocess.STDOUT, universal_newlines=True) 603 else: 604 efibootmgr_output = subprocess.check_output([ 605 efibootmgr, 606 '-c', 607 '-d', efi_disk, 608 '-p', efi_partition.removeprefix(efi_disk).removeprefix('p'), 609 '-l', f'\\efi\\limine\\{boot_file}', 610 '-L', 'Limine', 611 ], stderr=subprocess.STDOUT, universal_newlines=True) 612 613 if config('biosSupport'): 614 if cpu_family != 'x86': 615 raise Exception(f'Unsupported CPU family for BIOS install: {cpu_family}') 616 617 limine_sys = os.path.join(str(config('liminePath')), 'share', 'limine', 'limine-bios.sys') 618 limine_sys_dest = os.path.join(limine_install_dir, 'limine-bios.sys') 619 620 copy_file(limine_sys, limine_sys_dest) 621 622 device = str(config('biosDevice')) 623 624 if device == 'nodev': 625 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) 626 return 627 628 limine_deploy_args: List[str] = [limine_binary, 'bios-install', device] 629 630 if config('partitionIndex'): 631 limine_deploy_args.append(str(config('partitionIndex'))) 632 633 if config('forceMbr'): 634 limine_deploy_args.append('--force-mbr') 635 636 try: 637 subprocess.run(limine_deploy_args) 638 except: 639 raise Exception( 640 'Failed to deploy BIOS stage 1 Limine bootloader!\n' + 641 'You might want to try enabling the `boot.loader.limine.forceMbr` option.') 642 643 print("removing unused boot files...") 644 for path in paths: 645 if not paths[path] and os.path.exists(path): 646 os.remove(path) 647 648def main() -> None: 649 try: 650 install_bootloader() 651 finally: 652 # Since fat32 provides little recovery facilities after a crash, 653 # it can leave the system in an unbootable state, when a crash/outage 654 # happens shortly after an update. To decrease the likelihood of this 655 # event sync the efi filesystem after each update. 656 rc = libc.syncfs(os.open(f"{str(config('efiMountPoint'))}", os.O_RDONLY)) 657 if rc != 0: 658 print(f"could not sync {str(config('efiMountPoint'))}: {os.strerror(rc)}", file=sys.stderr) 659 660if __name__ == '__main__': 661 main()