Skip to content

Commit

Permalink
nixos/systemd-boot: add windows option for easy dual-booting
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
iFreilicht committed Oct 11, 2024
1 parent f2e5b04 commit 73011ba
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 25 deletions.
2 changes: 2 additions & 0 deletions nixos/doc/manual/release-notes/rl-2411.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,8 @@
The derivation now installs "impl" headers selectively instead of by a wildcard.
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

- Unprivileged access to the kernel syslog via `dmesg` is now restricted by default. Users wanting to keep an
Expand Down
147 changes: 122 additions & 25 deletions nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ in

sortKey = mkOption {
default = "nixos";
type = lib.types.str;
type = types.str;
description = ''
The sort key used for the NixOS bootloader entries.
This key determines sorting relative to non-NixOS entries.
Expand Down Expand Up @@ -438,6 +438,87 @@ in
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 {
Expand Down Expand Up @@ -490,7 +571,13 @@ in
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);
]) (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;

Expand All @@ -503,34 +590,44 @@ in
(mkIf cfg.netbootxyz.enable {
"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";
})
];

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}
'';
})
(mkIf cfg.edk2-uefi-shell.enable {
"edk2-uefi-shell.conf" = ''
title EDK2 UEFI Shell
efi /${edk2ShellEspPath}
sort-key ${cfg.edk2-uefi-shell.sortKey}
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}
'';
})
(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)
);

boot.bootspec.extensions."org.nixos.systemd-boot" = {
inherit (config.boot.loader.systemd-boot) sortKey;
Expand Down
45 changes: 45 additions & 0 deletions nixos/tests/systemd-boot.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
name = "systemd-boot-memtest-sortkey";
meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];
Expand Down

0 comments on commit 73011ba

Please sign in to comment.