nixos/systemd-boot: add windows option for easy dual-booting

When installing NixOS on a machine with Windows, the "easiest" solution
to dual-boot is re-using the existing EFI System Partition (ESP), which
allows systemd-boot to detect Windows automatically.

However, if there are multiple ESPs, maybe even on multiple disks,
systemd-boot is unable to detect the other OSes, and you either have to
use Grub and os-prober, or do a tedious manual configuration as
described in the wiki:
https://wiki.nixos.org/w/index.php?title=Dual_Booting_NixOS_and_Windows&redirect=no#EFI_with_multiple_disks

This commit automates and documents this properly so only a single line
like

    boot.loader.systemd-boot.windows."10".efiDeviceHandle = "HD0c2";

is required.

In the future, we might want to try automatically detecting this
during installation, but finding the correct device handle while the
kernel is running is tricky.
This commit is contained in:
Felix Uhl 2024-09-25 01:07:46 +02:00
parent f2e5b04c4e
commit 73011ba96f
3 changed files with 169 additions and 25 deletions

View File

@ -522,6 +522,8 @@
The derivation now installs "impl" headers selectively instead of by a wildcard. The derivation now installs "impl" headers selectively instead of by a wildcard.
Use `imgui.src` if you just want to access the unpacked sources. Use `imgui.src` if you just want to access the unpacked sources.
- The new `boot.loader.systemd-boot.windows` option makes setting up dual-booting with Windows on a different drive easier
- Linux 4.19 has been removed because it will reach its end of life within the lifespan of 24.11 - Linux 4.19 has been removed because it will reach its end of life within the lifespan of 24.11
- Unprivileged access to the kernel syslog via `dmesg` is now restricted by default. Users wanting to keep an - Unprivileged access to the kernel syslog via `dmesg` is now restricted by default. Users wanting to keep an

View File

@ -182,7 +182,7 @@ in
sortKey = mkOption { sortKey = mkOption {
default = "nixos"; default = "nixos";
type = lib.types.str; type = types.str;
description = '' description = ''
The sort key used for the NixOS bootloader entries. The sort key used for the NixOS bootloader entries.
This key determines sorting relative to non-NixOS entries. This key determines sorting relative to non-NixOS entries.
@ -438,6 +438,87 @@ in
Windows can unseal the encryption key. Windows can unseal the encryption key.
''; '';
}; };
windows = mkOption {
default = { };
description = ''
Make Windows bootable from systemd-boot. This option is not necessary when Windows and
NixOS use the same EFI System Partition (ESP). In that case, Windows will automatically be
detected by systemd-boot.
However, if Windows is installed on a separate drive or ESP, you can use this option to add
a menu entry for each installation manually.
The attribute name is used for the title of the menu entry and internal file names.
'';
example = literalExpression ''
{
"10".efiDeviceHandle = "HD0c3";
"11-ame" = {
title = "Windows 11 Ameliorated Edition";
efiDeviceHandle = "HD0b1";
};
"11-home" = {
title = "Windows 11 Home";
efiDeviceHandle = "FS1";
sortKey = "z_windows";
};
}
'';
type = types.attrsOf (
types.submodule (
{ name, ... }:
{
options = {
efiDeviceHandle = mkOption {
type = types.str;
example = "HD1b3";
description = ''
The device handle of the EFI System Partition (ESP) where the Windows bootloader is
located. This is the device handle that the EDK2 UEFI Shell uses to load the
bootloader.
To find this handle, follow these steps:
1. Set {option}`boot.loader.systemd-boot.edk2-uefi-shell.enable` to `true`
2. Run `nixos-rebuild boot`
3. Reboot and select "EDK2 UEFI Shell" from the systemd-boot menu
4. Run `map -c` to list all consistent device handles
5. For each device handle (for example, `HD0c1`), run `ls HD0c1:\EFI`
6. If the output contains the directory `Microsoft`, you might have found the correct device handle
7. Run `HD0c1:\EFI\Microsoft\Boot\Bootmgfw.efi` to check if Windows boots correctly
8. If it does, this device handle is the one you need (in this example, `HD0c1`)
This option is required, there is no useful default.
'';
};
title = mkOption {
type = types.str;
example = "Michaelsoft Binbows";
default = "Windows ${name}";
defaultText = ''attribute name of this entry, prefixed with "Windows "'';
description = ''
The title of the boot menu entry.
'';
};
sortKey = mkOption {
type = types.str;
default = "o_windows_${name}";
defaultText = ''attribute name of this entry, prefixed with "o_windows_"'';
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`..
'';
};
};
}
)
);
};
}; };
config = mkIf cfg.enable { config = mkIf cfg.enable {
@ -490,7 +571,13 @@ in
assertion = !(hasInfix "nixos/.extra-files" (toLower filename)); 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"; 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); ]) (builtins.attrNames cfg.extraFiles)
++ concatMap (winVersion: [
{
assertion = lib.match "^[-_0-9A-Za-z]+$" winVersion != null;
message = "boot.loader.systemd-boot.windows.${winVersion} is invalid: key must only contain alphanumeric characters, hyphens, and underscores";
}
]) (builtins.attrNames cfg.windows);
boot.loader.grub.enable = mkDefault false; boot.loader.grub.enable = mkDefault false;
@ -503,34 +590,44 @@ in
(mkIf cfg.netbootxyz.enable { (mkIf cfg.netbootxyz.enable {
"efi/netbootxyz/netboot.xyz.efi" = "${pkgs.netbootxyz-efi}"; "efi/netbootxyz/netboot.xyz.efi" = "${pkgs.netbootxyz-efi}";
}) })
(mkIf cfg.edk2-uefi-shell.enable { (mkIf (cfg.edk2-uefi-shell.enable || cfg.windows != { }) {
${edk2ShellEspPath} = "${pkgs.edk2-uefi-shell}/shell.efi"; ${edk2ShellEspPath} = "${pkgs.edk2-uefi-shell}/shell.efi";
}) })
]; ];
boot.loader.systemd-boot.extraEntries = mkMerge [ boot.loader.systemd-boot.extraEntries = mkMerge (
(mkIf cfg.memtest86.enable { [
"memtest86.conf" = '' (mkIf cfg.memtest86.enable {
title Memtest86+ "memtest86.conf" = ''
efi /efi/memtest86/memtest.efi title Memtest86+
sort-key ${cfg.memtest86.sortKey} 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}
'';
})
(mkIf cfg.edk2-uefi-shell.enable {
"edk2-uefi-shell.conf" = ''
title EDK2 UEFI Shell
efi /${edk2ShellEspPath}
sort-key ${cfg.edk2-uefi-shell.sortKey}
'';
})
]
++ (mapAttrsToList (winVersion: cfg: {
"windows_${winVersion}.conf" = ''
title ${cfg.title}
efi /${edk2ShellEspPath}
options -nointerrupt -nomap -noversion ${cfg.efiDeviceHandle}:EFI\Microsoft\Boot\Bootmgfw.efi
sort-key ${cfg.sortKey}
''; '';
}) }) cfg.windows)
(mkIf cfg.netbootxyz.enable { );
"netbootxyz.conf" = ''
title netboot.xyz
efi /efi/netbootxyz/netboot.xyz.efi
sort-key ${cfg.netbootxyz.sortKey}
'';
})
(mkIf cfg.edk2-uefi-shell.enable {
"edk2-uefi-shell.conf" = ''
title EDK2 UEFI Shell
efi /${edk2ShellEspPath}
sort-key ${cfg.edk2-uefi-shell.sortKey}
'';
})
];
boot.bootspec.extensions."org.nixos.systemd-boot" = { boot.bootspec.extensions."org.nixos.systemd-boot" = {
inherit (config.boot.loader.systemd-boot) sortKey; inherit (config.boot.loader.systemd-boot) sortKey;

View File

@ -354,6 +354,51 @@ in
''; '';
}; };
windows = makeTest {
name = "systemd-boot-windows";
meta.maintainers = with pkgs.lib.maintainers; [ iFreilicht ];
nodes.machine = { ... }: {
imports = [ common ];
boot.loader.systemd-boot.windows = {
"7" = {
efiDeviceHandle = "HD0c1";
sortKey = "before_all_others";
};
"Ten".efiDeviceHandle = "FS0";
"11" = {
title = "Title with-_-punctuation ...?!";
efiDeviceHandle = "HD0d4";
sortKey = "zzz";
};
};
};
testScript = ''
machine.succeed("test -e /boot/efi/edk2-uefi-shell/shell.efi")
machine.succeed("test -e /boot/loader/entries/windows_7.conf")
machine.succeed("test -e /boot/loader/entries/windows_Ten.conf")
machine.succeed("test -e /boot/loader/entries/windows_11.conf")
machine.succeed("grep 'efi /efi/edk2-uefi-shell/shell.efi' /boot/loader/entries/windows_7.conf")
machine.succeed("grep 'efi /efi/edk2-uefi-shell/shell.efi' /boot/loader/entries/windows_Ten.conf")
machine.succeed("grep 'efi /efi/edk2-uefi-shell/shell.efi' /boot/loader/entries/windows_11.conf")
machine.succeed("grep 'HD0c1:EFI\\\\Microsoft\\\\Boot\\\\Bootmgfw.efi' /boot/loader/entries/windows_7.conf")
machine.succeed("grep 'FS0:EFI\\\\Microsoft\\\\Boot\\\\Bootmgfw.efi' /boot/loader/entries/windows_Ten.conf")
machine.succeed("grep 'HD0d4:EFI\\\\Microsoft\\\\Boot\\\\Bootmgfw.efi' /boot/loader/entries/windows_11.conf")
machine.succeed("grep 'sort-key before_all_others' /boot/loader/entries/windows_7.conf")
machine.succeed("grep 'sort-key o_windows_Ten' /boot/loader/entries/windows_Ten.conf")
machine.succeed("grep 'sort-key zzz' /boot/loader/entries/windows_11.conf")
machine.succeed("grep 'title Windows 7' /boot/loader/entries/windows_7.conf")
machine.succeed("grep 'title Windows Ten' /boot/loader/entries/windows_Ten.conf")
machine.succeed('grep "title Title with-_-punctuation ...?!" /boot/loader/entries/windows_11.conf')
'';
};
memtestSortKey = makeTest { memtestSortKey = makeTest {
name = "systemd-boot-memtest-sortkey"; name = "systemd-boot-memtest-sortkey";
meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ]; meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];