From 81a001c3d395cfcedf487352c0410eabd92c7ba5 Mon Sep 17 00:00:00 2001 From: Padreug Date: Tue, 6 Oct 2026 19:17:18 +0200 Subject: [PATCH] refactor(deploy): one USB-image helper for both bootloader shapes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit disk-image-sintra-usb was a 60-line inline copy of everything mkUsbDiskImage already does, plus the GRUB/hybrid bits the Aaeon firmware needs — so the two implementations had already drifted: the sintra image never picked up the `nofail` /boot that keeps a slow ESP-USB enumeration out of emergency mode, nor the uas/autosuspend hardening batm3.nix and douro.nix carry. - mkUsbDiskImage takes named args with `partitionTableType` ("efi" for systemd-boot, "hybrid" for GRUB) and `grubBiosDevice`. The ESP relabel is layout-independent: the hybrid table creates the ESP first and bios_grub second, so it stays partition 1 either way. - New `usbGrubHybridModule` + `usbBusHardening` modules. The hardening is scoped to the -usb configs rather than hardware/upboard.nix, which sintra's eMMC install also reads — no cmdline change on a production machine. - `nixosConfigurations.sintra-usb` is now a named config, so a running stick can be updated in place (nix copy + switch-to-configuration) like batm3-usb and douro-usb. - grub.devices is "nodev" in the config and mkForce'd to the build VM's disk only for the image: an in-place switch on a live stick has no /dev/vda, and GRUB's embedded core.img reads grub.cfg off the partition, so the MBR stage needs no per-generation rewrite. Plain definition rather than mkForce, since two mkForce lists merge into [ "/dev/vda" "nodev" ] instead of replacing. douro-usb and batm3-usb evaluate to byte-identical kernelParams, blacklistedKernelModules, fileSystems, bootloader and autoUpgrade config as before. Co-Authored-By: Claude Opus 5 (1M context) --- flake.nix | 180 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 107 insertions(+), 73 deletions(-) diff --git a/flake.nix b/flake.nix index e6ddaaf..0c90ca8 100644 --- a/flake.nix +++ b/flake.nix @@ -391,19 +391,92 @@ system.autoUpgrade.enable = lib.mkForce false; }; + # Bus hardening that makes a USB stick a reliable boot medium: keep the + # flash drive off the flaky UAS driver (many bridges advertise UAS then + # drop off the bus under sustained write load — "device offline error, + # dev sdb"), and stop USB autosuspend cutting power mid-I/O. douro.nix + # and batm3.nix carry this in their hardware files because those machines + # boot from USB exclusively; hardware/upboard.nix is SHARED with sintra's + # eMMC install, so for the UP Board models it is scoped to the -usb + # config here rather than changing a production machine's cmdline. + usbBusHardening = { + boot.blacklistedKernelModules = [ "uas" ]; + boot.kernelParams = [ "usbcore.autosuspend=-1" ]; + }; + + # Aaeon UP Board firmware (tejo, sintra) USB-boots in Legacy/BIOS mode — + # it boots the live ISO via its isolinux (BIOS) El Torito image, not the + # UEFI ESP. systemd-boot is UEFI-only, so a dd'd systemd-boot image is not + # recognised as bootable at all. Switch the UP Board USB configs to GRUB + # with BOTH BIOS (MBR + bios_grub partition, via mkUsbDiskImage's + # partitionTableType = "hybrid") and UEFI (removable + # /EFI/BOOT/BOOTX64.EFI) — mirroring the live ISO's dual boot — so the + # stick boots on Legacy and UEFI alike. Scoped to the USB configs; the + # eMMC installs keep systemd-boot. + # + # devices = [ "nodev" ] here, NOT the image's disk. This config is also + # what in-place updates (`nix copy` + switch-to-configuration) run against + # on a LIVE stick, where the build VM's /dev/vda does not exist and a BIOS + # grub-install against it would fail the switch. "nodev" regenerates + # grub.cfg and skips the MBR write, which is the correct behaviour for an + # update: GRUB's embedded core.img reads grub.cfg off the partition, so + # the MBR stage never needs rewriting per generation. mkUsbDiskImage's + # grubBiosDevice overrides this for the image build, where the BIOS stage + # genuinely has to be written. + usbGrubHybridModule = { lib, ... }: { + boot.loader.systemd-boot.enable = lib.mkForce false; + boot.loader.efi.canTouchEfiVariables = lib.mkForce false; + boot.loader.grub = { + enable = lib.mkForce true; + efiSupport = true; + efiInstallAsRemovable = true; + # Plain definition, NOT mkForce: grubBiosDevice overrides it with + # mkForce, and two mkForce list definitions would merge (both + # priority 50) into [ "/dev/vda" "nodev" ] instead of replacing. + # Nothing else in the module stack defines grub.devices. + devices = [ "nodev" ]; + }; + }; + # dd-able USB image of a -usb config. make-disk-image gives the # ext4 root the nixos-usb label directly (-L) but hardcodes the ESP FAT # label to "ESP", so the volume is relabelled to ESP-USB afterwards — # volume label only; bootloader files are untouched and UEFI loads - # /EFI/BOOT/BOOTX64.EFI regardless. Keeps systemd-boot: both the batm3 - # and douro firmware UEFI-USB-boot fine via that removable fallback. - mkUsbDiskImage = machineModel: usbConfig: + # /EFI/BOOT/BOOTX64.EFI regardless. + # + # partitionTableType: "efi" (GPT + ESP, systemd-boot) for batm3 and douro, + # whose firmware UEFI-USB-boots fine via that removable fallback; + # "hybrid" (GPT + bios_grub + ESP) for the UP Board models, paired with + # usbGrubHybridModule. In BOTH layouts the ESP is partition 1 — the hybrid + # table creates the ESP first and the bios_grub partition second — so the + # parted/mlabel relabel below is layout-independent. + # + # grubBiosDevice: the build VM's disk, for hybrid images only. GRUB must + # write its BIOS stage to that disk's MBR at image-build time, while the + # config itself says "nodev" so in-place updates on a live stick work; + # see usbGrubHybridModule. + mkUsbDiskImage = + { machineModel + , usbConfig + , partitionTableType ? "efi" + , grubBiosDevice ? null + }: let + imageConfig = + if grubBiosDevice == null then + usbConfig + else + usbConfig.extendModules { + modules = [ + ({ lib, ... }: { + boot.loader.grub.devices = lib.mkForce [ grubBiosDevice ]; + }) + ]; + }; baseImage = import (nixpkgs + "/nixos/lib/make-disk-image.nix") { - inherit pkgs lib; - config = usbConfig.config; + inherit pkgs lib partitionTableType; + config = imageConfig.config; format = "raw"; - partitionTableType = "efi"; diskSize = "auto"; label = "nixos-usb"; # ext4 root label (make-disk-image -L) }; @@ -469,6 +542,14 @@ douro-usb = self.nixosConfigurations.douro-installed.extendModules { modules = [ usbBootModule ]; }; + # UP Board models get the same run-from-USB shape plus two things the + # Bay Trail / OptiPlex boxes don't need: GRUB on a hybrid table, because + # the Aaeon firmware USB-boots in Legacy/BIOS mode (usbGrubHybridModule), + # and the uas/autosuspend hardening from here instead of + # hardware/upboard.nix, which sintra's eMMC install also reads. + sintra-usb = self.nixosConfigurations.sintra-installed.extendModules { + modules = [ usbBootModule usbBusHardening usbGrubHybridModule ]; + }; }; # ── Standalone NixOS module ─────────────────────────────────── @@ -536,71 +617,6 @@ diskSize = "auto"; }; - # USB-bootable Sintra image with DISTINCT partition labels - # (nixos-usb / ESP-USB) so the stick can be booted on a Sintra whose - # eMMC already holds a nixos/ESP-labelled install without a by-label - # collision — stage-1 would otherwise race between the two roots and - # likely mount the eMMC. Auto-upgrade is disabled: this is a portable - # test / hand-off image, not a managed fleet member, and disabling it - # also removes the scheduled bootloader writes that could otherwise - # land on the eMMC's ESP. - disk-image-sintra-usb = - let - cfg = self.nixosConfigurations.sintra-installed.extendModules { - modules = [ - ({ lib, ... }: { - fileSystems."/".device = lib.mkForce "/dev/disk/by-label/nixos-usb"; - fileSystems."/boot".device = lib.mkForce "/dev/disk/by-label/ESP-USB"; - system.autoUpgrade.enable = lib.mkForce false; - - # The Sintra's Aaeon firmware USB-boots in Legacy/BIOS mode — it - # boots the live ISO via its isolinux (BIOS) El Torito image, not - # the UEFI ESP. systemd-boot is UEFI-only, so a dd'd systemd-boot - # image isn't recognised as bootable. Switch THIS USB image to - # GRUB with BOTH BIOS (MBR + bios_grub partition, via the "hybrid" - # table below) and UEFI (removable /EFI/BOOT/BOOTX64.EFI) — mirroring - # the live ISO's dual boot — so it boots on Legacy and UEFI alike. - # Scoped to the USB image; the eMMC install keeps systemd-boot. - boot.loader.systemd-boot.enable = lib.mkForce false; - boot.loader.efi.canTouchEfiVariables = lib.mkForce false; - boot.loader.grub = { - enable = lib.mkForce true; - efiSupport = true; - efiInstallAsRemovable = true; - # make-disk-image's build VM exposes the image as /dev/vda; - # GRUB installs its BIOS stage to that disk's MBR. - devices = lib.mkForce [ "/dev/vda" ]; - }; - }) - ]; - }; - baseImage = import (nixpkgs + "/nixos/lib/make-disk-image.nix") { - inherit pkgs lib; - config = cfg.config; - format = "raw"; - # hybrid = GPT + bios_grub partition + ESP → BIOS + UEFI bootable. - partitionTableType = "hybrid"; - diskSize = "auto"; - label = "nixos-usb"; # ext4 root label (make-disk-image -L) - }; - in - pkgs.runCommand "nixos-disk-image-sintra-usb" - { nativeBuildInputs = [ pkgs.parted pkgs.mtools ]; } - '' - mkdir -p $out - cp --sparse=always ${baseImage}/nixos.img $out/nixos.img - chmod +w $out/nixos.img - # make-disk-image hardcodes the ESP FAT label to "ESP"; relabel the - # volume to ESP-USB so /boot (by-label/ESP-USB) doesn't collide with - # the eMMC's ESP. Volume label only — bootloader files are untouched, - # and UEFI loads /EFI/BOOT/BOOTX64.EFI regardless of the label. - espStart=$(parted -sm "$out/nixos.img" unit B print | awk -F: '$1==1 {gsub("B","",$2); print $2}') - echo "ESP partition starts at byte $espStart — relabelling to ESP-USB" - export MTOOLS_SKIP_CHECK=1 - mlabel -i "$out/nixos.img@@$espStart" ::ESP-USB - printf 'verify ESP label: '; mlabel -i "$out/nixos.img@@$espStart" -s :: || true - ''; - # USB-bootable images (see usbBootModule / mkUsbDiskImage in the let # block). Flash with dd or balenaEtcher, boot the stick, done — no # installer step. The plain disk-image- reuses the generic @@ -609,8 +625,26 @@ # stage-1's by-label/nixos resolve to the internal drive instead of the # stick — the stage-2 init path baked into the USB's boot entry isn't on # that root, so stage 1 aborts. These variants can't hit that. - disk-image-batm3-usb = mkUsbDiskImage "batm3" self.nixosConfigurations.batm3-usb; - disk-image-douro-usb = mkUsbDiskImage "douro" self.nixosConfigurations.douro-usb; + disk-image-batm3-usb = mkUsbDiskImage { + machineModel = "batm3"; + usbConfig = self.nixosConfigurations.batm3-usb; + }; + disk-image-douro-usb = mkUsbDiskImage { + machineModel = "douro"; + usbConfig = self.nixosConfigurations.douro-usb; + }; + + # UP Board models: hybrid table + GRUB, so one stick boots on the Aaeon + # firmware's Legacy/BIOS USB path as well as UEFI. Distinct labels also + # matter more here than on douro — a sintra's eMMC already holds a + # nixos/ESP-labelled install, and stage-1 would otherwise race the two + # roots and likely mount the eMMC. + disk-image-sintra-usb = mkUsbDiskImage { + machineModel = "sintra"; + usbConfig = self.nixosConfigurations.sintra-usb; + partitionTableType = "hybrid"; + grubBiosDevice = "/dev/vda"; + }; # Backwards compat iso = self.nixosConfigurations.douro.config.system.build.isoImage;