nixos/limine: Add support for booting Xen

This commit is contained in:
Rane
2025-08-30 14:43:43 +10:00
parent c0dcc49d24
commit 647c04f2e9
@@ -16,6 +16,15 @@ import sys
import tempfile
import textwrap
@dataclass
class XenBootSpec:
"""Represent the bootspec extension for Xen dom0 kernels"""
efiPath: str
multibootPath: str
params: List[str]
version: str
@dataclass
class BootSpec:
system: str
@@ -25,6 +34,7 @@ class BootSpec:
label: str
toplevel: str
specialisations: Dict[str, "BootSpec"]
xen: XenBootSpec | None
initrd: str | None = None
initrdSecrets: str | None = None
@@ -129,11 +139,136 @@ def get_kernel_uri(kernel_path: str) -> str:
def bootjson_to_bootspec(bootjson: dict) -> BootSpec:
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']
return BootSpec(
**bootjson['org.nixos.bootspec.v1'],
specialisations=specialisations,
xen=xen,
)
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
Xen keys.
Arguments:
bootspec -- the NixOS BootSpec requiring Xen EFI configuration
gen -- The system generation requiring Xen EFI configuration
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_cfg_dir = os.path.dirname(xen_efi_path)
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'
)
# 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'
# set kernel and copy in-place
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'
)
# set ramdisk and copy initrd in-place
if bootspec.initrd:
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'
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:
"""Generate EFI and BIOS entries for Xen dom0 kernels.
Arguments:
levels -- The number of Limine menu levels for entries
bootspec -- The NixOS BootSpec used for generating this Limine configuration
xenVersion -- The version of Xen the entry is generated for, from the boot extension
gen -- The system generation these entries are generated for
time -- The build time for the configuration
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'
# 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'])
):
# Use the EFI protocol and generate Xen EFI configuration
# files and directories which are loaded by Xen's EFI binary
# directly.
# Ideally both EFI and BIOS booting would use multiboot2,
# however Limine's multiboot2 module has trouble finding
# 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'
)
elif (
'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 += (
'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:
# TODO: Understand why the first argument is ignored below?
# --- to work around first argument being ignored
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'
# 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'
)
if bootspec.initrd:
# the final module is the initrd
entry += 'module_path: ' + get_kernel_uri(bootspec.initrd) + '\n'
return entry
def config_entry(levels: int, bootspec: BootSpec, label: str, time: str) -> str:
entry = '/' * levels + label + '\n'
@@ -175,6 +310,13 @@ def generate_config_entry(profile: str, gen: str, special: bool) -> str:
depth = 2
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'):
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)
@@ -189,6 +331,7 @@ def generate_config_entry(profile: str, gen: str, special: bool) -> str:
for spec, spec_boot_spec in specialisation_list:
entry += config_entry(depth, spec_boot_spec, f'{spec}', str(time))
return entry
@@ -273,6 +416,13 @@ 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')
if os.path.exists(limine_xen_dir):
print(f'cleaning {limine_xen_dir}')
shutil.rmtree(limine_xen_dir)
os.makedirs(limine_xen_dir)
profiles = [('system', get_gens())]
for profile in get_profiles():
@@ -482,7 +632,7 @@ def install_bootloader() -> None:
print("removing unused boot files...")
for path in paths:
if not paths[path]:
if not paths[path] and os.path.exists(path):
os.remove(path)
def main() -> None: