diff --git a/deploy/nixos/configuration.nix b/deploy/nixos/configuration.nix index 8f7928e..fe00d43 100644 --- a/deploy/nixos/configuration.nix +++ b/deploy/nixos/configuration.nix @@ -3,6 +3,122 @@ { config, lib, pkgs, pkgs-unstable, ... }: +let + # ── Firmware pruning (bitspire#70 sizing) ──────────────────────────── + # hardware.enableRedistributableFirmware installs the entire linux-firmware + # tree: 752MB compressed, 16% of the image and its single largest item. The + # fleet is four fixed Intel boards. The other ~640MB is firmware for + # Qualcomm, Mellanox, NVIDIA, Marvell, AMD and MediaTek parts that will + # never appear in one of these machines. + # + # Keep only what a bitSpire board can plausibly load. Entries are paths + # inside lib/firmware; nothing outside this list is copied. + firmwareKeep = [ + # Intel GPU. Gen9 (Apollo Lake) loads DMC from here. Bay Trail and + # Haswell load nothing, but 9.6MB is cheap insurance against a board swap. + "i915" + # Intel WiFi, 89MB and the bulk of what survives, covering every Intel + # card since 2008. This is the conservative half of the trade: losing the + # network on a deployed ATM is not remotely recoverable. Narrow it to the + # specific generation once each machine's card is known, via + # `lspci -k | grep -A3 Network` on the box. + "intel/iwlwifi" + "rtl_nic" # Realtek GbE (r8169) — the UP Board's onboard NIC + "rtw88" # Realtek WiFi, the usual M.2 or USB retrofit + "rtw89" + "brcm" # Broadcom WiFi, the other usual retrofit + # Intel Smart Sound Technology DSP, 420KB. Cherry Trail boards (sintra, + # tejo) probe intel_sst_acpi at boot whether or not anything will use the + # audio, and without the blob every boot logs + # Direct firmware load for intel/fw_sst_22a8.bin failed with error -2 + # Found by pruning, rebooting sintra and reading dmesg. The audio stack is + # gone so this changes no behaviour, but a recurring error in a payment + # terminal's boot log is worth 420KB to remove: an error people learn to + # ignore is one they will ignore when it matters. + "intel/fw_sst_0f28.bin" + "intel/fw_sst_0f28_ssp0.bin" + "intel/fw_sst_22a8.bin" + ]; + + # Prune the tree rather than hand-pick files, so a firmware bump can't + # silently drop a blob we depend on. Left UNCOMPRESSED on purpose: NixOS + # compresses each hardware.firmware entry itself, zstd or xz depending on + # what the machine's kernel understands, and douro's 5.15 predates zstd + # firmware support. Pre-compressing here would hand douro a tree it cannot + # read. + bitspireFirmware = pkgs.runCommand "linux-firmware-bitspire" + { + inherit (pkgs.linux-firmware) version; + meta = pkgs.linux-firmware.meta // { + description = "linux-firmware pruned to the hardware bitSpire ships on"; + }; + } + '' + src=${pkgs.linux-firmware}/lib/firmware + dst=$out/lib/firmware + mkdir -p "$dst" + + for p in ${lib.escapeShellArgs firmwareKeep}; do + if [ ! -e "$src/$p" ]; then + echo "ERROR: firmwareKeep entry '$p' is not in linux-firmware" >&2 + exit 1 + fi + mkdir -p "$dst/$(dirname "$p")" + cp -a "$src/$p" "$dst/$p" + done + + # A kept directory can contain symlinks pointing at blobs OUTSIDE it: + # brcm/brcmfmac*.bin are links into cypress/, for instance. Left dangling + # they fail nixpkgs' firmware compression step, and silently deleting + # them would quietly drop firmware a device needs. So pull the targets in + # instead. Looped because a resolved target can itself be a link. + for _pass in 1 2 3; do + _pulled=0 + while IFS= read -r link; do + tgt=$(readlink -m "$link") + case "$tgt" in + "$dst"/*) rel=''${tgt#"$dst"/} ;; + *) continue ;; + esac + if [ ! -e "$dst/$rel" ] && [ -e "$src/$rel" ]; then + mkdir -p "$dst/$(dirname "$rel")" + cp -a "$src/$rel" "$dst/$rel" + _pulled=1 + fi + done < <(find "$dst" -xtype l) + [ "$_pulled" -eq 0 ] && break + done + + # Anything still dangling is not in linux-firmware at all. Fail loudly + # rather than ship a tree with holes in it. + if find "$dst" -xtype l | grep -q .; then + echo "ERROR: dangling firmware symlinks after resolution:" >&2 + find "$dst" -xtype l >&2 + exit 1 + fi + + # linux-firmware stores many blobs under a vendor directory and leaves a + # flat top-level symlink pointing at them, e.g. + # iwlwifi-cc-a0-77.ucode -> intel/iwlwifi/iwlwifi-cc-a0-77.ucode. The + # kernel requests the flat name, so a kept blob is useless without its + # link. Recreate every top-level link whose target survived the prune. + ( cd "$src" + find . -maxdepth 1 -type l -printf '%f\t%l\n' \ + | while IFS="$(printf '\t')" read -r link target; do + # if/then, not `[ ... ] && ln`: the latter makes the loop's exit + # status depend on whether the LAST candidate matched, and a + # non-match returns 1, which set -e turns into a build failure. + # Whether it fails is then a function of readdir order. + if [ -e "$dst/$target" ]; then + ln -s "$target" "$dst/$link" + fi + done + ) + + echo "firmware kept: $(find "$dst" -type f | wc -l) files, \ + $(find "$dst" -type l | wc -l) links, $(du -sh "$dst" | cut -f1) uncompressed" + ''; +in { # System basics system.stateVersion = "24.05"; @@ -28,6 +144,26 @@ documentation.enable = false; documentation.nixos.enable = false; + # Ship the pruned firmware tree instead of all of linux-firmware. mkForce + # because every hardware/*.nix sets enableRedistributableFirmware = true; + # overriding once here keeps the four machines in step. Turning that option + # off also drops the extras it bundles (sof-firmware, libreelec-dvb, + # alsa-firmware, intel2200BG, zd1211fw and friends), none of which applies to + # a soundless kiosk on a wired Intel board. The regulatory database is + # normally implied by the same option, so ask for it explicitly: without it + # WiFi is pinned to the most restrictive channel set. + hardware.enableRedistributableFirmware = lib.mkForce false; + hardware.wirelessRegulatoryDatabase = true; + hardware.firmware = [ bitspireFirmware ]; + + # Make the prune stick. Without this, a nixpkgs bump or a stray module + # setting enableRedistributableFirmware back to true silently re-adds 750MB + # and nobody notices until an eMMC runs out of room at 04:00. The regex + # matches the upstream package's versioned name (linux-firmware-20260519) + # and deliberately not ours (linux-firmware-bitspire), so the pruned tree + # passes and the full one fails the build with a readable error. + system.forbiddenDependenciesRegexes = [ "linux-firmware-[0-9]" ]; + # Networking networking = { hostName = "bitspire";