From 583f3128eba4b510148c7bc6690746d9499177be Mon Sep 17 00:00:00 2001 From: Jimmy Van Veen Date: Wed, 26 Aug 2026 15:14:09 -0400 Subject: [PATCH 1/6] Add qcom-firmware-extract Copy the vendor-signed Qualcomm firmware named by a Snapdragon laptop device tree from its Windows driver store into /usr/lib/firmware/updates. --- .../.omarchy/package.json | 3 + pkgbuilds/qcom-firmware-extract/PKGBUILD | 38 +++ .../qcom-firmware-extract/README.package.md | 52 +++ .../qcom-firmware-extract | 321 ++++++++++++++++++ 4 files changed, 414 insertions(+) create mode 100644 pkgbuilds/qcom-firmware-extract/.omarchy/package.json create mode 100644 pkgbuilds/qcom-firmware-extract/PKGBUILD create mode 100644 pkgbuilds/qcom-firmware-extract/README.package.md create mode 100755 pkgbuilds/qcom-firmware-extract/qcom-firmware-extract diff --git a/pkgbuilds/qcom-firmware-extract/.omarchy/package.json b/pkgbuilds/qcom-firmware-extract/.omarchy/package.json new file mode 100644 index 000000000..2a9719d3f --- /dev/null +++ b/pkgbuilds/qcom-firmware-extract/.omarchy/package.json @@ -0,0 +1,3 @@ +{ + "source": "local" +} diff --git a/pkgbuilds/qcom-firmware-extract/PKGBUILD b/pkgbuilds/qcom-firmware-extract/PKGBUILD new file mode 100644 index 000000000..326bc8b0a --- /dev/null +++ b/pkgbuilds/qcom-firmware-extract/PKGBUILD @@ -0,0 +1,38 @@ +# Maintainer: Jimmy Van Veen + +# Snapdragon X laptops need a handful of firmware files signed by the laptop +# vendor (GPU zap shader, audio and compute DSP images) that linux-firmware is +# not allowed to ship. This tool copies them from the owner's own Windows +# installation on the same machine, driven by the device tree's firmware-name +# properties, into /usr/lib/firmware/updates. The Omarchy installer stages the +# files in the live session before the disk is written and installs them from +# install/hardware/qualcomm/firmware.sh; `sudo qcom-firmware-extract` does the +# same on an installed system. +# +# arch=('any') on purpose: the omarchy repo's package lists are downloaded by +# the x86_64 ISO build too, and the tool exits immediately on a machine +# without a device tree. +# +# RETIRE when linux-firmware ships the vendor directories for the supported +# laptops (it already does for Lenovo's and Dell's); see the script header. + +pkgname=qcom-firmware-extract +pkgver=1 +pkgrel=1 +pkgdesc="Copy vendor-signed Qualcomm Snapdragon firmware from the machine's Windows installation" +arch=('any') +url="https://github.com/omacom-io/omarchy-pkgs" +license=('GPL-2.0-or-later') +depends=('bash' 'coreutils' 'findutils' 'util-linux') +optdepends=('limine-mkinitcpio-hook: rebuild the boot image after installing firmware') +options=('!debug') +source=( + 'qcom-firmware-extract' + 'README.package.md' +) +sha256sums=('SKIP' 'SKIP') + +package() { + install -Dm755 qcom-firmware-extract "${pkgdir}/usr/bin/qcom-firmware-extract" + install -Dm644 README.package.md "${pkgdir}/usr/share/doc/${pkgname}/README.md" +} diff --git a/pkgbuilds/qcom-firmware-extract/README.package.md b/pkgbuilds/qcom-firmware-extract/README.package.md new file mode 100644 index 000000000..8a2bf416b --- /dev/null +++ b/pkgbuilds/qcom-firmware-extract/README.package.md @@ -0,0 +1,52 @@ +# qcom-firmware-extract + +Copies the vendor-signed Qualcomm firmware a Snapdragon laptop needs — the GPU +zap shader and the audio/compute DSP images — from the owner's own Windows +installation on the same machine into `/usr/lib/firmware/updates/`. + +Omarchy ships no vendor-signed firmware: the live ISO and every package carry +only files from linux-firmware. The files this tool copies never leave the +machine and are never downloaded. It is a temporary measure until the laptop +vendors contribute the files to linux-firmware, as Lenovo and Dell have. + +## How it works + +The device tree names every firmware file the kernel will ask for +(`firmware-name` properties). The tool keeps the names that are missing under +`/usr/lib/firmware{,/updates}`, finds each one by file name in +`Windows/System32/DriverStore/FileRepository` on any NTFS partition it can +mount read-only (newest copy wins), and installs it under +`/usr/lib/firmware/updates/`. The GPU zap shader is added to the +initramfs through `/etc/mkinitcpio.conf.d/qcom-firmware.conf`; the DSP images +are loaded from the root filesystem. What was installed, from where, and its +checksum is recorded in `/var/lib/omarchy/qcom-firmware/manifest`. + +Nothing is model-specific. A laptop whose firmware linux-firmware already +ships gets nothing copied; a machine without a device tree exits at once. + +## When it runs + +- **Installer, live session** — `qcom-firmware-extract --stage DIR` right + after the disk is chosen, before anything is written. A full-disk install + destroys the Windows partition the files come from, so this is the only + moment they can be read. The stage is copied into the target. +- **Installer, hardware setup** — `qcom-firmware-extract --install + --no-rebuild` from `install/hardware/qualcomm/firmware.sh`, using the stage + (or a Windows partition still on disk). The installer builds the boot image + once afterwards. +- **Installed system** — `sudo qcom-firmware-extract` scans the disks again, + or `sudo qcom-firmware-extract -d /path/to/FileRepository` takes any driver + store you can mount (a Windows install of the *same model*: the files are + tied to the vendor's signing keys). It rebuilds the boot image; reboot + afterwards. + +BitLocker volumes cannot be read; turn BitLocker off in Windows first. + +## Retirement + +When linux-firmware ships a machine's vendor directory the tool finds nothing +missing and does nothing. When every supported machine is covered, drop the +package and prune the `/usr/lib/firmware/updates` entries listed in the +manifest. + +Derived from Canonical's `qcom-firmware-extract` (GPL-2+). diff --git a/pkgbuilds/qcom-firmware-extract/qcom-firmware-extract b/pkgbuilds/qcom-firmware-extract/qcom-firmware-extract new file mode 100755 index 000000000..cd4ed3adf --- /dev/null +++ b/pkgbuilds/qcom-firmware-extract/qcom-firmware-extract @@ -0,0 +1,321 @@ +#!/bin/bash +# +# qcom-firmware-extract — copy the vendor-signed Qualcomm firmware a Snapdragon +# laptop needs from the owner's own Windows installation. +# +# Snapdragon X laptops boot Linux with mainline drivers, but the GPU "zap" +# shader and the audio/compute DSP images are signed by the laptop vendor and +# not (yet) redistributable, so linux-firmware cannot ship them. Every file +# the kernel will ask for is named by the device tree (the `firmware-name` +# properties), and every one of them sits in the Windows driver store on the +# same machine. This tool joins the two: it reads the device tree, keeps only +# the names that are missing from /usr/lib/firmware, finds them in the driver +# store and copies them to /usr/lib/firmware/updates/. +# +# Nothing here is model-specific. A machine whose blobs linux-firmware already +# ships gets nothing copied; a machine without a device tree (x86) exits at +# once with nothing to do. +# +# Modes +# --stage DIR Live ISO, before the disk is touched: scan the Windows +# partitions and save the missing files under DIR (the +# installer copies DIR into the target). Never fails. +# --install Installed system or the installer's chroot: take the +# files from the stage (--stage-dir, default +# /var/lib/omarchy/firmware-stage), else from a Windows +# partition still on disk, write them under +# /usr/lib/firmware/updates, record a manifest, add the +# zap shader to the initramfs, and rebuild the boot image +# (unless --no-rebuild). This is the default mode. +# --list-missing Print the device-tree firmware names not present under +# /usr/lib/firmware, one per line. Needs no root. +# -d DIR Use DIR as the driver store (a Windows/System32/ +# DriverStore/FileRepository directory, or any directory +# tree holding the files) instead of scanning partitions. +# +# Files land in /usr/lib/firmware/updates/, never /usr/lib/firmware/: the +# kernel and mkinitcpio search updates/ first and no package owns anything +# there, so the day linux-firmware ships a machine's files pacman does not +# collide with the copies. +# +# RETIRE for a given machine when linux-firmware ships its vendor directory +# (as it already does for Lenovo's and Dell's X1E laptops): the tool finds +# nothing missing and does nothing. When every supported machine is covered, +# drop the package and prune /usr/lib/firmware/updates entries listed in +# /var/lib/omarchy/qcom-firmware/manifest. +# +# Derived from Canonical's qcom-firmware-extract (Tobias Heider, GPL-2+): +# https://salsa.debian.org/debian/qcom-firmware-extract +# That tool keys on /proc/device-tree/model with a per-model table, extracts +# a fixed list of file names, and builds a .deb; this one reads the names +# from the device tree and writes the files directly. +# +# SPDX-License-Identifier: GPL-2.0-or-later + +set -euo pipefail + +DT_ROOT=${QCOM_FW_DT_ROOT:-/sys/firmware/devicetree/base} +FW_ROOT=${QCOM_FW_FIRMWARE_ROOT:-/usr/lib/firmware} +# Destination prefix for the installed-system paths (tests only). +ROOT=${QCOM_FW_ROOT:-} +STATE_DIR="$ROOT/var/lib/omarchy/qcom-firmware" +MANIFEST="$STATE_DIR/manifest" +INITCPIO_CONF="$ROOT/etc/mkinitcpio.conf.d/qcom-firmware.conf" +DEFAULT_STAGE_DIR="$ROOT/var/lib/omarchy/firmware-stage" +DRIVERSTORE_PATH="Windows/System32/DriverStore/FileRepository" + +mode=install +stage_dir="" +driverstore="" +rebuild=yes + +usage() { + sed -n '2,/^# SPDX/{/^# SPDX/d;s/^# \{0,1\}//p}' "$0" +} + +while (($#)); do + case $1 in + --stage) mode=stage; stage_dir=${2:?--stage needs a directory}; shift ;; + --stage-dir) stage_dir=${2:?--stage-dir needs a directory}; shift ;; + --install) mode=install ;; + --list-missing) mode=list ;; + --no-rebuild) rebuild=no ;; + -d) driverstore=${2:?-d needs a directory}; shift ;; + -h | --help) usage; exit 0 ;; + *) echo "qcom-firmware-extract: unknown option: $1" >&2; usage >&2; exit 2 ;; + esac + shift +done + +log() { printf 'qcom-firmware-extract: %s\n' "$*"; } +warn() { printf 'qcom-firmware-extract: %s\n' "$*" >&2; } + +# --------------------------------------------------------------------------- +# 1. What does this machine ask for, and what is already there? +# --------------------------------------------------------------------------- + +# Prints " " for every firmware-name entry in the +# device tree. A property can hold several NUL-separated strings. +dt_firmware_names() { + local prop node name + [[ -d $DT_ROOT ]] || return 0 + find "$DT_ROOT" -name firmware-name -type f -print0 2>/dev/null | sort -z | + while IFS= read -r -d '' prop; do + node=$(basename "$(dirname "$prop")") + node=${node%%@*} + tr '\0' '\n' <"$prop" | while IFS= read -r name; do + [[ -n $name ]] && printf '%s %s\n' "$node" "$name" + done + done +} + +# True when the kernel can already load NAME: plain or compressed, from +# updates/ or the packaged tree. +firmware_present() { + local name=$1 dir ext + for dir in "$FW_ROOT/updates" "$FW_ROOT"; do + for ext in '' .zst .xz .gz; do + [[ -f "$dir/$name$ext" ]] && return 0 + done + done + return 1 +} + +declare -a missing_nodes=() missing_names=() +while read -r node name; do + firmware_present "$name" && continue + missing_nodes+=("$node") + missing_names+=("$name") +done < <(dt_firmware_names) + +if [[ $mode == list ]]; then + ((${#missing_names[@]})) && printf '%s\n' "${missing_names[@]}" + exit 0 +fi + +if [[ ! -d $DT_ROOT ]]; then + log "no device tree on this machine, nothing to do" + exit 0 +fi +if ((${#missing_names[@]} == 0)); then + log "every firmware file the device tree names is already installed" + exit 0 +fi + +# Root is needed to write under /usr/lib/firmware (and, below, to mount +# partitions); staging a -d driver store into a user directory needs neither. +if ((EUID != 0)) && [[ $mode == install && -z $ROOT ]]; then + warn "must run as root" + exit 1 +fi + +# --------------------------------------------------------------------------- +# 2. Where can they come from? +# --------------------------------------------------------------------------- + +declare -a sources=() # directories to search, best first +declare -a mounts=() # temporary mounts to undo +cleanup() { + local m + for m in "${mounts[@]}"; do + umount "$m" 2>/dev/null || true + rmdir "$m" 2>/dev/null || true + done +} +trap cleanup EXIT + +# Mount every NTFS partition read-only and keep the ones that hold a driver +# store. Internal disks first, so a Windows recovery stick does not win over +# the installation the machine actually runs. A BitLocker volume cannot be +# read; it is reported so the user knows why nothing was found. +scan_windows_partitions() { + local dev fstype tran mp bitlocker="" + if ((EUID != 0)); then + warn "must run as root to read the Windows partitions" + exit 1 + fi + local -a internal=() external=() + while read -r dev fstype tran; do + case $fstype in + ntfs) ;; + BitLocker) bitlocker+=" $dev"; continue ;; + *) continue ;; + esac + if [[ $tran == usb ]]; then external+=("$dev"); else internal+=("$dev"); fi + done < <(lsblk -rpno PATH,FSTYPE,TRAN 2>/dev/null | awk 'NF>=2') + + for dev in "${internal[@]}" "${external[@]}"; do + mp=$(mktemp -d /run/qcom-firmware-extract.XXXXXX) + if mount -t ntfs3 -o ro,nosuid,nodev,noexec "$dev" "$mp" 2>/dev/null; then + mounts+=("$mp") + if [[ -d "$mp/$DRIVERSTORE_PATH" ]]; then + log "Windows driver store found on $dev" + sources+=("$mp/$DRIVERSTORE_PATH") + fi + else + warn "could not mount $dev read-only (ntfs3); skipping it" + rmdir "$mp" 2>/dev/null || true + fi + done + [[ -n $bitlocker ]] && warn "BitLocker volume(s) skipped:$bitlocker — turn BitLocker off in Windows to let the firmware be read" + return 0 +} + +# Newest copy of a file name anywhere under the sources. +find_in_sources() { + local base=$1 + find "${sources[@]}" -type f -iname "$base" -printf '%T@ %p\n' 2>/dev/null | + sort -rn | head -n1 | cut -d' ' -f2- +} + +# --------------------------------------------------------------------------- +# 3. Stage: live session, before the disk is written +# --------------------------------------------------------------------------- + +if [[ $mode == stage ]]; then + if [[ -f "$stage_dir/manifest" ]]; then + log "already staged in $stage_dir" + exit 0 + fi + if [[ -n $driverstore ]]; then sources=("$driverstore"); else scan_windows_partitions; fi + mkdir -p "$stage_dir" + staged=0 + : >"$stage_dir/manifest.partial" + for i in "${!missing_names[@]}"; do + name=${missing_names[$i]} + src="" + ((${#sources[@]})) && src=$(find_in_sources "$(basename "$name")") + if [[ -z $src ]]; then + warn "not found in the driver store: $name" + continue + fi + install -Dm644 "$src" "$stage_dir/$name" + printf '%s %s %s\n' "$name" "$(sha256sum "$src" | cut -d' ' -f1)" "$src" >>"$stage_dir/manifest.partial" + staged=$((staged + 1)) + done + mv "$stage_dir/manifest.partial" "$stage_dir/manifest" + log "staged $staged of ${#missing_names[@]} firmware files in $stage_dir" + exit 0 +fi + +# --------------------------------------------------------------------------- +# 4. Install: from the stage, else from a Windows partition still on disk +# --------------------------------------------------------------------------- + +[[ -n $stage_dir ]] || stage_dir=$DEFAULT_STAGE_DIR +if [[ -n $driverstore ]]; then + sources=("$driverstore") +elif [[ -f "$stage_dir/manifest" ]]; then + log "using the firmware staged by the installer in $stage_dir" + sources=("$stage_dir") +else + scan_windows_partitions +fi + +mkdir -p "$STATE_DIR" +touch "$MANIFEST" +installed=0 +for i in "${!missing_names[@]}"; do + name=${missing_names[$i]} + src="" + ((${#sources[@]})) && src=$(find_in_sources "$(basename "$name")") + if [[ -z $src ]]; then + warn "not found: $name" + continue + fi + install -Dm644 "$src" "$FW_ROOT/updates/$name" + sum=$(sha256sum "$src" | cut -d' ' -f1) + # Keep one line per firmware name. + grep -v "^$name " "$MANIFEST" >"$MANIFEST.new" || true + printf '%s %s %s %s\n' "$name" "$sum" "$src" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >>"$MANIFEST.new" + mv "$MANIFEST.new" "$MANIFEST" + log "installed $name" + installed=$((installed + 1)) +done + +# The GPU zap shader is the one file needed before the root filesystem is +# mounted: the msm driver is in the initramfs (mkinitcpio's kms hook) and +# loads the zap shader the first time the DRM device is opened. The DSP images +# are loaded later from the root filesystem. List only files that exist — +# a FILES entry that does not exist fails every later initramfs build. +declare -a initramfs_files=() +while read -r node name; do + [[ $node == zap-shader ]] || continue + for dir in "$FW_ROOT/updates" "$FW_ROOT"; do + for ext in '' .zst .xz .gz; do + if [[ -f "$dir/$name$ext" ]]; then + initramfs_files+=("${dir#"$ROOT"}/$name$ext") + break 2 + fi + done + done +done < <(dt_firmware_names) +if ((${#initramfs_files[@]})); then + mkdir -p "$(dirname "$INITCPIO_CONF")" + { + echo "# Generated by qcom-firmware-extract; the GPU zap shader has to be in the" + echo "# initramfs because the DRM driver loads it before the root filesystem is up." + printf 'FILES+=(%s)\n' "$(printf '%q ' "${initramfs_files[@]}" | sed 's/ $//')" + } >"$INITCPIO_CONF" +else + rm -f "$INITCPIO_CONF" +fi + +still_missing=$(( ${#missing_names[@]} - installed )) +if ((installed == 0)); then + if ((${#sources[@]} == 0)); then + warn "no Windows installation found to copy from; run again with -d when one is available" + fi + log "nothing installed; $still_missing firmware file(s) still missing" + exit 0 +fi + +log "installed $installed firmware file(s), $still_missing still missing" +if [[ $rebuild == yes ]]; then + if command -v limine-update >/dev/null 2>&1; then + limine-update + else + mkinitcpio -P + fi + log "reboot to load the added firmware" +fi From ad43022d7e0fb3c888018b2cd76e2c4ddd2208ff Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Thu, 27 Aug 2026 22:33:28 +0200 Subject: [PATCH 2/6] Select matching Qualcomm firmware variants --- pkgbuilds/qcom-firmware-extract/PKGBUILD | 8 +- .../qcom-firmware-extract/README.package.md | 9 +- .../qcom-firmware-extract | 110 ++++++++++++++++-- pkgbuilds/qcom-firmware-extract/test.sh | 52 +++++++++ 4 files changed, 165 insertions(+), 14 deletions(-) create mode 100644 pkgbuilds/qcom-firmware-extract/test.sh diff --git a/pkgbuilds/qcom-firmware-extract/PKGBUILD b/pkgbuilds/qcom-firmware-extract/PKGBUILD index 326bc8b0a..140d91f4d 100644 --- a/pkgbuilds/qcom-firmware-extract/PKGBUILD +++ b/pkgbuilds/qcom-firmware-extract/PKGBUILD @@ -13,17 +13,17 @@ # the x86_64 ISO build too, and the tool exits immediately on a machine # without a device tree. # -# RETIRE when linux-firmware ships the vendor directories for the supported -# laptops (it already does for Lenovo's and Dell's); see the script header. +# RETIRE when linux-firmware ships complete vendor directories for the +# supported laptops; see the script header. pkgname=qcom-firmware-extract pkgver=1 -pkgrel=1 +pkgrel=2 pkgdesc="Copy vendor-signed Qualcomm Snapdragon firmware from the machine's Windows installation" arch=('any') url="https://github.com/omacom-io/omarchy-pkgs" license=('GPL-2.0-or-later') -depends=('bash' 'coreutils' 'findutils' 'util-linux') +depends=('bash' 'coreutils' 'findutils' 'gzip' 'util-linux' 'xz' 'zstd') optdepends=('limine-mkinitcpio-hook: rebuild the boot image after installing firmware') options=('!debug') source=( diff --git a/pkgbuilds/qcom-firmware-extract/README.package.md b/pkgbuilds/qcom-firmware-extract/README.package.md index 8a2bf416b..6721fe56f 100644 --- a/pkgbuilds/qcom-firmware-extract/README.package.md +++ b/pkgbuilds/qcom-firmware-extract/README.package.md @@ -6,8 +6,8 @@ installation on the same machine into `/usr/lib/firmware/updates/`. Omarchy ships no vendor-signed firmware: the live ISO and every package carry only files from linux-firmware. The files this tool copies never leave the -machine and are never downloaded. It is a temporary measure until the laptop -vendors contribute the files to linux-firmware, as Lenovo and Dell have. +machine and are never downloaded. It is a temporary measure until laptop +vendors contribute complete firmware sets to linux-firmware. ## How it works @@ -15,7 +15,10 @@ The device tree names every firmware file the kernel will ask for (`firmware-name` properties). The tool keeps the names that are missing under `/usr/lib/firmware{,/updates}`, finds each one by file name in `Windows/System32/DriverStore/FileRepository` on any NTFS partition it can -mount read-only (newest copy wins), and installs it under +mount read-only. If Windows carries several variants and linux-firmware has a +companion image from the same device-tree node, the sibling image's hash +selects the compatible variant; otherwise the newest copy wins. The result is +installed under `/usr/lib/firmware/updates/`. The GPU zap shader is added to the initramfs through `/etc/mkinitcpio.conf.d/qcom-firmware.conf`; the DSP images are loaded from the root filesystem. What was installed, from where, and its diff --git a/pkgbuilds/qcom-firmware-extract/qcom-firmware-extract b/pkgbuilds/qcom-firmware-extract/qcom-firmware-extract index cd4ed3adf..f1c8f9916 100755 --- a/pkgbuilds/qcom-firmware-extract/qcom-firmware-extract +++ b/pkgbuilds/qcom-firmware-extract/qcom-firmware-extract @@ -157,7 +157,8 @@ declare -a sources=() # directories to search, best first declare -a mounts=() # temporary mounts to undo cleanup() { local m - for m in "${mounts[@]}"; do + for m in "${mounts[@]-}"; do + [[ -n $m ]] || continue umount "$m" 2>/dev/null || true rmdir "$m" 2>/dev/null || true done @@ -201,11 +202,104 @@ scan_windows_partitions() { return 0 } -# Newest copy of a file name anywhere under the sources. +# Resolve a packaged firmware file, including the compression formats used by +# linux-firmware packages. +firmware_file() { + local name=$1 dir ext + for dir in "$FW_ROOT/updates" "$FW_ROOT"; do + for ext in '' .zst .xz .gz; do + if [[ -f "$dir/$name$ext" ]]; then + printf '%s\n' "$dir/$name$ext" + return 0 + fi + done + done + return 1 +} + +firmware_sha256() { + local file=$1 + case "$file" in + *.zst) zstd -qdc -- "$file" ;; + *.xz) xz -dc -- "$file" ;; + *.gz) gzip -dc -- "$file" ;; + *) cat -- "$file" ;; + esac | sha256sum | cut -d' ' -f1 +} + +# When Windows carries several versions of a board-specific DTB, select the +# one beside the same DSP image linux-firmware already installed. The Yoga +# Slim 7x driver store contains multiple cdsp_dtbs.elf files, and loading a +# DTB signed for a different companion image prevents the CDSP from starting. +matching_companion_candidate() { + local node=$1 + shift + local -a candidates=("$@") + local dt_node dt_name installed installed_hash candidate sibling + local companion_seen=0 + + while read -r dt_node dt_name; do + [[ $dt_node == "$node" ]] || continue + installed=$(firmware_file "$dt_name") || continue + companion_seen=1 + installed_hash=$(firmware_sha256 "$installed") + + for candidate in "${candidates[@]}"; do + sibling=$(find "${candidate%/*}" -maxdepth 1 -type f \ + -iname "$(basename "$dt_name")" -print -quit 2>/dev/null) + [[ -n $sibling ]] || continue + if [[ $(sha256sum "$sibling" | cut -d' ' -f1) == "$installed_hash" ]]; then + printf '%s\n' "$candidate" + return 0 + fi + done + done < <(dt_firmware_names) + + ((companion_seen)) && return 1 + return 2 +} + +# Newest copy of an unambiguous file name anywhere under the sources. When +# several variants exist and an installed sibling identifies the right one, +# require that match instead of guessing by modification time. find_in_sources() { - local base=$1 - find "${sources[@]}" -type f -iname "$base" -printf '%T@ %p\n' 2>/dev/null | - sort -rn | head -n1 | cut -d' ' -f2- + local base=$1 node=$2 selected result candidate newest newest_mtime mtime + local -a candidates=() + while IFS= read -r -d '' candidate; do + candidates+=("$candidate") + done < <(find "${sources[@]}" -type f -iname "$base" -print0 2>/dev/null) + + ((${#candidates[@]})) || return 1 + if ((${#candidates[@]} == 1)); then + printf '%s\n' "${candidates[0]}" + return 0 + fi + + if selected=$(matching_companion_candidate "$node" "${candidates[@]}"); then + printf '%s\n' "$selected" + return 0 + else + result=$? + fi + + if ((result == 1)); then + warn "refusing ambiguous $base: no variant matches its installed companion firmware" + return 1 + fi + + # No installed companion exists to distinguish driver versions (common on + # machines whose entire vendor firmware set is absent). Preserve the prior + # behavior there and take Windows' newest copy. + newest=${candidates[0]} + newest_mtime=$(stat -c %Y "$newest" 2>/dev/null || stat -f %m "$newest") + for candidate in "${candidates[@]:1}"; do + mtime=$(stat -c %Y "$candidate" 2>/dev/null || stat -f %m "$candidate") + if ((mtime > newest_mtime)); then + newest=$candidate + newest_mtime=$mtime + fi + done + printf '%s\n' "$newest" } # --------------------------------------------------------------------------- @@ -222,9 +316,10 @@ if [[ $mode == stage ]]; then staged=0 : >"$stage_dir/manifest.partial" for i in "${!missing_names[@]}"; do + node=${missing_nodes[$i]} name=${missing_names[$i]} src="" - ((${#sources[@]})) && src=$(find_in_sources "$(basename "$name")") + ((${#sources[@]})) && src=$(find_in_sources "$(basename "$name")" "$node") if [[ -z $src ]]; then warn "not found in the driver store: $name" continue @@ -256,9 +351,10 @@ mkdir -p "$STATE_DIR" touch "$MANIFEST" installed=0 for i in "${!missing_names[@]}"; do + node=${missing_nodes[$i]} name=${missing_names[$i]} src="" - ((${#sources[@]})) && src=$(find_in_sources "$(basename "$name")") + ((${#sources[@]})) && src=$(find_in_sources "$(basename "$name")" "$node") if [[ -z $src ]]; then warn "not found: $name" continue diff --git a/pkgbuilds/qcom-firmware-extract/test.sh b/pkgbuilds/qcom-firmware-extract/test.sh new file mode 100644 index 000000000..591ef54d4 --- /dev/null +++ b/pkgbuilds/qcom-firmware-extract/test.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +set -euo pipefail + +package_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +extractor="$package_dir/qcom-firmware-extract" +scratch=$(mktemp -d) +trap 'rm -rf "$scratch"' EXIT + +# macOS' install(1) gives -D a different meaning. Prefer GNU coreutils when +# this focused test runs on a contributor's Mac; Arch uses GNU install already. +test_bin="$scratch/bin" +mkdir -p "$test_bin" +if command -v ginstall >/dev/null 2>&1; then + ln -s "$(command -v ginstall)" "$test_bin/install" +fi + +dt_root="$scratch/device-tree" +firmware_root="$scratch/firmware" +driver_store="$scratch/DriverStore" +stage="$scratch/stage" +node="$dt_root/remoteproc@0" +firmware_path="qcom/x1e80100/LENOVO/83ED" + +mkdir -p "$node" "$firmware_root/$firmware_path" \ + "$driver_store/wrong" "$driver_store/matching" +printf '%s\0%s\0' \ + "$firmware_path/qccdsp8380.mbn" \ + "$firmware_path/cdsp_dtbs.elf" >"$node/firmware-name" + +# linux-firmware carries the main CDSP image but omits its signed DTB. Windows +# retains several driver versions; deliberately make the incompatible one +# newer so a newest-filename policy selects the wrong file. +printf 'installed-cdsp' >"$firmware_root/$firmware_path/qccdsp8380.mbn" +printf 'other-cdsp' >"$driver_store/wrong/qccdsp8380.mbn" +printf 'wrong-dtb' >"$driver_store/wrong/cdsp_dtbs.elf" +printf 'installed-cdsp' >"$driver_store/matching/qccdsp8380.mbn" +printf 'matching-dtb' >"$driver_store/matching/cdsp_dtbs.elf" +touch -t 203001010000 "$driver_store/wrong/cdsp_dtbs.elf" +touch -t 202001010000 "$driver_store/matching/cdsp_dtbs.elf" + +QCOM_FW_DT_ROOT="$dt_root" \ + QCOM_FW_FIRMWARE_ROOT="$firmware_root" \ + PATH="$test_bin:$PATH" \ + bash "$extractor" --stage "$stage" -d "$driver_store" + +[[ $(<"$stage/$firmware_path/cdsp_dtbs.elf") == matching-dtb ]] || { + echo "not ok - extractor did not select the DTB matching the installed DSP image" >&2 + exit 1 +} + +echo "ok - extractor selects an ambiguous DTB by its companion firmware hash" From b57f40a4c63f2ffb993460db2faff22140f5973e Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Fri, 28 Aug 2026 13:04:00 +0200 Subject: [PATCH 3/6] Trim firmware extractor comments --- pkgbuilds/qcom-firmware-extract/PKGBUILD | 17 +----- .../qcom-firmware-extract/README.package.md | 15 +++-- .../qcom-firmware-extract | 61 +++---------------- pkgbuilds/qcom-firmware-extract/test.sh | 4 +- 4 files changed, 20 insertions(+), 77 deletions(-) diff --git a/pkgbuilds/qcom-firmware-extract/PKGBUILD b/pkgbuilds/qcom-firmware-extract/PKGBUILD index 140d91f4d..d62de5f97 100644 --- a/pkgbuilds/qcom-firmware-extract/PKGBUILD +++ b/pkgbuilds/qcom-firmware-extract/PKGBUILD @@ -1,20 +1,7 @@ # Maintainer: Jimmy Van Veen -# Snapdragon X laptops need a handful of firmware files signed by the laptop -# vendor (GPU zap shader, audio and compute DSP images) that linux-firmware is -# not allowed to ship. This tool copies them from the owner's own Windows -# installation on the same machine, driven by the device tree's firmware-name -# properties, into /usr/lib/firmware/updates. The Omarchy installer stages the -# files in the live session before the disk is written and installs them from -# install/hardware/qualcomm/firmware.sh; `sudo qcom-firmware-extract` does the -# same on an installed system. -# -# arch=('any') on purpose: the omarchy repo's package lists are downloaded by -# the x86_64 ISO build too, and the tool exits immediately on a machine -# without a device tree. -# -# RETIRE when linux-firmware ships complete vendor directories for the -# supported laptops; see the script header. +# Copy vendor-signed Qualcomm firmware named by the running device tree. +# The tool exits without changes on machines that do not need it. pkgname=qcom-firmware-extract pkgver=1 diff --git a/pkgbuilds/qcom-firmware-extract/README.package.md b/pkgbuilds/qcom-firmware-extract/README.package.md index 6721fe56f..1fbe160a3 100644 --- a/pkgbuilds/qcom-firmware-extract/README.package.md +++ b/pkgbuilds/qcom-firmware-extract/README.package.md @@ -1,7 +1,7 @@ # qcom-firmware-extract -Copies the vendor-signed Qualcomm firmware a Snapdragon laptop needs — the GPU -zap shader and the audio/compute DSP images — from the owner's own Windows +Copies the vendor-signed Qualcomm firmware a Snapdragon laptop needs, including +the GPU zap shader and audio/compute DSP images, from the owner's own Windows installation on the same machine into `/usr/lib/firmware/updates/`. Omarchy ships no vendor-signed firmware: the live ISO and every package carry @@ -18,8 +18,7 @@ The device tree names every firmware file the kernel will ask for mount read-only. If Windows carries several variants and linux-firmware has a companion image from the same device-tree node, the sibling image's hash selects the compatible variant; otherwise the newest copy wins. The result is -installed under -`/usr/lib/firmware/updates/`. The GPU zap shader is added to the +installed under `/usr/lib/firmware/updates/`. The GPU zap shader is added to the initramfs through `/etc/mkinitcpio.conf.d/qcom-firmware.conf`; the DSP images are loaded from the root filesystem. What was installed, from where, and its checksum is recorded in `/var/lib/omarchy/qcom-firmware/manifest`. @@ -29,15 +28,15 @@ ships gets nothing copied; a machine without a device tree exits at once. ## When it runs -- **Installer, live session** — `qcom-firmware-extract --stage DIR` right +- **Installer, live session:** `qcom-firmware-extract --stage DIR` right after the disk is chosen, before anything is written. A full-disk install destroys the Windows partition the files come from, so this is the only moment they can be read. The stage is copied into the target. -- **Installer, hardware setup** — `qcom-firmware-extract --install - --no-rebuild` from `install/hardware/qualcomm/firmware.sh`, using the stage +- **Installer, hardware setup:** `qcom-firmware-extract --install --no-rebuild` + from `install/hardware/qualcomm/firmware.sh`, using the stage (or a Windows partition still on disk). The installer builds the boot image once afterwards. -- **Installed system** — `sudo qcom-firmware-extract` scans the disks again, +- **Installed system:** `sudo qcom-firmware-extract` scans the disks again, or `sudo qcom-firmware-extract -d /path/to/FileRepository` takes any driver store you can mount (a Windows install of the *same model*: the files are tied to the vendor's signing keys). It rebuilds the boot image; reboot diff --git a/pkgbuilds/qcom-firmware-extract/qcom-firmware-extract b/pkgbuilds/qcom-firmware-extract/qcom-firmware-extract index f1c8f9916..70a54093a 100755 --- a/pkgbuilds/qcom-firmware-extract/qcom-firmware-extract +++ b/pkgbuilds/qcom-firmware-extract/qcom-firmware-extract @@ -1,20 +1,7 @@ #!/bin/bash # -# qcom-firmware-extract — copy the vendor-signed Qualcomm firmware a Snapdragon -# laptop needs from the owner's own Windows installation. -# -# Snapdragon X laptops boot Linux with mainline drivers, but the GPU "zap" -# shader and the audio/compute DSP images are signed by the laptop vendor and -# not (yet) redistributable, so linux-firmware cannot ship them. Every file -# the kernel will ask for is named by the device tree (the `firmware-name` -# properties), and every one of them sits in the Windows driver store on the -# same machine. This tool joins the two: it reads the device tree, keeps only -# the names that are missing from /usr/lib/firmware, finds them in the driver -# store and copies them to /usr/lib/firmware/updates/. -# -# Nothing here is model-specific. A machine whose blobs linux-firmware already -# ships gets nothing copied; a machine without a device tree (x86) exits at -# once with nothing to do. +# Copy missing vendor-signed Qualcomm firmware from a Windows driver store. +# Required filenames come from the running device tree. # # Modes # --stage DIR Live ISO, before the disk is touched: scan the Windows @@ -33,23 +20,10 @@ # DriverStore/FileRepository directory, or any directory # tree holding the files) instead of scanning partitions. # -# Files land in /usr/lib/firmware/updates/, never /usr/lib/firmware/: the -# kernel and mkinitcpio search updates/ first and no package owns anything -# there, so the day linux-firmware ships a machine's files pacman does not -# collide with the copies. -# -# RETIRE for a given machine when linux-firmware ships its vendor directory -# (as it already does for Lenovo's and Dell's X1E laptops): the tool finds -# nothing missing and does nothing. When every supported machine is covered, -# drop the package and prune /usr/lib/firmware/updates entries listed in -# /var/lib/omarchy/qcom-firmware/manifest. +# Files land in /usr/lib/firmware/updates to avoid package ownership conflicts. # # Derived from Canonical's qcom-firmware-extract (Tobias Heider, GPL-2+): # https://salsa.debian.org/debian/qcom-firmware-extract -# That tool keys on /proc/device-tree/model with a per-model table, extracts -# a fixed list of file names, and builds a .deb; this one reads the names -# from the device tree and writes the files directly. -# # SPDX-License-Identifier: GPL-2.0-or-later set -euo pipefail @@ -165,10 +139,7 @@ cleanup() { } trap cleanup EXIT -# Mount every NTFS partition read-only and keep the ones that hold a driver -# store. Internal disks first, so a Windows recovery stick does not win over -# the installation the machine actually runs. A BitLocker volume cannot be -# read; it is reported so the user knows why nothing was found. +# Mount Windows driver stores read-only, preferring internal disks. scan_windows_partitions() { local dev fstype tran mp bitlocker="" if ((EUID != 0)); then @@ -198,7 +169,7 @@ scan_windows_partitions() { rmdir "$mp" 2>/dev/null || true fi done - [[ -n $bitlocker ]] && warn "BitLocker volume(s) skipped:$bitlocker — turn BitLocker off in Windows to let the firmware be read" + [[ -n $bitlocker ]] && warn "BitLocker volume(s) skipped:$bitlocker; turn BitLocker off in Windows to let the firmware be read" return 0 } @@ -227,10 +198,7 @@ firmware_sha256() { esac | sha256sum | cut -d' ' -f1 } -# When Windows carries several versions of a board-specific DTB, select the -# one beside the same DSP image linux-firmware already installed. The Yoga -# Slim 7x driver store contains multiple cdsp_dtbs.elf files, and loading a -# DTB signed for a different companion image prevents the CDSP from starting. +# Match ambiguous DTBs to an installed sibling firmware blob from the same node. matching_companion_candidate() { local node=$1 shift @@ -259,9 +227,7 @@ matching_companion_candidate() { return 2 } -# Newest copy of an unambiguous file name anywhere under the sources. When -# several variants exist and an installed sibling identifies the right one, -# require that match instead of guessing by modification time. +# Require a companion match when several firmware variants exist. find_in_sources() { local base=$1 node=$2 selected result candidate newest newest_mtime mtime local -a candidates=() @@ -287,9 +253,7 @@ find_in_sources() { return 1 fi - # No installed companion exists to distinguish driver versions (common on - # machines whose entire vendor firmware set is absent). Preserve the prior - # behavior there and take Windows' newest copy. + # With no installed companion, fall back to the newest copy. newest=${candidates[0]} newest_mtime=$(stat -c %Y "$newest" 2>/dev/null || stat -f %m "$newest") for candidate in "${candidates[@]:1}"; do @@ -369,11 +333,7 @@ for i in "${!missing_names[@]}"; do installed=$((installed + 1)) done -# The GPU zap shader is the one file needed before the root filesystem is -# mounted: the msm driver is in the initramfs (mkinitcpio's kms hook) and -# loads the zap shader the first time the DRM device is opened. The DSP images -# are loaded later from the root filesystem. List only files that exist — -# a FILES entry that does not exist fails every later initramfs build. +# Include existing GPU zap shaders in the initramfs for the early DRM driver. declare -a initramfs_files=() while read -r node name; do [[ $node == zap-shader ]] || continue @@ -389,8 +349,7 @@ done < <(dt_firmware_names) if ((${#initramfs_files[@]})); then mkdir -p "$(dirname "$INITCPIO_CONF")" { - echo "# Generated by qcom-firmware-extract; the GPU zap shader has to be in the" - echo "# initramfs because the DRM driver loads it before the root filesystem is up." + echo "# Generated by qcom-firmware-extract for early DRM firmware loading." printf 'FILES+=(%s)\n' "$(printf '%q ' "${initramfs_files[@]}" | sed 's/ $//')" } >"$INITCPIO_CONF" else diff --git a/pkgbuilds/qcom-firmware-extract/test.sh b/pkgbuilds/qcom-firmware-extract/test.sh index 591ef54d4..3c82a0fc9 100644 --- a/pkgbuilds/qcom-firmware-extract/test.sh +++ b/pkgbuilds/qcom-firmware-extract/test.sh @@ -28,9 +28,7 @@ printf '%s\0%s\0' \ "$firmware_path/qccdsp8380.mbn" \ "$firmware_path/cdsp_dtbs.elf" >"$node/firmware-name" -# linux-firmware carries the main CDSP image but omits its signed DTB. Windows -# retains several driver versions; deliberately make the incompatible one -# newer so a newest-filename policy selects the wrong file. +# Make the incompatible Windows firmware newer than the matching variant. printf 'installed-cdsp' >"$firmware_root/$firmware_path/qccdsp8380.mbn" printf 'other-cdsp' >"$driver_store/wrong/qccdsp8380.mbn" printf 'wrong-dtb' >"$driver_store/wrong/cdsp_dtbs.elf" From 040b8e032ef59b064512322f781dae78f6baf8aa Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Sun, 6 Sep 2026 01:20:40 +0200 Subject: [PATCH 4/6] Keep firmware selection and initramfs updates safe on partial installs --- pkgbuilds/qcom-firmware-extract/PKGBUILD | 7 +- .../qcom-firmware-extract/README.package.md | 7 +- .../qcom-firmware-extract | 114 ++++++++++-------- pkgbuilds/qcom-firmware-extract/test.sh | 58 +++++++++ 4 files changed, 126 insertions(+), 60 deletions(-) diff --git a/pkgbuilds/qcom-firmware-extract/PKGBUILD b/pkgbuilds/qcom-firmware-extract/PKGBUILD index d62de5f97..ec14d188d 100644 --- a/pkgbuilds/qcom-firmware-extract/PKGBUILD +++ b/pkgbuilds/qcom-firmware-extract/PKGBUILD @@ -1,16 +1,15 @@ # Maintainer: Jimmy Van Veen # Copy vendor-signed Qualcomm firmware named by the running device tree. -# The tool exits without changes on machines that do not need it. pkgname=qcom-firmware-extract pkgver=1 -pkgrel=2 +pkgrel=3 pkgdesc="Copy vendor-signed Qualcomm Snapdragon firmware from the machine's Windows installation" arch=('any') -url="https://github.com/omacom-io/omarchy-pkgs" +url="https://github.com/omacom/omarchy-pkgs" license=('GPL-2.0-or-later') -depends=('bash' 'coreutils' 'findutils' 'gzip' 'util-linux' 'xz' 'zstd') +depends=('bash' 'coreutils' 'diffutils' 'findutils' 'gzip' 'util-linux' 'xz' 'zstd') optdepends=('limine-mkinitcpio-hook: rebuild the boot image after installing firmware') options=('!debug') source=( diff --git a/pkgbuilds/qcom-firmware-extract/README.package.md b/pkgbuilds/qcom-firmware-extract/README.package.md index 1fbe160a3..816e0f513 100644 --- a/pkgbuilds/qcom-firmware-extract/README.package.md +++ b/pkgbuilds/qcom-firmware-extract/README.package.md @@ -17,7 +17,8 @@ The device tree names every firmware file the kernel will ask for `Windows/System32/DriverStore/FileRepository` on any NTFS partition it can mount read-only. If Windows carries several variants and linux-firmware has a companion image from the same device-tree node, the sibling image's hash -selects the compatible variant; otherwise the newest copy wins. The result is +selects the compatible variant. Identical duplicates are accepted; differing +variants without a unique companion match are skipped. The result is installed under `/usr/lib/firmware/updates/`. The GPU zap shader is added to the initramfs through `/etc/mkinitcpio.conf.d/qcom-firmware.conf`; the DSP images are loaded from the root filesystem. What was installed, from where, and its @@ -46,8 +47,8 @@ BitLocker volumes cannot be read; turn BitLocker off in Windows first. ## Retirement -When linux-firmware ships a machine's vendor directory the tool finds nothing -missing and does nothing. When every supported machine is covered, drop the +When linux-firmware ships a machine's vendor directory the tool copies nothing +and still configures its GPU firmware for early display. When every supported machine is covered, drop the package and prune the `/usr/lib/firmware/updates` entries listed in the manifest. diff --git a/pkgbuilds/qcom-firmware-extract/qcom-firmware-extract b/pkgbuilds/qcom-firmware-extract/qcom-firmware-extract index 70a54093a..dffc9163a 100755 --- a/pkgbuilds/qcom-firmware-extract/qcom-firmware-extract +++ b/pkgbuilds/qcom-firmware-extract/qcom-firmware-extract @@ -75,8 +75,8 @@ dt_firmware_names() { [[ -d $DT_ROOT ]] || return 0 find "$DT_ROOT" -name firmware-name -type f -print0 2>/dev/null | sort -z | while IFS= read -r -d '' prop; do - node=$(basename "$(dirname "$prop")") - node=${node%%@*} + node=${prop%/firmware-name} + node=${node#"$DT_ROOT"/} tr '\0' '\n' <"$prop" | while IFS= read -r name; do [[ -n $name ]] && printf '%s %s\n' "$node" "$name" done @@ -111,7 +111,7 @@ if [[ ! -d $DT_ROOT ]]; then log "no device tree on this machine, nothing to do" exit 0 fi -if ((${#missing_names[@]} == 0)); then +if [[ $mode != install ]] && ((${#missing_names[@]} == 0)); then log "every firmware file the device tree names is already installed" exit 0 fi @@ -203,67 +203,64 @@ matching_companion_candidate() { local node=$1 shift local -a candidates=("$@") - local dt_node dt_name installed installed_hash candidate sibling - local companion_seen=0 - - while read -r dt_node dt_name; do - [[ $dt_node == "$node" ]] || continue - installed=$(firmware_file "$dt_name") || continue - companion_seen=1 - installed_hash=$(firmware_sha256 "$installed") - - for candidate in "${candidates[@]}"; do + local dt_node dt_name installed installed_hash candidate sibling matched compatible + local -a matches=() + + for candidate in "${candidates[@]}"; do + matched=0 + compatible=1 + while read -r dt_node dt_name; do + [[ $dt_node == "$node" ]] || continue + installed=$(firmware_file "$dt_name") || continue + installed_hash=$(firmware_sha256 "$installed") || return 1 sibling=$(find "${candidate%/*}" -maxdepth 1 -type f \ -iname "$(basename "$dt_name")" -print -quit 2>/dev/null) [[ -n $sibling ]] || continue if [[ $(sha256sum "$sibling" | cut -d' ' -f1) == "$installed_hash" ]]; then - printf '%s\n' "$candidate" - return 0 + matched=1 + else + compatible=0 + break fi - done - done < <(dt_firmware_names) + done < <(dt_firmware_names) + if ((matched && compatible)); then + matches+=("$candidate") + fi + done - ((companion_seen)) && return 1 - return 2 + unique_candidate "${matches[@]}" +} + +# Duplicate copies are safe only when their contents agree. +unique_candidate() { + (($#)) || return 1 + local first=$1 candidate + shift + for candidate in "$@"; do + cmp -s "$first" "$candidate" || return 1 + done + printf '%s\n' "$first" } # Require a companion match when several firmware variants exist. find_in_sources() { - local base=$1 node=$2 selected result candidate newest newest_mtime mtime + local base=$1 node=$2 candidate local -a candidates=() while IFS= read -r -d '' candidate; do candidates+=("$candidate") done < <(find "${sources[@]}" -type f -iname "$base" -print0 2>/dev/null) ((${#candidates[@]})) || return 1 - if ((${#candidates[@]} == 1)); then - printf '%s\n' "${candidates[0]}" + if unique_candidate "${candidates[@]}"; then return 0 fi - if selected=$(matching_companion_candidate "$node" "${candidates[@]}"); then - printf '%s\n' "$selected" + if matching_companion_candidate "$node" "${candidates[@]}"; then return 0 - else - result=$? fi - if ((result == 1)); then - warn "refusing ambiguous $base: no variant matches its installed companion firmware" - return 1 - fi - - # With no installed companion, fall back to the newest copy. - newest=${candidates[0]} - newest_mtime=$(stat -c %Y "$newest" 2>/dev/null || stat -f %m "$newest") - for candidate in "${candidates[@]:1}"; do - mtime=$(stat -c %Y "$candidate" 2>/dev/null || stat -f %m "$candidate") - if ((mtime > newest_mtime)); then - newest=$candidate - newest_mtime=$mtime - fi - done - printf '%s\n' "$newest" + warn "refusing ambiguous $base: no unique variant matches its installed companion firmware" + return 1 } # --------------------------------------------------------------------------- @@ -283,7 +280,9 @@ if [[ $mode == stage ]]; then node=${missing_nodes[$i]} name=${missing_names[$i]} src="" - ((${#sources[@]})) && src=$(find_in_sources "$(basename "$name")" "$node") + if ((${#sources[@]})); then + src=$(find_in_sources "$(basename "$name")" "$node") || true + fi if [[ -z $src ]]; then warn "not found in the driver store: $name" continue @@ -307,7 +306,7 @@ if [[ -n $driverstore ]]; then elif [[ -f "$stage_dir/manifest" ]]; then log "using the firmware staged by the installer in $stage_dir" sources=("$stage_dir") -else +elif ((${#missing_names[@]})); then scan_windows_partitions fi @@ -318,7 +317,9 @@ for i in "${!missing_names[@]}"; do node=${missing_nodes[$i]} name=${missing_names[$i]} src="" - ((${#sources[@]})) && src=$(find_in_sources "$(basename "$name")" "$node") + if ((${#sources[@]})); then + src=$(find_in_sources "$(basename "$name")" "$node") || true + fi if [[ -z $src ]]; then warn "not found: $name" continue @@ -336,7 +337,7 @@ done # Include existing GPU zap shaders in the initramfs for the early DRM driver. declare -a initramfs_files=() while read -r node name; do - [[ $node == zap-shader ]] || continue + [[ ${node##*/} == zap-shader || ${node##*/} == zap-shader@* ]] || continue for dir in "$FW_ROOT/updates" "$FW_ROOT"; do for ext in '' .zst .xz .gz; do if [[ -f "$dir/$name$ext" ]]; then @@ -346,26 +347,33 @@ while read -r node name; do done done done < <(dt_firmware_names) +initramfs_changed=0 if ((${#initramfs_files[@]})); then - mkdir -p "$(dirname "$INITCPIO_CONF")" - { + initramfs_config=$( echo "# Generated by qcom-firmware-extract for early DRM firmware loading." printf 'FILES+=(%s)\n' "$(printf '%q ' "${initramfs_files[@]}" | sed 's/ $//')" - } >"$INITCPIO_CONF" -else + ) + if [[ ! -f $INITCPIO_CONF || $(<"$INITCPIO_CONF") != "$initramfs_config" ]]; then + mkdir -p "$(dirname "$INITCPIO_CONF")" + printf '%s\n' "$initramfs_config" >"$INITCPIO_CONF" + initramfs_changed=1 + fi +elif [[ -f $INITCPIO_CONF ]]; then rm -f "$INITCPIO_CONF" + initramfs_changed=1 fi still_missing=$(( ${#missing_names[@]} - installed )) if ((installed == 0)); then - if ((${#sources[@]} == 0)); then + if ((still_missing > 0 && ${#sources[@]} == 0)); then warn "no Windows installation found to copy from; run again with -d when one is available" fi log "nothing installed; $still_missing firmware file(s) still missing" - exit 0 + ((initramfs_changed)) || exit 0 +else + log "installed $installed firmware file(s), $still_missing still missing" fi -log "installed $installed firmware file(s), $still_missing still missing" if [[ $rebuild == yes ]]; then if command -v limine-update >/dev/null 2>&1; then limine-update diff --git a/pkgbuilds/qcom-firmware-extract/test.sh b/pkgbuilds/qcom-firmware-extract/test.sh index 3c82a0fc9..10606de65 100644 --- a/pkgbuilds/qcom-firmware-extract/test.sh +++ b/pkgbuilds/qcom-firmware-extract/test.sh @@ -48,3 +48,61 @@ QCOM_FW_DT_ROOT="$dt_root" \ } echo "ok - extractor selects an ambiguous DTB by its companion firmware hash" + +run_extractor() { + QCOM_FW_DT_ROOT="$dt_root" \ + QCOM_FW_FIRMWARE_ROOT="$firmware_root" \ + QCOM_FW_ROOT="$scratch/root" \ + PATH="$test_bin:$PATH" \ + bash "$extractor" "$@" +} + +# A different remote processor must not supply the matching companion. +mkdir -p "$dt_root/remoteproc@1" "$driver_store/unrelated" +printf '%s\0' "$firmware_path/qcadsp8380.mbn" >"$dt_root/remoteproc@1/firmware-name" +printf 'installed-adsp' >"$firmware_root/$firmware_path/qcadsp8380.mbn" +printf 'installed-adsp' >"$driver_store/unrelated/qcadsp8380.mbn" +printf 'unrelated-dtb' >"$driver_store/unrelated/cdsp_dtbs.elf" +printf 'missing-cdsp' >"$firmware_root/$firmware_path/qccdsp8380.mbn" +run_extractor --stage "$scratch/unmatched" -d "$driver_store" +[[ ! -e $scratch/unmatched/$firmware_path/cdsp_dtbs.elf ]] +echo "ok - companion matching stays within the exact device-tree node" + +# Firmware not found in Windows must not stop other files from being staged. +printf '%s\0%s\0%s\0' "$firmware_path/not-in-windows.mbn" \ + "$firmware_path/cdsp_dtbs.elf" "$firmware_path/qccdsp8380.mbn" >"$node/firmware-name" +printf 'installed-cdsp' >"$firmware_root/$firmware_path/qccdsp8380.mbn" +run_extractor --stage "$scratch/partial" -d "$driver_store" +[[ $(<"$scratch/partial/$firmware_path/cdsp_dtbs.elf") == matching-dtb ]] +run_extractor --install --no-rebuild --stage-dir "$scratch/partial" +[[ $(<"$firmware_root/updates/$firmware_path/cdsp_dtbs.elf") == matching-dtb ]] +echo "ok - missing firmware does not abort staging or installation" + +# With no companion, only byte-identical duplicates are safe to select. +printf '%s\0' "$firmware_path/duplicate.mbn" >"$node/firmware-name" +printf 'one' >"$driver_store/wrong/duplicate.mbn" +printf 'two' >"$driver_store/matching/duplicate.mbn" +run_extractor --stage "$scratch/ambiguous" -d "$driver_store" +[[ ! -e $scratch/ambiguous/$firmware_path/duplicate.mbn ]] +printf 'one' >"$driver_store/matching/duplicate.mbn" +run_extractor --stage "$scratch/identical" -d "$driver_store" +[[ $(<"$scratch/identical/$firmware_path/duplicate.mbn") == one ]] +echo "ok - differing variants require a unique match" + +# A packaged zap shader still needs an initramfs entry when nothing is missing. +printf '%s\0' "$firmware_path/qccdsp8380.mbn" >"$node/firmware-name" +mkdir -p "$dt_root/gpu@0/zap-shader" +printf '%s\0' "$firmware_path/qcdxkmsuc8380.mbn" >"$dt_root/gpu@0/zap-shader/firmware-name" +printf 'zap' | gzip >"$firmware_root/$firmware_path/qcdxkmsuc8380.mbn.gz" +run_extractor --install --no-rebuild -d "$driver_store" +grep -Fq 'qcdxkmsuc8380.mbn.gz' "$scratch/root/etc/mkinitcpio.conf.d/qcom-firmware.conf" +echo "ok - already installed GPU firmware is included in the initramfs" + +limine-update() { printf 'rebuild\n' >>"$QCOM_FW_ROOT/rebuilds"; } +export -f limine-update +run_extractor --install -d "$driver_store" +[[ ! -e $scratch/root/rebuilds ]] +rm "$scratch/root/etc/mkinitcpio.conf.d/qcom-firmware.conf" +run_extractor --install -d "$driver_store" +[[ $(<"$scratch/root/rebuilds") == rebuild ]] +echo "ok - configuration-only changes rebuild the initramfs once" From e9a082c5fbe1c490a4dcdb4fcf111b49df824805 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Sun, 6 Sep 2026 03:22:15 +0200 Subject: [PATCH 5/6] Run package regression suites in existing CI --- .github/workflows/test.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9fc270f3b..106fe6aac 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,9 +25,16 @@ jobs: -w /workspace \ archlinux:base-devel bash -lc ' set -euo pipefail - pacman -Syu --noconfirm git jq + pacman -Syu --noconfirm git jq python-yaml xz zstd ./bin/sync-upstream self-test ./bin/sync-rebuilds --self-test ./bin/omarchy-pkgs self-test ./bin/omarchy-release self-test + shopt -s nullglob + for test in pkgbuilds/*/test.sh; do + bash "$test" + done + if [[ -d test ]]; then + runuser -u nobody -- python -m unittest discover -s test -v + fi ' From 57a2c444d1515a4a74e1e3e418553f4a0a1e26d4 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Sun, 6 Sep 2026 03:22:15 +0200 Subject: [PATCH 6/6] Match kernel firmware compression and search order --- pkgbuilds/qcom-firmware-extract/PKGBUILD | 4 +- .../qcom-firmware-extract | 47 ++++++------------- pkgbuilds/qcom-firmware-extract/test.sh | 37 +++++++++++++-- 3 files changed, 51 insertions(+), 37 deletions(-) diff --git a/pkgbuilds/qcom-firmware-extract/PKGBUILD b/pkgbuilds/qcom-firmware-extract/PKGBUILD index ec14d188d..f550fa2d0 100644 --- a/pkgbuilds/qcom-firmware-extract/PKGBUILD +++ b/pkgbuilds/qcom-firmware-extract/PKGBUILD @@ -4,12 +4,12 @@ pkgname=qcom-firmware-extract pkgver=1 -pkgrel=3 +pkgrel=4 pkgdesc="Copy vendor-signed Qualcomm Snapdragon firmware from the machine's Windows installation" arch=('any') url="https://github.com/omacom/omarchy-pkgs" license=('GPL-2.0-or-later') -depends=('bash' 'coreutils' 'diffutils' 'findutils' 'gzip' 'util-linux' 'xz' 'zstd') +depends=('bash' 'coreutils' 'diffutils' 'findutils' 'util-linux' 'xz' 'zstd') optdepends=('limine-mkinitcpio-hook: rebuild the boot image after installing firmware') options=('!debug') source=( diff --git a/pkgbuilds/qcom-firmware-extract/qcom-firmware-extract b/pkgbuilds/qcom-firmware-extract/qcom-firmware-extract index dffc9163a..f8ef7fb41 100755 --- a/pkgbuilds/qcom-firmware-extract/qcom-firmware-extract +++ b/pkgbuilds/qcom-firmware-extract/qcom-firmware-extract @@ -6,7 +6,8 @@ # Modes # --stage DIR Live ISO, before the disk is touched: scan the Windows # partitions and save the missing files under DIR (the -# installer copies DIR into the target). Never fails. +# installer copies DIR into the target). Missing firmware +# is reported without aborting staging. # --install Installed system or the installer's chroot: take the # files from the stage (--stage-dir, default # /var/lib/omarchy/firmware-stage), else from a Windows @@ -83,13 +84,16 @@ dt_firmware_names() { done } -# True when the kernel can already load NAME: plain or compressed, from -# updates/ or the packaged tree. -firmware_present() { +# The kernel searches every directory for plain firmware before trying zstd +# and xz. gzip is not an automatic firmware-loader fallback. +firmware_file() { local name=$1 dir ext - for dir in "$FW_ROOT/updates" "$FW_ROOT"; do - for ext in '' .zst .xz .gz; do - [[ -f "$dir/$name$ext" ]] && return 0 + for ext in '' .zst .xz; do + for dir in "$FW_ROOT/updates" "$FW_ROOT"; do + if [[ -f "$dir/$name$ext" ]]; then + printf '%s\n' "$dir/$name$ext" + return 0 + fi done done return 1 @@ -97,7 +101,7 @@ firmware_present() { declare -a missing_nodes=() missing_names=() while read -r node name; do - firmware_present "$name" && continue + firmware_file "$name" >/dev/null && continue missing_nodes+=("$node") missing_names+=("$name") done < <(dt_firmware_names) @@ -173,27 +177,11 @@ scan_windows_partitions() { return 0 } -# Resolve a packaged firmware file, including the compression formats used by -# linux-firmware packages. -firmware_file() { - local name=$1 dir ext - for dir in "$FW_ROOT/updates" "$FW_ROOT"; do - for ext in '' .zst .xz .gz; do - if [[ -f "$dir/$name$ext" ]]; then - printf '%s\n' "$dir/$name$ext" - return 0 - fi - done - done - return 1 -} - firmware_sha256() { local file=$1 case "$file" in *.zst) zstd -qdc -- "$file" ;; *.xz) xz -dc -- "$file" ;; - *.gz) gzip -dc -- "$file" ;; *) cat -- "$file" ;; esac | sha256sum | cut -d' ' -f1 } @@ -338,14 +326,9 @@ done declare -a initramfs_files=() while read -r node name; do [[ ${node##*/} == zap-shader || ${node##*/} == zap-shader@* ]] || continue - for dir in "$FW_ROOT/updates" "$FW_ROOT"; do - for ext in '' .zst .xz .gz; do - if [[ -f "$dir/$name$ext" ]]; then - initramfs_files+=("${dir#"$ROOT"}/$name$ext") - break 2 - fi - done - done + if file=$(firmware_file "$name"); then + initramfs_files+=("${file#"$ROOT"}") + fi done < <(dt_firmware_names) initramfs_changed=0 if ((${#initramfs_files[@]})); then diff --git a/pkgbuilds/qcom-firmware-extract/test.sh b/pkgbuilds/qcom-firmware-extract/test.sh index 10606de65..6d26721c5 100644 --- a/pkgbuilds/qcom-firmware-extract/test.sh +++ b/pkgbuilds/qcom-firmware-extract/test.sh @@ -93,10 +93,41 @@ echo "ok - differing variants require a unique match" printf '%s\0' "$firmware_path/qccdsp8380.mbn" >"$node/firmware-name" mkdir -p "$dt_root/gpu@0/zap-shader" printf '%s\0' "$firmware_path/qcdxkmsuc8380.mbn" >"$dt_root/gpu@0/zap-shader/firmware-name" -printf 'zap' | gzip >"$firmware_root/$firmware_path/qcdxkmsuc8380.mbn.gz" +zap="$firmware_path/qcdxkmsuc8380.mbn" +config="$scratch/root/etc/mkinitcpio.conf.d/qcom-firmware.conf" +for compression in zstd xz; do + if [[ $compression == zstd ]]; then suffix=zst; else suffix=xz; fi + printf 'zap' | "$compression" -c >"$firmware_root/$zap.$suffix" + run_extractor --install --no-rebuild -d "$driver_store" + [[ -z $(run_extractor --list-missing) ]] + grep -Fq "$zap.$suffix" "$config" + rm "$firmware_root/$zap.$suffix" +done +echo "ok - zstd and xz firmware are loadable and included in the initramfs" + +printf 'zap' | gzip >"$firmware_root/$zap.gz" +[[ $(run_extractor --list-missing) == "$zap" ]] +run_extractor --install --no-rebuild -d "$driver_store" +[[ ! -e $config ]] +printf 'zap' >"$driver_store/matching/qcdxkmsuc8380.mbn" +run_extractor --install --no-rebuild -d "$driver_store" +cmp "$driver_store/matching/qcdxkmsuc8380.mbn" "$firmware_root/updates/$zap" +grep -Fq "updates/$zap" "$config" +echo "ok - gzip does not hide missing firmware or prevent extraction" + +# Compressed updates must not take precedence over a plain packaged file. +mv "$firmware_root/updates/$zap" "$firmware_root/$zap" +printf 'update' | zstd -c >"$firmware_root/updates/$zap.zst" +run_extractor --install --no-rebuild -d "$driver_store" +grep -Fq "$firmware_root/$zap" "$config" +if grep -Fq "updates/$zap" "$config"; then + echo "not ok - firmware selection differs from the kernel search order" >&2 + exit 1 +fi +printf 'update' >"$firmware_root/updates/$zap" run_extractor --install --no-rebuild -d "$driver_store" -grep -Fq 'qcdxkmsuc8380.mbn.gz' "$scratch/root/etc/mkinitcpio.conf.d/qcom-firmware.conf" -echo "ok - already installed GPU firmware is included in the initramfs" +grep -Fq "updates/$zap" "$config" +echo "ok - plain firmware is preferred before compressed directory overrides" limine-update() { printf 'rebuild\n' >>"$QCOM_FW_ROOT/rebuilds"; } export -f limine-update