From 12779dc0915268d28a62777708770409e0556cae Mon Sep 17 00:00:00 2001 From: programmerlexi Date: Sun, 2 Mar 2025 13:17:25 +0100 Subject: [PATCH] nixos/limine: init module Co-Authored-By: Gabriel Waksmundzki --- ci/OWNERS | 3 + .../manual/release-notes/rl-2505.section.md | 2 + nixos/modules/module-list.nix | 1 + .../boot/loader/limine/limine-install.py | 409 ++++++++++++++++++ .../system/boot/loader/limine/limine.nix | 371 ++++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/limine/checksum.nix | 32 ++ nixos/tests/limine/default.nix | 8 + nixos/tests/limine/uefi.nix | 31 ++ 9 files changed, 858 insertions(+) create mode 100644 nixos/modules/system/boot/loader/limine/limine-install.py create mode 100644 nixos/modules/system/boot/loader/limine/limine.nix create mode 100644 nixos/tests/limine/checksum.nix create mode 100644 nixos/tests/limine/default.nix create mode 100644 nixos/tests/limine/uefi.nix diff --git a/ci/OWNERS b/ci/OWNERS index 49d64693fe5a..042589e3253a 100644 --- a/ci/OWNERS +++ b/ci/OWNERS @@ -129,6 +129,9 @@ nixos/modules/installer/tools/nix-fallback-paths.nix @NixOS/nix-team @raitobeza # Systemd-boot /nixos/modules/system/boot/loader/systemd-boot @JulienMalka +# Limine +/nixos/modules/system/boot/loader/limine @lzcunt + # Images and installer media /nixos/modules/profiles/installation-device.nix @ElvishJerricco /nixos/modules/installer/cd-dvd/ @ElvishJerricco diff --git a/nixos/doc/manual/release-notes/rl-2505.section.md b/nixos/doc/manual/release-notes/rl-2505.section.md index e3c61c882cb4..380f58d121f6 100644 --- a/nixos/doc/manual/release-notes/rl-2505.section.md +++ b/nixos/doc/manual/release-notes/rl-2505.section.md @@ -177,6 +177,8 @@ - [Rebuilderd](https://github.com/kpcyrd/rebuilderd) an independent verification of binary packages - Reproducible Builds. Available as [services.rebuilderd](#opt-services.rebuilderd.enable). +- [Limine](https://github.com/limine-bootloader/limine) a modern, advanced, portable, multiprotocol bootloader and boot manager. Available as [boot.loader.limine](#opt-boot.loader.limine.enable) + ## Backward Incompatibilities {#sec-release-25.05-incompatibilities} diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index efa81f833fb5..18c47ea38245 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1716,6 +1716,7 @@ ./system/boot/loader/grub/memtest.nix ./system/boot/loader/external/external.nix ./system/boot/loader/init-script/init-script.nix + ./system/boot/loader/limine/limine.nix ./system/boot/loader/loader.nix ./system/boot/loader/systemd-boot/systemd-boot.nix ./system/boot/luksroot.nix diff --git a/nixos/modules/system/boot/loader/limine/limine-install.py b/nixos/modules/system/boot/loader/limine/limine-install.py new file mode 100644 index 000000000000..96a5b999d5fc --- /dev/null +++ b/nixos/modules/system/boot/loader/limine/limine-install.py @@ -0,0 +1,409 @@ +#!@python3@/bin/python3 -B + +from dataclasses import dataclass +from typing import Any, Callable, Dict, List, Optional, Tuple + +import datetime +import hashlib +import json +import os +import psutil +import re +import shutil +import subprocess +import sys +import tempfile +import textwrap + + +limine_dir = None +can_use_direct_paths = False +install_config = json.load(open('@configPath@', 'r')) + + +def config(*path: List[str]) -> Optional[Any]: + result = install_config + for component in path: + result = result[component] + return result + + +def get_system_path(profile: str = 'system', gen: Optional[str] = None, spec: Optional[str] = None) -> str: + if profile == 'system': + result = os.path.join('/nix', 'var', 'nix', 'profiles', 'system') + else: + result = os.path.join('/nix', 'var', 'nix', 'profiles', 'system-profiles', profile + f'-{gen}-link' if gen is not None else '') + + if spec is not None: + result = os.path.join(result, 'specialisation', spec) + + return result + + +def get_profiles() -> List[str]: + 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')] + + +def get_gens(profile: str = 'system') -> List[Tuple[int, List[str]]]: + nix_env = os.path.join(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'):] + + +def is_encrypted(device: str) -> bool: + for name, _ in config('luksDevices').items(): + 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') + + +def get_copied_path_uri(path: str, target: str) -> str: + result = '' + + package_id = os.path.basename(os.path.dirname(path)) + suffix = os.path.basename(path) + dest_file = f'{package_id}-{suffix}' + dest_path = os.path.join(limine_dir, target, dest_file) + + if not os.path.exists(dest_path): + copy_file(path, dest_path) + + 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: + b2sum = hashlib.blake2b() + b2sum.update(file.read()) + + result += f'#{b2sum.hexdigest()}' + + return result + + +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: + 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) + + +def get_kernel_uri(kernel_path: str) -> str: + return get_copied_path_uri(kernel_path, "kernels") + + +@dataclass +class BootSpec: + system: str + init: str + kernel: str + kernelParams: List[str] + label: str + toplevel: str + specialisations: Dict[str, "BootSpec"] + initrd: str | None = None + initrdSecrets: str | None = None + + +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()} + return BootSpec( + **bootjson['org.nixos.bootspec.v1'], + specialisations=specialisations, + ) + + +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' + if bootspec.initrd: + entry += f'module_path: ' + get_kernel_uri(bootspec.initrd) + '\n' + + if bootspec.initrdSecrets: + initrd_secrets_path = limine_dir + '/kernels/' + os.path.basename(toplevel) + '-secrets' + os.makedirs(initrd_secrets_path) + + old_umask = os.umask() + os.umask(0o137) + initrd_secrets_path_temp = tempfile.mktemp(os.path.basename(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') + + 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' + + os.umask(old_umask) + return entry + + +def generate_config_entry(profile: str, gen: str) -> 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')) + boot_spec = bootjson_to_bootspec(boot_json) + + entry = config_entry(2, boot_spec, f'Generation {gen}', time) + for spec in boot_spec.specialisations: + entry += config_entry(2, boot_spec, f'Generation {gen}, Specialisation {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)) + disk = os.path.dirname(disk) + + return os.path.join('/dev', os.path.basename(disk)) + + +def find_mounted_device(path: str) -> str: + path = os.path.abspath(path) + + while not os.path.ismount(path): + path = os.path.dirname(path) + + devices = [x for x in psutil.disk_partitions(all=True) if x.mountpoint == path] + + assert len(devices) == 1 + return devices[0].device + + +def copy_file(from_path: str, to_path: str): + dirname = os.path.dirname(to_path) + + if not os.path.exists(dirname): + os.makedirs(dirname) + + shutil.copyfile(from_path, to_path) + +def option_from_config(name: str, config_path: List[str], conversion: Callable[[str], str] | None = None) -> str: + if config(*config_path): + return f'{name}: {conversion(config(*config_path)) if conversion else config(*config_path)}\n' + return '' + + +def main(): + global limine_dir + + boot_fs = None + + for mount_point, fs in config('fileSystems').items(): + if mount_point == '/boot': + boot_fs = fs + + if config('efiSupport'): + limine_dir = os.path.join(config('efiMountPoint'), 'limine') + elif boot_fs and is_fs_type_supported(boot_fs['fsType']) and not is_encrypted(boot_fs['device']): + limine_dir = '/boot/limine' + else: + possible_causes = [] + if not boot_fs: + 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=})') + + causes_str = textwrap.indent('\n'.join(possible_causes), ' - ') + + raise Exception(textwrap.dedent(''' + Could not find a valid place for Limine configuration files!' + Possible candidates that were ruled out: + ''') + causes_str + textwrap.dedent(''' + Limine cannot be installed on a system without an unencrypted + partition formatted as FAT. + ''')) + + if not os.path.exists(limine_dir): + os.makedirs(limine_dir) + + if os.path.exists(os.path.join(limine_dir, 'kernels')): + print(f'nuking {os.path.join(limine_dir, "kernels")}') + shutil.rmtree(os.path.join(limine_dir, 'kernels')) + + os.makedirs(os.path.join(limine_dir, "kernels")) + + profiles = [('system', get_gens())] + + for profile in get_profiles(): + profiles += (profile, get_gens(profile)) + + timeout = config('timeout') + editor_enabled = 'yes' if config('enableEditor') else 'no' + hash_mismatch_panic = 'yes' if config('panicOnChecksumMismatch') else 'no' + + config_file = config('extraConfig') + '\n' + config_file += textwrap.dedent(f''' + timeout: {timeout} + editor_enabled: {editor_enabled} + hash_mismatch_panic: {hash_mismatch_panic} + graphics: yes + default_entry: 2 + ''') + + if os.path.exists(os.path.join(limine_dir, 'wallpapers')): + print(f'nuking {os.path.join(limine_dir, "wallpapers")}') + shutil.rmtree(os.path.join(limine_dir, 'wallpapers')) + + if len(config('style', 'wallpapers')) > 0: + os.makedirs(os.path.join(limine_dir, 'wallpapers')) + + 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('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_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(''' + # NixOS boot entries start here + ''') + + for (profile, gens) in profiles: + group_name = 'default profile' if profile == 'system' else f"profile '{profile}'" + config_file += f'/+NixOS {group_name}\n' + + for gen in sorted(gens, key=lambda x: x, reverse=True): + config_file += generate_config_entry(profile, gen) + + config_file_path = os.path.join(limine_dir, 'limine.conf') + config_file += '\n# NixOS boot entries end here\n\n' + + config_file += config('extraEntries') + + with open(config_file_path, 'w') as file: + file.truncate() + file.write(config_file.strip()) + + for dest_path, source_path in config('additionalFiles').items(): + dest_path = os.path.join(limine_dir, dest_path) + + copy_file(source_path, dest_path) + + limine_binary = os.path.join(config('liminePath'), 'bin', 'limine') + cpu_family = config('hostArchitecture', 'family') + if config('efiSupport'): + 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")}') + else: + raise Exception(f'Unsupported CPU family: {cpu_family}') + + efi_path = os.path.join(config('liminePath'), 'share', 'limine', boot_file) + dest_path = os.path.join(config('efiMountPoint'), 'efi', 'boot' if config('efiRemovable') else 'limine', boot_file) + + copy_file(efi_path, dest_path) + + if config('enrollConfig'): + b2sum = hashlib.blake2b() + b2sum.update(config_file.strip().encode()) + try: + subprocess.run([limine_binary, 'enroll-config', dest_path, b2sum.hexdigest()]) + except: + print('error: failed to enroll limine config.', 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 config('canTouchEfiVariables'): + if config('efiRemovable'): + print('note: boot.loader.limine.efiInstallAsRemovable is true, no need to add EFI entry.') + else: + efibootmgr = os.path.join(config('efiBootMgrPath'), 'bin', 'efibootmgr') + efi_partition = find_mounted_device(config('efiMountPoint')) + efi_disk = find_disk_device(efi_partition) + 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) + + for line in efibootmgr_output.split('\n'): + if matches := re.findall(r'Boot([0-9a-fA-F]{4}) has same label Limine', line): + subprocess.run( + [efibootmgr, '-b', matches[0], '-B'], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + if config('biosSupport'): + if cpu_family != 'x86': + raise Exception(f'Unsupported CPU family for BIOS install: {cpu_family}') + + limine_sys = os.path.join(config('liminePath'), 'share', 'limine', 'limine-bios.sys') + limine_sys_dest = os.path.join(limine_dir, 'limine-bios.sys') + + copy_file(limine_sys, limine_sys_dest) + + device = 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) + return + else: + limine_deploy_args = [limine_binary, 'bios-install', device] + + if config('partitionIndex'): + limine_deploy_args.append(config('partitionIndex')) + + if config('forceMbr'): + limine_deploy_args += '--force-mbr' + 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.forceMbr` option.') + +main() diff --git a/nixos/modules/system/boot/loader/limine/limine.nix b/nixos/modules/system/boot/loader/limine/limine.nix new file mode 100644 index 000000000000..182868bd6973 --- /dev/null +++ b/nixos/modules/system/boot/loader/limine/limine.nix @@ -0,0 +1,371 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.boot.loader.limine; + efi = config.boot.loader.efi; + limineInstallConfig = pkgs.writeText "limine-install.json" ( + builtins.toJSON { + nixPath = config.nix.package; + efiBootMgrPath = pkgs.efibootmgr; + liminePath = cfg.package; + efiMountPoint = efi.efiSysMountPoint; + fileSystems = config.fileSystems; + luksDevices = config.boot.initrd.luks.devices; + canTouchEfiVariables = efi.canTouchEfiVariables; + efiSupport = cfg.efiSupport; + efiRemovable = cfg.efiInstallAsRemovable; + biosSupport = cfg.biosSupport; + biosDevice = cfg.biosDevice; + partitionIndex = cfg.partitionIndex; + forceMbr = cfg.forceMbr; + enrollConfig = cfg.enrollConfig; + style = cfg.style; + maxGenerations = if cfg.maxGenerations == null then 0 else cfg.maxGenerations; + hostArchitecture = pkgs.stdenv.hostPlatform.parsed.cpu; + timeout = if config.boot.loader.timeout != null then config.boot.loader.timeout else 10; + enableEditor = cfg.enableEditor; + extraConfig = cfg.extraConfig; + extraEntries = cfg.extraEntries; + additionalFiles = cfg.additionalFiles; + validateChecksums = cfg.validateChecksums; + panicOnChecksumMismatch = cfg.panicOnChecksumMismatch; + } + ); + defaultWallpaper = pkgs.nixos-artwork.wallpapers.simple-dark-gray-bootloader.gnomeFilePath; +in +{ + meta.maintainers = with lib.maintainers; [ + lzcunt + phip1611 + programmerlexi + ]; + + options.boot.loader.limine = { + enable = lib.mkEnableOption "the Limine Bootloader"; + + enableEditor = lib.mkEnableOption null // { + description = '' + Whether to allow editing the boot entries before booting them. + It is recommended to set this to false, as it allows gaining root + access by passing `init=/bin/sh` as a kernel parameter. + ''; + }; + + maxGenerations = lib.mkOption { + default = null; + example = 50; + type = lib.types.nullOr lib.types.int; + description = '' + Maximum number of latest generations in the boot menu. + Useful to prevent boot partition of running out of disk space. + `null` means no limit i.e. all generations that were not + garbage collected yet. + ''; + }; + + extraConfig = lib.mkOption { + default = ""; + type = lib.types.lines; + example = lib.literalExpression '' + serial: yes + ''; + description = '' + A string which is prepended to limine.conf. The config format can be found [here](https://github.com/limine-bootloader/limine/blob/trunk/CONFIG.md). + ''; + }; + + extraEntries = lib.mkOption { + default = ""; + type = lib.types.lines; + example = lib.literalExpression '' + /memtest86 + protocol: chainload + path: boot():///efi/memtest86/memtest86.efi + ''; + description = '' + A string which is appended to the end of limine.conf. The config format can be found [here](https://github.com/limine-bootloader/limine/blob/trunk/CONFIG.md). + ''; + }; + + additionalFiles = lib.mkOption { + default = { }; + type = lib.types.attrsOf lib.types.path; + example = lib.literalExpression '' + { "efi/memtest86/memtest86.efi" = "''${pkgs.memtest86-efi}/BOOTX64.efi"; } + ''; + description = '' + A set of files to be copied to {file}`/boot`. Each attribute name denotes the + destination file name in {file}`/boot`, while the corresponding attribute value + specifies the source file. + ''; + }; + + validateChecksums = lib.mkEnableOption null // { + default = true; + description = '' + Whether to validate file checksums before booting. + ''; + }; + + panicOnChecksumMismatch = lib.mkEnableOption null // { + description = '' + Whether or not checksum validation failure should be a fatal + error at boot time. + ''; + }; + + package = lib.mkPackageOption pkgs "limine" { }; + + efiSupport = lib.mkEnableOption null // { + default = pkgs.stdenv.hostPlatform.isEfi; + defaultText = lib.literalExpression "pkgs.stdenv.hostPlatform.isEfi"; + description = '' + Whether or not to install the limine EFI files. + ''; + }; + + efiInstallAsRemovable = lib.mkEnableOption null // { + default = !efi.canTouchEfiVariables; + defaultText = lib.literalExpression "!config.boot.loader.efi.canTouchEfiVariables"; + description = '' + Whether or not to install the limine EFI files as removable. + + See {option}`boot.loader.grub.efiInstallAsRemovable` + ''; + }; + + biosSupport = lib.mkEnableOption null // { + default = !cfg.efiSupport && pkgs.stdenv.hostPlatform.isx86; + defaultText = lib.literalExpression "!config.boot.loader.limine.efiSupport && pkgs.stdenv.hostPlatform.isx86"; + description = '' + Whether or not to install limine for BIOS. + ''; + }; + + biosDevice = lib.mkOption { + default = "nodev"; + type = lib.types.str; + description = '' + Device to install the BIOS version of limine on. + ''; + }; + + partitionIndex = lib.mkOption { + default = null; + type = lib.types.nullOr lib.types.int; + description = '' + The 1-based index of the dedicated partition for limine's second stage. + ''; + }; + + enrollConfig = lib.mkEnableOption null // { + default = cfg.panicOnChecksumMismatch; + defaultText = lib.literalExpression "boot.loader.limine.panicOnChecksumMismatch"; + description = '' + Whether or not to enroll the config. + Only works on EFI! + ''; + }; + + forceMbr = lib.mkEnableOption null // { + description = '' + Force MBR detection to work even if the safety checks fail, use absolutely only if necessary! + ''; + }; + + style = { + wallpapers = lib.mkOption { + default = [ ]; + example = lib.literalExpression "[ pkgs.nixos-artwork.wallpapers.simple-dark-gray-bootloader.gnomeFilePath ]"; + type = lib.types.listOf lib.types.path; + description = '' + A list of wallpapers. + If more than one is specified, a random one will be selected at boot. + ''; + }; + + wallpaperStyle = lib.mkOption { + default = "streched"; + type = lib.types.enum [ + "centered" + "streched" + "tiled" + ]; + description = '' + How the wallpaper should be fit to the screen. + ''; + }; + + backdrop = lib.mkOption { + default = null; + example = "7EBAE4"; + type = lib.types.nullOr lib.types.str; + description = '' + Color to fill the rest of the screen with when wallpaper_style is centered in RRGGBB format. + ''; + }; + + interface = { + resolution = lib.mkOption { + default = null; + type = lib.types.nullOr lib.types.str; + description = '' + The resolution of the interface. + ''; + }; + + branding = lib.mkOption { + default = null; + type = lib.types.nullOr lib.types.str; + description = '' + The title at the top of the screen. + ''; + }; + + brandingColor = lib.mkOption { + default = null; + type = lib.types.nullOr lib.types.int; + description = '' + Color index of the title at the top of the screen in the range of 0-7 (Limine defaults to 6 (cyan)). + ''; + }; + + helpHidden = lib.mkEnableOption null // { + description = '' + Whether or not to hide the keybinds at the top of the screen. + ''; + }; + }; + graphicalTerminal = { + font = { + scale = lib.mkOption { + default = null; + example = lib.literalExpression "2x2"; + type = lib.types.nullOr lib.types.str; + description = '' + The scale of the font in the format x. + ''; + }; + + spacing = lib.mkOption { + default = null; + type = lib.types.nullOr lib.types.int; + description = '' + The horizontal spacing between characters in pixels. + ''; + }; + }; + + palette = lib.mkOption { + default = null; + type = lib.types.nullOr lib.types.str; + description = '' + A ; seperated array of 8 colors in the format RRGGBB: + black, red, green, brown, blue, magenta, cyan, and gray. + ''; + }; + + brightPalette = lib.mkOption { + default = null; + type = lib.types.nullOr lib.types.str; + description = '' + A ; seperated array of 8 colors in the format RRGGBB: + dark gray, bright red, bright green, yellow, bright blue, bright magenta, bright cyan, and white. + ''; + }; + + foreground = lib.mkOption { + default = null; + type = lib.types.nullOr lib.types.str; + description = '' + Text foreground color (RRGGBB). + ''; + }; + + background = lib.mkOption { + default = null; + type = lib.types.nullOr lib.types.str; + description = '' + Text background color (TTRRGGBB). TT is transparency. + ''; + }; + + brightForeground = lib.mkOption { + default = null; + type = lib.types.nullOr lib.types.str; + description = '' + Text foreground bright color (RRGGBB). + ''; + }; + + brightBackground = lib.mkOption { + default = null; + type = lib.types.nullOr lib.types.str; + description = '' + Text background bright color (RRGGBB). + ''; + }; + + margin = lib.mkOption { + default = null; + type = lib.types.nullOr lib.types.int; + description = '' + The amount of margin around the terminal. + ''; + }; + + marginGradient = lib.mkOption { + default = null; + type = lib.types.nullOr lib.types.int; + description = '' + The thickness in pixels for the margin around the terminal. + ''; + }; + }; + }; + }; + + config = lib.mkMerge [ + { + boot.loader.limine.style.wallpapers = lib.mkDefault [ defaultWallpaper ]; + } + (lib.mkIf (cfg.style.wallpapers == [ defaultWallpaper ]) { + boot.loader.limine.style.backdrop = lib.mkDefault "2F302F"; + boot.loader.limine.style.wallpaperStyle = lib.mkDefault "streched"; + }) + (lib.mkIf cfg.enable { + assertions = [ + { + assertion = + pkgs.stdenv.hostPlatform.isx86_64 + || pkgs.stdenv.hostPlatform.isi686 + || pkgs.stdenv.hostPlatform.isAarch64; + message = "Limine can only be installed on aarch64 & x86 platforms"; + } + { + assertion = cfg.efiSupport || cfg.biosSupport; + message = "Both UEFI support and BIOS support for Limine are disabled, this will result in an unbootable system"; + } + ]; + + boot.loader.grub.enable = lib.mkDefault false; + + boot.loader.supportsInitrdSecrets = true; + + system = { + boot.loader.id = "limine"; + build.installBootLoader = pkgs.substituteAll { + src = ./limine-install.py; + isExecutable = true; + + python3 = pkgs.python3.withPackages (python-packages: [ python-packages.psutil ]); + configPath = limineInstallConfig; + }; + }; + }) + ]; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 809440bfbf97..85eaa986a7aa 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -598,6 +598,7 @@ in { lightdm = handleTest ./lightdm.nix {}; lighttpd = handleTest ./lighttpd.nix {}; limesurvey = handleTest ./limesurvey.nix {}; + limine = import ./limine { inherit runTest; }; listmonk = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./listmonk.nix {}; litestream = handleTest ./litestream.nix {}; lldap = handleTest ./lldap.nix {}; diff --git a/nixos/tests/limine/checksum.nix b/nixos/tests/limine/checksum.nix new file mode 100644 index 000000000000..78352bf5da83 --- /dev/null +++ b/nixos/tests/limine/checksum.nix @@ -0,0 +1,32 @@ +{ lib, ... }: +{ + name = "checksum"; + meta.maintainers = with lib.maintainers; [ + lzcunt + phip1611 + programmerlexi + ]; + meta.platforms = [ + "aarch64-linux" + "i686-linux" + "x86_64-linux" + ]; + nodes.machine = + { ... }: + { + virtualisation.useBootLoader = true; + virtualisation.useEFIBoot = true; + + boot.loader.efi.canTouchEfiVariables = true; + boot.loader.limine.enable = true; + boot.loader.limine.efiSupport = true; + boot.loader.limine.panicOnChecksumMismatch = true; + boot.loader.timeout = 0; + }; + + testScript = '' + machine.start() + with subtest('Machine boots correctly'): + machine.wait_for_unit('multi-user.target') + ''; +} diff --git a/nixos/tests/limine/default.nix b/nixos/tests/limine/default.nix new file mode 100644 index 000000000000..7458b9641633 --- /dev/null +++ b/nixos/tests/limine/default.nix @@ -0,0 +1,8 @@ +{ + runTest, + ... +}: +{ + checksum = runTest ./checksum.nix; + uefi = runTest ./uefi.nix; +} diff --git a/nixos/tests/limine/uefi.nix b/nixos/tests/limine/uefi.nix new file mode 100644 index 000000000000..12f2f695a865 --- /dev/null +++ b/nixos/tests/limine/uefi.nix @@ -0,0 +1,31 @@ +{ lib, ... }: +{ + name = "uefi"; + meta.maintainers = with lib.maintainers; [ + lzcunt + phip1611 + programmerlexi + ]; + meta.platforms = [ + "aarch64-linux" + "i686-linux" + "x86_64-linux" + ]; + nodes.machine = + { ... }: + { + virtualisation.useBootLoader = true; + virtualisation.useEFIBoot = true; + + boot.loader.efi.canTouchEfiVariables = true; + boot.loader.limine.enable = true; + boot.loader.limine.efiSupport = true; + boot.loader.timeout = 0; + }; + + testScript = '' + machine.start() + with subtest('Machine boots correctly'): + machine.wait_for_unit('multi-user.target') + ''; +}