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()