Recently Written · git

waybase

Bootable installer: KISS, Gentoo, Arch or Alpine + Nix + a Wayland compositor (sway, river, labwc, dwl)

git clone https://github.com/equwal/waybase

Log | Files | Refs


commit 9557ec7b9b78a8cbe5ad02889b4cb85aea8fb049
equwal <13551856+equwal@users.noreply.github.com>
2026-10-04 19:36:25 -0700

firmware.sh: install pinned linux-firmware files with sha256 check

 .github/workflows/ci.yml |  2 +-
 answers.example          |  2 ++
 build-image.sh           |  2 +-
 firmware.example         |  3 +++
 firmware.sh              | 37 +++++++++++++++++++++++++++++++++++++
 install.sh               |  3 +++
 test-qemu.py             |  3 +++
 7 files changed, 50 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index cbfa033..a834a0b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,7 +5,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
-      - run: shellcheck install.sh build-image.sh test.sh backends/*.sh
+      - run: shellcheck install.sh firmware.sh build-image.sh test.sh backends/*.sh
       - run: python3 -m py_compile test-qemu.py
   qemu-smoke:
     # Alpine + sway, LUKS: build the image, install, boot, check (about 20 min).
diff --git a/answers.example b/answers.example
index e8856f3..a9d839b 100644
--- a/answers.example
+++ b/answers.example
@@ -11,5 +11,7 @@ USER_PASS=change-me
 ROOT_PASS=change-me
 HOSTNAME_NEW=waybase
 #FIRMWARE=no              # skip linux-firmware (VMs)
+#FIRMWARE_TAG=20260916      # pinned linux-firmware files: tag and list (see firmware.example)
+#FIRMWARE_LIST=$D/firmware.example
 #SERIAL=1                 # console and login on ttyS0 (tests)
 #NIXPKGS=github:NixOS/nixpkgs/nixos-unstable
diff --git a/build-image.sh b/build-image.sh
index 04fa43c..7977768 100644
--- a/build-image.sh
+++ b/build-image.sh
@@ -13,7 +13,7 @@ build() {
     ISO=$OUT/alpine-standard-$R-x86_64.iso
     [ -s "$ISO" ] || wget -q -O "$ISO" "https://dl-cdn.alpinelinux.org/alpine/v$V/releases/x86_64/${ISO##*/}"
     W=$(mktemp -d); mkdir -p "$W/waybase"
-    cp -r "$SRC/install.sh" "$SRC/answers.example" "$SRC/backends" "$W/waybase/"
+    cp -r "$SRC/install.sh" "$SRC/firmware.sh" "$SRC/firmware.example" "$SRC/answers.example" "$SRC/backends" "$W/waybase/"
     # serial console as well as the screen, so the installer runs headless too
     xorriso -osirrox on -indev "$ISO" -extract /boot/grub/grub.cfg "$W/grub.cfg" \
         -extract /boot/syslinux/syslinux.cfg "$W/syslinux.cfg" 2>/dev/null
diff --git a/firmware.example b/firmware.example
new file mode 100644
index 0000000..e8079e2
--- /dev/null
+++ b/firmware.example
@@ -0,0 +1,3 @@
+# SHA256 PATH of one linux-firmware file, pinned to the tag in FIRMWARE_TAG.
+# firmware.sh -s TAG prints lines like these for the files dmesg reports missing.
+2cde41c3e5ad181423bcc3e98ff9c49f743c88f18646af4d0b3c3a9664b831a1 i915/kbl_dmc_ver1_04.bin
diff --git a/firmware.sh b/firmware.sh
new file mode 100644
index 0000000..f022167
--- /dev/null
+++ b/firmware.sh
@@ -0,0 +1,37 @@
+#!/bin/sh
+# firmware.sh - install pinned linux-firmware files into /lib/firmware.
+#   sh firmware.sh [-r ROOT] TAG LIST   for each "SHA256 PATH" line of LIST: fetch PATH from
+#                                       linux-firmware tag TAG, check the hash, install ROOT/lib/firmware/PATH
+#   sh firmware.sh -s TAG               print LIST lines for files the kernel failed to load (dmesg);
+#                                       the hashes come from the download: review them before you pin
+set -eu
+URL=https://gitlab.com/kernel-firmware/linux-firmware/-/raw
+die() { echo "firmware.sh: $*" >&2; exit 1; }
+ROOT=; SCAN=
+[ "${1:-}" = -r ] && { ROOT=$2; shift 2; }
+[ "${1:-}" = -s ] && { SCAN=1; shift; }
+[ $# -ge 1 ] || die "usage: firmware.sh [-r ROOT] TAG LIST | -s TAG"
+TAG=$1
+T=$(mktemp); trap 'rm -f "$T"' EXIT
+fetch() { curl -fsSL --retry 5 --retry-all-errors --retry-delay 5 -o "$T" "$URL/$TAG/$1"; }
+safe() { case $1 in /* | *..*) die "bad path: $1" ;; esac; }
+
+if [ -n "$SCAN" ]; then
+    dmesg | sed -n 's/.*Direct firmware load for \(.*\) failed with error.*/\1/p' | sort -u |
+    while read -r p; do
+        safe "$p"
+        [ -e "/lib/firmware/$p" ] && continue
+        if fetch "$p" 2>/dev/null; then echo "$(sha256sum "$T" | cut -d' ' -f1) $p"; else echo "# not in $TAG: $p"; fi
+    done
+    exit 0
+fi
+
+[ -r "${2:-}" ] || die "list not readable: ${2:-}"
+while read -r sum p; do
+    case $sum in '' | '#'*) continue ;; esac
+    safe "$p"
+    fetch "$p" || die "download failed: $p"
+    echo "$sum  $T" | sha256sum -c -s - || die "hash mismatch: $p"
+    install -D -m 644 "$T" "$ROOT/lib/firmware/$p"
+    echo "firmware: $p"
+done < "$2"
diff --git a/install.sh b/install.sh
index cb95638..43aad78 100644
--- a/install.sh
+++ b/install.sh
@@ -15,6 +15,8 @@
 #   SERIAL=1    login getty on ttyS0 and kernel console there (for tests)
 #   NIXPKGS     flake ref for Nix-supplied packages (default: nixpkgs)
 #   FIRMWARE=no skip linux-firmware (VMs; real hardware usually needs it)
+#   FIRMWARE_TAG, FIRMWARE_LIST   install the pinned files of LIST (SHA256 PATH lines, see firmware.sh)
+#               from linux-firmware tag FIRMWARE_TAG into /lib/firmware
 set -eu
 D=$(cd "$(dirname "$0")" && pwd)
 M=/mnt/waybase
@@ -289,6 +291,7 @@ esac; done
 ch "chown -R $USERNAME:\$(id -g $USERNAME) /home/$USERNAME"
 
 # --- kernel, initramfs, bootloader --------------------------------------
+[ -n "${FIRMWARE_LIST:-}" ] && { log "pinned firmware"; sh "$D/firmware.sh" -r $M "${FIRMWARE_TAG:?FIRMWARE_TAG is required with FIRMWARE_LIST}" "$FIRMWARE_LIST"; }
 log "kernel + initramfs"
 kernel
 if [ "$ENCRYPT" = yes ]; then ROOTARG="root=/dev/mapper/root $CRYPTARGS"; else ROOTARG="root=UUID=$UR"; fi
diff --git a/test-qemu.py b/test-qemu.py
index 43e09b0..65b1330 100644
--- a/test-qemu.py
+++ b/test-qemu.py
@@ -110,6 +110,7 @@ t0 = time.time()
 vm = VM(uefi=True, live=True)
 vm.expect("login:", 900); vm.send("root"); time.sleep(5)
 answers = (f"BASE={base}\nCOMPOSITOR='{comps}'\nDISK=/dev/vda\nYES=1\nSERIAL=1\nFIRMWARE=no\n"
+           f"FIRMWARE_TAG=20260916\nFIRMWARE_LIST=$D/firmware.example\n"
            f"ENCRYPT={'yes' if encrypt else 'no'}\nLUKS_PASS={PW['luks']}\nUSERNAME=tester\n"
            f"USER_PASS={PW['user']}\nROOT_PASS={PW['root']}\n")
 vm.run(f"cat > /tmp/answers <<'EOA'\n{answers}EOA")
@@ -136,6 +137,7 @@ nix --version 2>&1 | head -1 | sed 's/^/NIXVER /'
 nix store info --store daemon >/tmp/ns.log 2>&1 && echo NIXDAEMON_OK || tail -3 /tmp/ns.log
 for b in foot fuzzel pipewire wireplumber waybase-session; do command -v $b >/dev/null || echo MISSING:$b; done
 echo FROMNIX $(cat /etc/waybase-from-nix)
+test -s /lib/firmware/i915/kbl_dmc_ver1_04.bin && echo FW_OK
 for c in %s; do
   d=$(mktemp -d); chmod 700 $d
   XDG_RUNTIME_DIR=$d WLR_BACKENDS=headless WLR_RENDERER=pixman WLR_LIBINPUT_NO_DEVICES=1 $c >/tmp/$c.log 2>&1 &
@@ -147,6 +149,7 @@ done
 """ % comps, 900)
 check("nix --version", "NIXVER nix (Nix)" in r, (re.search(r"NIXVER (.*)", r) or [None, ""])[1].strip())
 check("nix daemon reachable", "NIXDAEMON_OK" in r)
+check("pinned firmware installed", "FW_OK" in r)
 check("session programs", "MISSING:" not in r, " ".join(re.findall(r"MISSING:\S+", r)))
 note("supplied by Nix: " + (re.search(r"FROMNIX(.*)", r) or [None, ""])[1].strip())
 for c in comps.split():