pin the bootloader explicitly and bound the grub menu to 10 generations

grub.enable defaults to true, so adding a boot.loader block silently
switched this host from the systemd-boot it was installed with over to
grub. nixos removes neither the old bootloader's files nor its nvram
entry, so both lived on the esp and the firmware kept booting a frozen
systemd-boot menu. pinning both options means a conflicting definition
elsewhere now fails loudly instead of quietly winning. note the pins
delete nothing - stale EFI/systemd/, loader/ and the leftover nvram entry
still have to be cleared by hand. done on shadow, not verified on t480.

configurationLimit bounds esp usage for real, not just the menu:
install-grub.pl unlinks any $boot/kernels/* that no kept generation
references. sizing measured on shadow rather than assumed - 10 kept
generations referenced 1 bzImage and 3 initrds for 114mib total, about
11mib per generation, because generations share aggressively. the
worst case is higher, around 47mib each, since nixpkgs embeds
/etc/modprobe.d/nixos.conf inside the initrd and so every kernel module
parameter edit mints a fresh one. 10 therefore costs ~115mib typically
and ~470mib worst case against a 1gib esp that also carries the grub
modules, the catppuccin theme, and windows' own boot files.

keeping it conservative matters because overflow fails as `copy ... or
die` in install-grub.pl, which runs *after* the system profile has
already been switched - the result is an activated but unbootable
generation. t480 reaches this module too and its esp may be smaller.
This commit is contained in:
2026-08-06 17:08:15 +01:00
parent 42c805f430
commit fc1772e5db
+57
View File
@@ -10,10 +10,67 @@
efi = { efi = {
canTouchEfiVariables = true; canTouchEfiVariables = true;
}; };
# Pinned explicitly. grub.enable defaults to true, so this host silently
# switched from systemd-boot (used at install time) to GRUB when this
# block was added. NixOS never removes the previous bootloader's files
# or its NVRAM entry, so both lived on the ESP and the firmware kept
# booting the frozen systemd-boot menu. Keep this explicit, and never
# set systemd-boot.enable alongside it.
#
# Note what this does and does not do. Both of these lines are no-ops
# against nixpkgs' defaults (grub.enable is already true,
# systemd-boot.enable already false); their value is that a conflicting
# definition elsewhere now fails loudly instead of silently winning. They
# delete nothing — stale EFI/systemd/, loader/ and the leftover NVRAM
# entry have to be removed by hand on any host installed with
# systemd-boot. Done on shadow; NOT yet verified on t480.
#
# This is stock UEFI GRUB: NixOS installs EFI/<distro><esp>/grubx64.efi
# (here EFI/NixOS-boot/grubx64.efi) and registers a matching NVRAM entry
# via efibootmgr, because canTouchEfiVariables is true. That NVRAM entry
# is what makes GRUB the default; NixOS does not manage the removable
# fallback path EFI/BOOT/BOOTX64.EFI and we deliberately do not ask it to
# (grub.efiInstallAsRemovable, which would need canTouchEfiVariables=false,
# is only for firmware that ignores NVRAM entries — neither host needs it).
#
# On shadow that fallback path is NOT empty, though: bcdboot left a copy of
# the *Windows* boot manager at EFI/BOOT/bootx64.efi (byte-identical to
# EFI/Microsoft/Boot/bootmgfw.efi). Normal boots are unaffected because the
# firmware follows its NVRAM entry to GRUB. But it does mean that if NVRAM
# is ever cleared — CMOS reset, battery, firmware update — the fallback
# lands on Windows rather than GRUB. Left as-is by choice; recover by
# rerunning `nixos-rebuild boot` to reinstate the NVRAM entry.
systemd-boot.enable = false;
grub = { grub = {
enable = true;
efiSupport = true; efiSupport = true;
device = "nodev"; device = "nodev";
theme = pkgs.catppuccin-grub; theme = pkgs.catppuccin-grub;
# Bound the menu so the ESP cannot fill up with old kernels.
# install-grub.pl unlinks any $boot/kernels/* not referenced by a kept
# generation, so this genuinely caps ESP usage rather than just
# trimming the menu.
#
# Sizing, measured on shadow rather than assumed. Generations share
# aggressively in practice: 10 kept generations referenced 1 bzImage
# (13.3MiB) and 3 initrds (~33.4MiB each) for 114MiB total, i.e. ~11MiB
# per generation, not ~47MiB. Only a kernel or initrd change mints a new
# file — and the initrd does change more often than you'd expect, since
# nixpkgs embeds /etc/modprobe.d/nixos.conf inside it, so every kernel
# module parameter edit produces a fresh one.
#
# So the real bound is between ~11MiB and ~47MiB per generation
# depending on churn: 10 generations costs ~115MiB typically and ~470MiB
# worst case, against a 1GiB ESP that also carries grub/x86_64-efi, the
# catppuccin theme, and Windows' own boot files (bootmgfw.efi + BCD,
# ~30-60MiB) since Windows was bcdboot'd onto this same ESP. 10 keeps
# the worst case under half the partition.
#
# Keep this conservative: overflow fails as `copy ... or die` in
# install-grub.pl, which runs *after* the system profile has already
# been switched, so the result is an activated but unbootable
# generation. t480 reaches this module too and its ESP may be smaller.
configurationLimit = 10;
}; };
}; };
plymouth = { plymouth = {