{...}: { flake.nixosModules.bootloader = { pkgs, lib, ... }: { boot = { loader = { timeout = 2; 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 = { enable = true; theme = "catppuccin-mocha"; themePackages = with pkgs; [ (catppuccin-plymouth.override { variant = "mocha"; }) ]; }; kernelPackages = pkgs.linuxPackages_latest; }; }; }