From fc1772e5dbf544e06ed72d0cb678f955ae3b3772 Mon Sep 17 00:00:00 2001 From: ahmza Date: Thu, 6 Aug 2026 17:08:15 +0100 Subject: [PATCH] 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. --- modules/system/core/boot.nix | 57 ++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/modules/system/core/boot.nix b/modules/system/core/boot.nix index 8b008f1..ef0cfcc 100644 --- a/modules/system/core/boot.nix +++ b/modules/system/core/boot.nix @@ -10,10 +10,67 @@ efi = { 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//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 = { + enable = true; efiSupport = true; device = "nodev"; 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 = {