nixos/systemd-boot: Simpler windows dual booting (#344327)

This commit is contained in:
Atemu 2024-10-11 20:25:08 +02:00 committed by GitHub
commit 12ef18d2e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 593 additions and 272 deletions

View File

@ -631,6 +631,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

View File

@ -1,4 +1,9 @@
{ config, lib, pkgs, ... }:
{
config,
lib,
pkgs,
...
}:
with lib;
@ -10,9 +15,12 @@ let
# 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" {
checkedSource =
pkgs.runCommand "systemd-boot"
{
preferLocalBuild = true;
} ''
}
''
install -m755 -D ${./systemd-boot-builder.py} $out
${lib.getExe pkgs.buildPackages.mypy} \
--no-implicit-optional \
@ -21,6 +29,8 @@ let
$out
'';
edk2ShellEspPath = "efi/edk2-uefi-shell/shell.efi";
systemdBootBuilder = pkgs.substituteAll rec {
name = "systemd-boot";
@ -44,13 +54,17 @@ let
configurationLimit = if cfg.configurationLimit == null then 0 else cfg.configurationLimit;
inherit (cfg) consoleMode graceful editor rebootForBitlocker;
inherit (cfg)
consoleMode
graceful
editor
rebootForBitlocker
;
inherit (efi) efiSysMountPoint canTouchEfiVariables;
bootMountPoint = if cfg.xbootldrMountPoint != null
then cfg.xbootldrMountPoint
else efi.efiSysMountPoint;
bootMountPoint =
if cfg.xbootldrMountPoint != null then cfg.xbootldrMountPoint else efi.efiSysMountPoint;
nixosDir = "/EFI/nixos";
@ -60,29 +74,35 @@ let
netbootxyz = optionalString cfg.netbootxyz.enable pkgs.netbootxyz-efi;
edk2-uefi-shell = optionalString cfg.edk2-uefi-shell.enable pkgs.edk2-uefi-shell;
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}"}
${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: ''
${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)}
'') cfg.extraFiles
)}
${concatStrings (mapAttrsToList (n: v: ''
${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)}
'') cfg.extraEntries
)}
'';
};
@ -91,20 +111,58 @@ let
${systemdBootBuilder}/bin/systemd-boot "$@"
${cfg.extraInstallCommands}
'';
in {
in
{
meta.maintainers = with lib.maintainers; [ julienmalka ];
imports =
[ (mkRenamedOptionModule [ "boot" "loader" "gummiboot" "enable" ] [ "boot" "loader" "systemd-boot" "enable" ])
imports = [
(mkRenamedOptionModule
[
"boot"
"loader"
"gummiboot"
"enable"
]
[
"boot"
"loader"
"systemd-boot"
"enable"
]
)
(lib.mkChangedOptionModule
[ "boot" "loader" "systemd-boot" "memtest86" "entryFilename" ]
[ "boot" "loader" "systemd-boot" "memtest86" "sortKey" ]
[
"boot"
"loader"
"systemd-boot"
"memtest86"
"entryFilename"
]
[
"boot"
"loader"
"systemd-boot"
"memtest86"
"sortKey"
]
(config: lib.strings.removeSuffix ".conf" config.boot.loader.systemd-boot.memtest86.entryFilename)
)
(lib.mkChangedOptionModule
[ "boot" "loader" "systemd-boot" "netbootxyz" "entryFilename" ]
[ "boot" "loader" "systemd-boot" "netbootxyz" "sortKey" ]
[
"boot"
"loader"
"systemd-boot"
"netbootxyz"
"entryFilename"
]
[
"boot"
"loader"
"systemd-boot"
"netbootxyz"
"sortKey"
]
(config: lib.strings.removeSuffix ".conf" config.boot.loader.systemd-boot.netbootxyz.entryFilename)
)
];
@ -124,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.
@ -218,7 +276,15 @@ in {
consoleMode = mkOption {
default = "keep";
type = types.enum [ "0" "1" "2" "5" "auto" "max" "keep" ];
type = types.enum [
"0"
"1"
"2"
"5"
"auto"
"max"
"keep"
];
description = ''
The resolution of the console. The following values are valid:
@ -281,6 +347,29 @@ in {
};
};
edk2-uefi-shell = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Make the EDK2 UEFI Shell available from the systemd-boot menu.
It can be used to manually boot other operating systems or for debugging.
'';
};
sortKey = mkOption {
type = types.str;
default = "o_edk2-uefi-shell";
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 = { };
@ -349,10 +438,92 @@ 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 {
assertions = [
assertions =
[
{
assertion = (hasPrefix "/" efi.efiSysMountPoint);
message = "The ESP mount point '${toString efi.efiSysMountPoint}' must be an absolute path";
@ -370,10 +541,14 @@ in {
message = "This kernel does not support the EFI boot stub";
}
{
assertion = cfg.installDeviceTree -> config.hardware.deviceTree.enable -> config.hardware.deviceTree.name != null;
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: [
]
++ concatMap (filename: [
{
assertion = !(hasInfix "/" filename);
message = "boot.loader.systemd-boot.extraEntries.${lib.strings.escapeNixIdentifier filename} is invalid: entries within folders are not supported";
@ -396,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;
@ -409,9 +590,13 @@ in {
(mkIf cfg.netbootxyz.enable {
"efi/netbootxyz/netboot.xyz.efi" = "${pkgs.netbootxyz-efi}";
})
(mkIf (cfg.edk2-uefi-shell.enable || cfg.windows != { }) {
${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" = ''
title Memtest86+
@ -426,7 +611,23 @@ in {
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;

View File

@ -1,6 +1,7 @@
{ system ? builtins.currentSystem,
{
system ? builtins.currentSystem,
config ? { },
pkgs ? import ../.. { inherit system config; }
pkgs ? import ../.. { inherit system config; },
}:
with import ../lib/testing-python.nix { inherit system pkgs; };
@ -16,7 +17,13 @@ let
system.switch.enable = true;
};
commonXbootldr = { config, lib, pkgs, ... }:
commonXbootldr =
{
config,
lib,
pkgs,
...
}:
let
diskImage = import ../lib/make-disk-image.nix {
inherit config lib pkgs;
@ -85,7 +92,10 @@ in
{
basic = makeTest {
name = "systemd-boot";
meta.maintainers = with pkgs.lib.maintainers; [ danielfullmer julienmalka ];
meta.maintainers = with pkgs.lib.maintainers; [
danielfullmer
julienmalka
];
nodes.machine = common;
@ -117,9 +127,12 @@ in
virtualisation.useSecureBoot = true;
};
testScript = let
testScript =
let
efiArch = pkgs.stdenv.hostPlatform.efiArch;
in { nodes, ... }: ''
in
{ nodes, ... }:
''
machine.start(allow_reboot=True)
machine.wait_for_unit("multi-user.target")
@ -141,7 +154,9 @@ in
nodes.machine = commonXbootldr;
testScript = { nodes, ... }: ''
testScript =
{ nodes, ... }:
''
${customDiskImage nodes}
machine.start()
@ -164,9 +179,14 @@ in
# Check that specialisations create corresponding boot entries.
specialisation = makeTest {
name = "systemd-boot-specialisation";
meta.maintainers = with pkgs.lib.maintainers; [ lukegb julienmalka ];
meta.maintainers = with pkgs.lib.maintainers; [
lukegb
julienmalka
];
nodes.machine = { pkgs, lib, ... }: {
nodes.machine =
{ pkgs, lib, ... }:
{
imports = [ common ];
specialisation.something.configuration = {
boot.loader.systemd-boot.sortKey = "something";
@ -179,14 +199,18 @@ in
# the correct contents.
boot.loader.systemd-boot.installDeviceTree = pkgs.stdenv.hostPlatform.isAarch64;
hardware.deviceTree.name = "dummy.dtb";
hardware.deviceTree.package = lib.mkForce (pkgs.runCommand "dummy-devicetree-package" { } ''
hardware.deviceTree.package = lib.mkForce (
pkgs.runCommand "dummy-devicetree-package" { } ''
mkdir -p $out
cp ${pkgs.emptyFile} $out/dummy.dtb
'');
''
);
};
};
testScript = { nodes, ... }: ''
testScript =
{ nodes, ... }:
''
machine.start()
machine.wait_for_unit("multi-user.target")
@ -199,7 +223,8 @@ in
machine.succeed(
"grep 'sort-key something' /boot/loader/entries/nixos-generation-1-specialisation-something.conf"
)
'' + pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isAarch64 ''
''
+ pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isAarch64 ''
machine.succeed(
r"grep 'devicetree /EFI/nixos/[a-z0-9]\{32\}.*dummy' /boot/loader/entries/nixos-generation-1-specialisation-something.conf"
)
@ -209,9 +234,14 @@ in
# Boot without having created an EFI entry--instead using default "/EFI/BOOT/BOOTX64.EFI"
fallback = makeTest {
name = "systemd-boot-fallback";
meta.maintainers = with pkgs.lib.maintainers; [ danielfullmer julienmalka ];
meta.maintainers = with pkgs.lib.maintainers; [
danielfullmer
julienmalka
];
nodes.machine = { pkgs, lib, ... }: {
nodes.machine =
{ pkgs, lib, ... }:
{
imports = [ common ];
boot.loader.efi.canTouchEfiVariables = mkForce false;
};
@ -235,7 +265,10 @@ in
update = makeTest {
name = "systemd-boot-update";
meta.maintainers = with pkgs.lib.maintainers; [ danielfullmer julienmalka ];
meta.maintainers = with pkgs.lib.maintainers; [
danielfullmer
julienmalka
];
nodes.machine = common;
@ -270,11 +303,15 @@ in
'';
};
memtest86 = with pkgs.lib; optionalAttrs (meta.availableOn { inherit system; } pkgs.memtest86plus) (makeTest {
memtest86 =
with pkgs.lib;
optionalAttrs (meta.availableOn { inherit system; } pkgs.memtest86plus) (makeTest {
name = "systemd-boot-memtest86";
meta.maintainers = with maintainers; [ julienmalka ];
nodes.machine = { pkgs, lib, ... }: {
nodes.machine =
{ pkgs, lib, ... }:
{
imports = [ common ];
boot.loader.systemd-boot.memtest86.enable = true;
};
@ -289,7 +326,9 @@ in
name = "systemd-boot-netbootxyz";
meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];
nodes.machine = { pkgs, lib, ... }: {
nodes.machine =
{ pkgs, lib, ... }:
{
imports = [ common ];
boot.loader.systemd-boot.netbootxyz.enable = true;
};
@ -300,11 +339,73 @@ in
'';
};
edk2-uefi-shell = makeTest {
name = "systemd-boot-edk2-uefi-shell";
meta.maintainers = with pkgs.lib.maintainers; [ iFreilicht ];
nodes.machine = { ... }: {
imports = [ common ];
boot.loader.systemd-boot.edk2-uefi-shell.enable = true;
};
testScript = ''
machine.succeed("test -e /boot/loader/entries/edk2-uefi-shell.conf")
machine.succeed("test -e /boot/efi/edk2-uefi-shell/shell.efi")
'';
};
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 ];
nodes.machine = { pkgs, lib, ... }: {
nodes.machine =
{ pkgs, lib, ... }:
{
imports = [ common ];
boot.loader.systemd-boot.memtest86.enable = true;
boot.loader.systemd-boot.memtest86.sortKey = "apple";
@ -321,12 +422,16 @@ in
name = "systemd-boot-entry-filename-xbootldr";
meta.maintainers = with pkgs.lib.maintainers; [ sdht0 ];
nodes.machine = { pkgs, lib, ... }: {
nodes.machine =
{ pkgs, lib, ... }:
{
imports = [ commonXbootldr ];
boot.loader.systemd-boot.memtest86.enable = true;
};
testScript = { nodes, ... }: ''
testScript =
{ nodes, ... }:
''
${customDiskImage nodes}
machine.start()
@ -342,7 +447,9 @@ in
name = "systemd-boot-extra-entries";
meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];
nodes.machine = { pkgs, lib, ... }: {
nodes.machine =
{ pkgs, lib, ... }:
{
imports = [ common ];
boot.loader.systemd-boot.extraEntries = {
"banana.conf" = ''
@ -361,7 +468,9 @@ in
name = "systemd-boot-extra-files";
meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];
nodes.machine = { pkgs, lib, ... }: {
nodes.machine =
{ pkgs, lib, ... }:
{
imports = [ common ];
boot.loader.systemd-boot.extraFiles = {
"efi/fruits/tomato.efi" = pkgs.netbootxyz-efi;
@ -381,7 +490,9 @@ in
nodes = {
inherit common;
machine = { pkgs, nodes, ... }: {
machine =
{ pkgs, nodes, ... }:
{
imports = [ common ];
boot.loader.systemd-boot.extraFiles = {
"efi/fruits/tomato.efi" = pkgs.netbootxyz-efi;
@ -394,17 +505,22 @@ in
];
};
with_netbootxyz = { pkgs, ... }: {
with_netbootxyz =
{ pkgs, ... }:
{
imports = [ common ];
boot.loader.systemd-boot.netbootxyz.enable = true;
};
};
testScript = { nodes, ... }: let
testScript =
{ nodes, ... }:
let
originalSystem = nodes.machine.system.build.toplevel;
baseSystem = nodes.common.system.build.toplevel;
finalSystem = nodes.with_netbootxyz.system.build.toplevel;
in ''
in
''
machine.succeed("test -e /boot/efi/fruits/tomato.efi")
machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi")
@ -438,7 +554,9 @@ in
nodes = {
inherit common;
machine = { pkgs, nodes, ... }: {
machine =
{ pkgs, nodes, ... }:
{
imports = [ common ];
# These are configs for different nodes, but we'll use them here in `machine`
@ -448,7 +566,8 @@ in
};
};
testScript = { nodes, ... }:
testScript =
{ nodes, ... }:
let
baseSystem = nodes.common.system.build.toplevel;
in
@ -461,8 +580,7 @@ in
'';
};
no-bootspec = makeTest
{
no-bootspec = makeTest {
name = "systemd-boot-no-bootspec";
meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];