From 73011ba96fe6dc0574c38b594a72657a5a7e2f39 Mon Sep 17 00:00:00 2001 From: Felix Uhl Date: Wed, 25 Sep 2024 01:07:46 +0200 Subject: [PATCH] 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. --- .../manual/release-notes/rl-2411.section.md | 2 + .../boot/loader/systemd-boot/systemd-boot.nix | 147 +++++++++++++++--- nixos/tests/systemd-boot.nix | 45 ++++++ 3 files changed, 169 insertions(+), 25 deletions(-) diff --git a/nixos/doc/manual/release-notes/rl-2411.section.md b/nixos/doc/manual/release-notes/rl-2411.section.md index c9be54b109c11..116007b51ea1d 100644 --- a/nixos/doc/manual/release-notes/rl-2411.section.md +++ b/nixos/doc/manual/release-notes/rl-2411.section.md @@ -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 diff --git a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix index f791bc9d76913..6490dc99d66f8 100644 --- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix +++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix @@ -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. @@ -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 { @@ -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; @@ -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; diff --git a/nixos/tests/systemd-boot.nix b/nixos/tests/systemd-boot.nix index d5cd6ae0117ff..812d6088ed4e2 100644 --- a/nixos/tests/systemd-boot.nix +++ b/nixos/tests/systemd-boot.nix @@ -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 ];