Implement custom systemd-boot module that generates user friendly bootloader entries
This commit is contained in:
429
modules/systemd-boot/default.nix
Normal file
429
modules/systemd-boot/default.nix
Normal file
@@ -0,0 +1,429 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.boot.loader.systemd-boot;
|
||||
|
||||
efi = config.boot.loader.efi;
|
||||
|
||||
# We check the source code in a derivation that does not depend on the
|
||||
# system configuration so that most users don't have to redo the check and require
|
||||
# the necessary dependencies.
|
||||
checkedSource = pkgs.runCommand "systemd-boot" {
|
||||
preferLocalBuild = true;
|
||||
} ''
|
||||
install -m755 -D ${./systemd-boot-builder.py} $out
|
||||
${lib.getExe pkgs.buildPackages.mypy} \
|
||||
--no-implicit-optional \
|
||||
--disallow-untyped-calls \
|
||||
--disallow-untyped-defs \
|
||||
$out
|
||||
'';
|
||||
|
||||
systemdBootBuilder = pkgs.substituteAll rec {
|
||||
name = "systemd-boot";
|
||||
|
||||
src = checkedSource;
|
||||
|
||||
isExecutable = true;
|
||||
|
||||
inherit (pkgs) python3;
|
||||
|
||||
systemd = config.systemd.package;
|
||||
|
||||
bootspecTools = pkgs.bootspec;
|
||||
|
||||
nix = config.nix.package.out;
|
||||
|
||||
timeout = if config.boot.loader.timeout == null then "menu-force" else config.boot.loader.timeout;
|
||||
|
||||
configurationLimit = if cfg.configurationLimit == null then 0 else cfg.configurationLimit;
|
||||
|
||||
inherit (cfg) consoleMode graceful editor rebootForBitlocker;
|
||||
|
||||
inherit (efi) efiSysMountPoint canTouchEfiVariables;
|
||||
|
||||
bootMountPoint = if cfg.xbootldrMountPoint != null
|
||||
then cfg.xbootldrMountPoint
|
||||
else efi.efiSysMountPoint;
|
||||
|
||||
nixosDir = "/EFI/nixos";
|
||||
|
||||
inherit (config.system.nixos) distroName;
|
||||
|
||||
memtest86 = optionalString cfg.memtest86.enable pkgs.memtest86plus;
|
||||
|
||||
netbootxyz = optionalString cfg.netbootxyz.enable pkgs.netbootxyz-efi;
|
||||
|
||||
checkMountpoints = pkgs.writeShellScript "check-mountpoints" ''
|
||||
fail() {
|
||||
echo "$1 = '$2' is not a mounted partition. Is the path configured correctly?" >&2
|
||||
exit 1
|
||||
}
|
||||
${pkgs.util-linuxMinimal}/bin/findmnt ${efiSysMountPoint} > /dev/null || fail efiSysMountPoint ${efiSysMountPoint}
|
||||
${lib.optionalString
|
||||
(cfg.xbootldrMountPoint != null)
|
||||
"${pkgs.util-linuxMinimal}/bin/findmnt ${cfg.xbootldrMountPoint} > /dev/null || fail xbootldrMountPoint ${cfg.xbootldrMountPoint}"}
|
||||
'';
|
||||
|
||||
copyExtraFiles = pkgs.writeShellScript "copy-extra-files" ''
|
||||
empty_file=$(${pkgs.coreutils}/bin/mktemp)
|
||||
|
||||
${concatStrings (mapAttrsToList (n: v: ''
|
||||
${pkgs.coreutils}/bin/install -Dp "${v}" "${bootMountPoint}/"${escapeShellArg n}
|
||||
${pkgs.coreutils}/bin/install -D $empty_file "${bootMountPoint}/${nixosDir}/.extra-files/"${escapeShellArg n}
|
||||
'') cfg.extraFiles)}
|
||||
|
||||
${concatStrings (mapAttrsToList (n: v: ''
|
||||
${pkgs.coreutils}/bin/install -Dp "${pkgs.writeText n v}" "${bootMountPoint}/loader/entries/"${escapeShellArg n}
|
||||
${pkgs.coreutils}/bin/install -D $empty_file "${bootMountPoint}/${nixosDir}/.extra-files/loader/entries/"${escapeShellArg n}
|
||||
'') cfg.extraEntries)}
|
||||
'';
|
||||
};
|
||||
|
||||
finalSystemdBootBuilder = pkgs.writeScript "install-systemd-boot.sh" ''
|
||||
#!${pkgs.runtimeShell}
|
||||
${systemdBootBuilder} "$@"
|
||||
${cfg.extraInstallCommands}
|
||||
'';
|
||||
in {
|
||||
# NOTE(m): This module overrides the default NixOS systemd-boot module to include
|
||||
# a custom systemd-boot-builder.py script that generates more user friendly bootloader entries
|
||||
disabledModules = [ "system/boot/loader/systemd-boot/systemd-boot.nix" ];
|
||||
meta.maintainers = with lib.maintainers; [ michaelshmitty ];
|
||||
|
||||
options.boot.loader.systemd-boot = {
|
||||
enable = mkOption {
|
||||
default = false;
|
||||
|
||||
type = types.bool;
|
||||
|
||||
description = ''
|
||||
Whether to enable the systemd-boot (formerly gummiboot) EFI boot manager.
|
||||
For more information about systemd-boot:
|
||||
https://www.freedesktop.org/wiki/Software/systemd/systemd-boot/
|
||||
'';
|
||||
};
|
||||
|
||||
sortKey = mkOption {
|
||||
default = "nixos";
|
||||
type = lib.types.str;
|
||||
description = ''
|
||||
The sort key used for the NixOS bootloader entries.
|
||||
This key determines sorting relative to non-NixOS entries.
|
||||
See also https://uapi-group.org/specifications/specs/boot_loader_specification/#sorting
|
||||
|
||||
This option can also be used to control the sorting of NixOS specialisations.
|
||||
|
||||
By default, specialisations inherit the sort key of their parent generation
|
||||
and will have the same value for both the sort-key and the version (i.e. the generation number),
|
||||
systemd-boot will therefore sort them based on their file name, meaning that
|
||||
in your boot menu you will have each main generation directly followed by
|
||||
its specialisations sorted alphabetically by their names.
|
||||
|
||||
If you want a different ordering for a specialisation, you can override
|
||||
its sort-key which will cause the specialisation to be uncoupled from its
|
||||
parent generation. It will then be sorted by its new sort-key just like
|
||||
any other boot entry.
|
||||
|
||||
The sort-key is stored in the generation's bootspec, which means that
|
||||
generations keep their sort-keys even if the original definition of the
|
||||
generation was removed from the NixOS configuration.
|
||||
It also means that updating the sort-key will only affect new generations,
|
||||
while old ones will keep the sort-key that they were originally built with.
|
||||
'';
|
||||
};
|
||||
|
||||
editor = mkOption {
|
||||
default = true;
|
||||
|
||||
type = types.bool;
|
||||
|
||||
description = ''
|
||||
Whether to allow editing the kernel command-line before
|
||||
boot. It is recommended to set this to false, as it allows
|
||||
gaining root access by passing init=/bin/sh as a kernel
|
||||
parameter. However, it is enabled by default for backwards
|
||||
compatibility.
|
||||
'';
|
||||
};
|
||||
|
||||
xbootldrMountPoint = mkOption {
|
||||
default = null;
|
||||
type = types.nullOr types.str;
|
||||
description = ''
|
||||
Where the XBOOTLDR partition is mounted.
|
||||
|
||||
If set, this partition will be used as $BOOT to store boot loader entries and extra files
|
||||
instead of the EFI partition. As per the bootloader specification, it is recommended that
|
||||
the EFI and XBOOTLDR partitions be mounted at `/efi` and `/boot`, respectively.
|
||||
'';
|
||||
};
|
||||
|
||||
configurationLimit = mkOption {
|
||||
default = null;
|
||||
example = 120;
|
||||
type = types.nullOr types.int;
|
||||
description = ''
|
||||
Maximum number of latest generations in the boot menu.
|
||||
Useful to prevent boot partition running out of disk space.
|
||||
|
||||
`null` means no limit i.e. all generations
|
||||
that have not been garbage collected yet.
|
||||
'';
|
||||
};
|
||||
|
||||
installDeviceTree = mkOption {
|
||||
default = with config.hardware.deviceTree; enable && name != null;
|
||||
defaultText = ''with config.hardware.deviceTree; enable && name != null'';
|
||||
description = ''
|
||||
Install the devicetree blob specified by `config.hardware.deviceTree.name`
|
||||
to the ESP and instruct systemd-boot to pass this DTB to linux.
|
||||
'';
|
||||
};
|
||||
|
||||
extraInstallCommands = mkOption {
|
||||
default = "";
|
||||
example = ''
|
||||
default_cfg=$(cat /boot/loader/loader.conf | grep default | awk '{print $2}')
|
||||
init_value=$(cat /boot/loader/entries/$default_cfg | grep init= | awk '{print $2}')
|
||||
sed -i "s|@INIT@|$init_value|g" /boot/custom/config_with_placeholder.conf
|
||||
'';
|
||||
type = types.lines;
|
||||
description = ''
|
||||
Additional shell commands inserted in the bootloader installer
|
||||
script after generating menu entries. It can be used to expand
|
||||
on extra boot entries that cannot incorporate certain pieces of
|
||||
information (such as the resulting `init=` kernel parameter).
|
||||
'';
|
||||
};
|
||||
|
||||
consoleMode = mkOption {
|
||||
default = "keep";
|
||||
|
||||
type = types.enum [ "0" "1" "2" "auto" "max" "keep" ];
|
||||
|
||||
description = ''
|
||||
The resolution of the console. The following values are valid:
|
||||
|
||||
- `"0"`: Standard UEFI 80x25 mode
|
||||
- `"1"`: 80x50 mode, not supported by all devices
|
||||
- `"2"`: The first non-standard mode provided by the device firmware, if any
|
||||
- `"auto"`: Pick a suitable mode automatically using heuristics
|
||||
- `"max"`: Pick the highest-numbered available mode
|
||||
- `"keep"`: Keep the mode selected by firmware (the default)
|
||||
'';
|
||||
};
|
||||
|
||||
memtest86 = {
|
||||
enable = mkOption {
|
||||
default = false;
|
||||
type = types.bool;
|
||||
description = ''
|
||||
Make Memtest86+ available from the systemd-boot menu. Memtest86+ is a
|
||||
program for testing memory.
|
||||
'';
|
||||
};
|
||||
|
||||
sortKey = mkOption {
|
||||
default = "o_memtest86";
|
||||
type = types.str;
|
||||
description = ''
|
||||
`systemd-boot` orders the menu entries by their sort keys,
|
||||
so if you want something to appear after all the NixOS entries,
|
||||
it should start with {file}`o` or onwards.
|
||||
|
||||
See also {option}`boot.loader.systemd-boot.sortKey`.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
netbootxyz = {
|
||||
enable = mkOption {
|
||||
default = false;
|
||||
type = types.bool;
|
||||
description = ''
|
||||
Make `netboot.xyz` available from the
|
||||
`systemd-boot` menu. `netboot.xyz`
|
||||
is a menu system that allows you to boot OS installers and
|
||||
utilities over the network.
|
||||
'';
|
||||
};
|
||||
|
||||
sortKey = mkOption {
|
||||
default = "o_netbootxyz";
|
||||
type = types.str;
|
||||
description = ''
|
||||
`systemd-boot` orders the menu entries by their sort keys,
|
||||
so if you want something to appear after all the NixOS entries,
|
||||
it should start with {file}`o` or onwards.
|
||||
|
||||
See also {option}`boot.loader.systemd-boot.sortKey`.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
extraEntries = mkOption {
|
||||
type = types.attrsOf types.lines;
|
||||
default = {};
|
||||
example = literalExpression ''
|
||||
{ "memtest86.conf" = '''
|
||||
title Memtest86+
|
||||
efi /efi/memtest86/memtest.efi
|
||||
sort-key z_memtest
|
||||
'''; }
|
||||
'';
|
||||
description = ''
|
||||
Any additional entries you want added to the `systemd-boot` menu.
|
||||
These entries will be copied to {file}`$BOOT/loader/entries`.
|
||||
Each attribute name denotes the destination file name,
|
||||
and the corresponding attribute value is the contents of the entry.
|
||||
|
||||
To control the ordering of the entry in the boot menu, use the sort-key
|
||||
field, see
|
||||
https://uapi-group.org/specifications/specs/boot_loader_specification/#sorting
|
||||
and {option}`boot.loader.systemd-boot.sortKey`.
|
||||
'';
|
||||
};
|
||||
|
||||
extraFiles = mkOption {
|
||||
type = types.attrsOf types.path;
|
||||
default = {};
|
||||
example = literalExpression ''
|
||||
{ "efi/memtest86/memtest.efi" = "''${pkgs.memtest86plus}/memtest.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.
|
||||
'';
|
||||
};
|
||||
|
||||
graceful = mkOption {
|
||||
default = false;
|
||||
|
||||
type = types.bool;
|
||||
|
||||
description = ''
|
||||
Invoke `bootctl install` with the `--graceful` option,
|
||||
which ignores errors when EFI variables cannot be written or when the EFI System Partition
|
||||
cannot be found. Currently only applies to random seed operations.
|
||||
|
||||
Only enable this option if `systemd-boot` otherwise fails to install, as the
|
||||
scope or implication of the `--graceful` option may change in the future.
|
||||
'';
|
||||
};
|
||||
|
||||
rebootForBitlocker = mkOption {
|
||||
default = false;
|
||||
|
||||
type = types.bool;
|
||||
|
||||
description = ''
|
||||
Enable *EXPERIMENTAL* BitLocker support.
|
||||
|
||||
Try to detect BitLocker encrypted drives along with an active
|
||||
TPM. If both are found and Windows Boot Manager is selected in
|
||||
the boot menu, set the "BootNext" EFI variable and restart the
|
||||
system. The firmware will then start Windows Boot Manager
|
||||
directly, leaving the TPM PCRs in expected states so that
|
||||
Windows can unseal the encryption key.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion = (hasPrefix "/" efi.efiSysMountPoint);
|
||||
message = "The ESP mount point '${toString efi.efiSysMountPoint}' must be an absolute path";
|
||||
}
|
||||
{
|
||||
assertion = cfg.xbootldrMountPoint == null || (hasPrefix "/" cfg.xbootldrMountPoint);
|
||||
message = "The XBOOTLDR mount point '${toString cfg.xbootldrMountPoint}' must be an absolute path";
|
||||
}
|
||||
{
|
||||
assertion = cfg.xbootldrMountPoint != efi.efiSysMountPoint;
|
||||
message = "The XBOOTLDR mount point '${toString cfg.xbootldrMountPoint}' cannot be the same as the ESP mount point '${toString efi.efiSysMountPoint}'";
|
||||
}
|
||||
{
|
||||
assertion = (config.boot.kernelPackages.kernel.features or { efiBootStub = true; }) ? efiBootStub;
|
||||
message = "This kernel does not support the EFI boot stub";
|
||||
}
|
||||
{
|
||||
assertion = cfg.installDeviceTree -> config.hardware.deviceTree.enable -> config.hardware.deviceTree.name != null;
|
||||
message = "Cannot install devicetree without 'config.hardware.deviceTree.enable' enabled and 'config.hardware.deviceTree.name' set";
|
||||
}
|
||||
] ++ concatMap (filename: [
|
||||
{
|
||||
assertion = !(hasInfix "/" filename);
|
||||
message = "boot.loader.systemd-boot.extraEntries.${lib.strings.escapeNixIdentifier filename} is invalid: entries within folders are not supported";
|
||||
}
|
||||
{
|
||||
assertion = hasSuffix ".conf" filename;
|
||||
message = "boot.loader.systemd-boot.extraEntries.${lib.strings.escapeNixIdentifier filename} is invalid: entries must have a .conf file extension";
|
||||
}
|
||||
]) (builtins.attrNames cfg.extraEntries)
|
||||
++ concatMap (filename: [
|
||||
{
|
||||
assertion = !(hasPrefix "/" filename);
|
||||
message = "boot.loader.systemd-boot.extraFiles.${lib.strings.escapeNixIdentifier filename} is invalid: paths must not begin with a slash";
|
||||
}
|
||||
{
|
||||
assertion = !(hasInfix ".." filename);
|
||||
message = "boot.loader.systemd-boot.extraFiles.${lib.strings.escapeNixIdentifier filename} is invalid: paths must not reference the parent directory";
|
||||
}
|
||||
{
|
||||
assertion = !(hasInfix "nixos/.extra-files" (toLower filename));
|
||||
message = "boot.loader.systemd-boot.extraFiles.${lib.strings.escapeNixIdentifier filename} is invalid: files cannot be placed in the nixos/.extra-files directory";
|
||||
}
|
||||
]) (builtins.attrNames cfg.extraFiles);
|
||||
|
||||
boot.loader.grub.enable = mkDefault false;
|
||||
|
||||
boot.loader.supportsInitrdSecrets = true;
|
||||
|
||||
boot.loader.systemd-boot.extraFiles = mkMerge [
|
||||
(mkIf cfg.memtest86.enable {
|
||||
"efi/memtest86/memtest.efi" = "${pkgs.memtest86plus.efi}";
|
||||
})
|
||||
(mkIf cfg.netbootxyz.enable {
|
||||
"efi/netbootxyz/netboot.xyz.efi" = "${pkgs.netbootxyz-efi}";
|
||||
})
|
||||
];
|
||||
|
||||
boot.loader.systemd-boot.extraEntries = mkMerge [
|
||||
(mkIf cfg.memtest86.enable {
|
||||
"memtest86.conf" = ''
|
||||
title Memtest86+
|
||||
efi /efi/memtest86/memtest.efi
|
||||
sort-key ${cfg.memtest86.sortKey}
|
||||
'';
|
||||
})
|
||||
(mkIf cfg.netbootxyz.enable {
|
||||
"netbootxyz.conf" = ''
|
||||
title netboot.xyz
|
||||
efi /efi/netbootxyz/netboot.xyz.efi
|
||||
sort-key ${cfg.netbootxyz.sortKey}
|
||||
'';
|
||||
})
|
||||
];
|
||||
|
||||
boot.bootspec.extensions."org.nixos.systemd-boot" = {
|
||||
inherit (config.boot.loader.systemd-boot) sortKey;
|
||||
devicetree = lib.mkIf cfg.installDeviceTree "${config.hardware.deviceTree.package}/${config.hardware.deviceTree.name}";
|
||||
};
|
||||
|
||||
system = {
|
||||
build.installBootLoader = finalSystemdBootBuilder;
|
||||
|
||||
boot.loader.id = "systemd-boot";
|
||||
|
||||
requiredKernelConfig = with config.lib.kernelConfig; [
|
||||
(isYes "EFI_STUB")
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user