#!/bin/sh
# Third-party drivers: what this PC needs from Debian's non-free archive, and
# installing it -- Windows Update's optional drivers, Ubuntu's "additional
# drivers". Root.
#
#   sg-drivers --list
#       One line per device that wants something:
#       DEVICE <slot>\t<vendor:device>\t<what it is>\t<packages or ->\t<note>
#   sg-drivers --recommended
#       The packages to install, space separated, on one line (empty: none).
#   sg-drivers --install-recommended [--pending]
#       Install them with apt, on the running system: DKMS builds the NVIDIA
#       and Broadcom drivers for the kernel it runs, and a kernel upgrade
#       needs the real boot partitions -- so Setup never does this into the
#       system it is making; sg-drivers.service does it at the first boot.
#       With --pending, only if Setup's "install third-party drivers" was
#       chosen: /etc/stained-glass/drivers.pending (the request, removed once
#       done, kept for the next boot if the archive cannot be reached), which
#       becomes /etc/stained-glass/drivers.auto -- the choice kept: at every
#       boot the PC gets what its hardware asks for, new devices included.
#       With the NVIDIA driver, modprobe.blacklist=nouveau goes on the kernel
#       command line; without it, nouveau stays the driver.
#       Besides the drivers chosen by device, the firmware the kernel asked
#       for and did not find: isenkram (Debian's firmware-to-package tables
#       and AppStream) names the packages. A package the archive does not
#       have is skipped, not fatal. Once firmware is installed, the firmware
#       initrd is made again (sg-firmware-initrd) and the devices that failed
#       to load it -- not the display -- are bound again.
#   sg-drivers --secure-boot-enroll [--root DIR]
#       With Secure Boot on: make this machine's module signing key (MOK) for
#       DKMS, and ask the firmware to enroll it at the next boot. Prints
#       "MOKPASSWORD <password>": the one-time password the enrollment screen
#       asks for. With Secure Boot off, prints nothing and does nothing.
#   sg-drivers --status
#       STATUS pending|none and the drivers packages installed.
#   sg-drivers --install-platform
#       This PC model's own hardware support, on the models that need more
#       than Debian has -- today Microsoft's Surface PCs (DMI sys_vendor
#       "Microsoft Corporation", product name or family "Surface..."; not
#       Hyper-V's "Virtual Machine"), whose touch screen and pen (Intel IPTS)
#       and buttons need the linux-surface project's kernel and its touch
#       daemon (iptsd), plus its libwacom (the pen's and screen's data).
#       Their archive (pkg.surfacelinux.com) is added on such a PC only, its
#       signing key pinned to it alone (Signed-By, the key sg-session ships)
#       and apt pinned so that it can only ever provide those packages --
#       nothing else, never a package Debian has. The stock kernel stays as
#       the fallback: the Surface kernel's boot entry is the default (the
#       newest kernel), with boot counting (kernel/90-sg-boot-tries.install),
#       so if it fails to start three times the boot loader goes back to the
#       stock one by itself. Later Surface kernels come with apt upgrades;
#       the running and the newest Surface kernels are kept, older ones go.
#       Run at every boot by sg-hwsupport.service on Surface PCs only
#       (ConditionFirmware); elsewhere it does nothing and changes nothing.
#       /etc/stained-glass/hwsupport.off: the owner said no.
#   sg-drivers --platform-off [--root DIR]
#       On such a PC, write hwsupport.off (Setup's third-party drivers not
#       chosen; sg-install runs it into the new system). Elsewhere nothing.
#
# NVIDIA: Debian's nvidia-detect chooses the package from the device id
# (nvidia-driver, the Tesla series, or nothing for a card no driver in this
# release supports -- which keeps the open nouveau driver). Firmware for AMD
# and Intel graphics and for Wi-Fi comes from non-free-firmware, by vendor.
#
# For tests: SG_DRIVERS_PCI names a file of "slot vendor device class" lines
# (hex, e.g. "0000:01:00.0 10de 1c82 030000") read instead of the PCI bus;
# SG_DRIVERS_NVIDIA_DETECT names the nvidia-detect to run; SG_SECUREBOOT=1|0
# overrides the firmware's Secure Boot state; SG_DRIVERS_FIRMWARE_LOOKUP
# names the missing-firmware lookup to run (isenkram-autoinstall-firmware
# -l: the packages, on one line); SG_DRIVERS_ETC is /etc/stained-glass;
# SG_DRIVERS_DMI is /sys/class/dmi/id (a directory of sys_vendor,
# product_name, product_family files); SG_DRIVERS_APT is /etc/apt (where the
# Surface archive's source and pin go); SG_DRIVERS_KEYRING is the Surface
# archive key sg-session ships; SG_DRIVERS_BOOT the boot partition (for its
# free space); SG_DRIVERS_UNAME the running kernel's version.
#
# SPDX-License-Identifier: AGPL-3.0-or-later
set -eu
PATH="$PATH:/usr/sbin:/sbin"
ROOT="" MODE="" PENDING=0
EFIVARS=/sys/firmware/efi/efivars
SB_VAR="$EFIVARS/SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c"

die() { printf 'sg-drivers: %s\n' "$*" >&2; exit 1; }
say() { printf 'sg-drivers: %s\n' "$*" >&2; }

while [ $# -gt 0 ]; do
    case "$1" in
        --list|--recommended|--install-recommended|--install-platform|--platform-off|--secure-boot-enroll|--status) MODE=${1#--} ;;
        --root) ROOT=${2:?}; shift ;;
        --pending) PENDING=1 ;;
        *) die "unknown argument: $1" ;;
    esac
    shift
done
[ -n "$MODE" ] || die "usage: sg-drivers --list | --recommended | --install-recommended [--pending] | --install-platform | --secure-boot-enroll [--root DIR] | --status"
[ -z "$ROOT" ] || [ -d "$ROOT/etc" ] || die "$ROOT is not a system root"
ETC=${SG_DRIVERS_ETC:-$ROOT/etc/stained-glass}
PENDING_FILE="$ETC/drivers.pending"
AUTO_FILE="$ETC/drivers.auto"

# Run a command in the system being set up (chrooted when --root: reading
# only, and making the Secure Boot key).
in_root() { if [ -n "$ROOT" ]; then chroot "$ROOT" "$@"; else "$@"; fi; }
# shellcheck disable=SC2016  # ${Status} is dpkg-query's, not the shell's
installed() { in_root dpkg-query -W -f '${Status}' "$1" 2>/dev/null | grep -q 'install ok installed'; }

# --- the devices ---------------------------------------------------------------
pci_devices() {
    if [ -n "${SG_DRIVERS_PCI:-}" ]; then grep -v '^#' "$SG_DRIVERS_PCI" | tr '[:upper:]' '[:lower:]'; return; fi
    for d in /sys/bus/pci/devices/*; do
        [ -r "$d/vendor" ] || continue
        printf '%s %s %s %s\n' "${d##*/}" "$(sed 's/^0x//' "$d/vendor")" "$(sed 's/^0x//' "$d/device")" \
            "$(sed 's/^0x//' "$d/class")"
    done
}

# Broadcom Wi-Fi chips that only Broadcom's own driver (wl) runs; the rest
# use brcmfmac with firmware (Debian's wiki, "wl").
BROADCOM_STA="4311 4312 4313 4315 4327 4328 4329 432a 432b 432c 432d 4331 4353 4357 4358 4359 4360 43a0 43b1 a8d8 a99d"
KERNEL_PAIR="linux-image-amd64 linux-headers-amd64"

nvidia_package() {   # device id -> the package nvidia-detect recommends, "" for none, "?" unknown
    if [ -n "${SG_DRIVERS_NVIDIA_DETECT:-}" ]; then out=$("$SG_DRIVERS_NVIDIA_DETECT" "10de$1" 2>/dev/null || :)
    elif [ -x "$ROOT/usr/bin/nvidia-detect" ]; then out=$(in_root /usr/bin/nvidia-detect "10de$1" 2>/dev/null || :)
    else echo "?"; return; fi
    printf '%s\n' "$out" | awk 'found { print $1; exit } /It is recommended to install the/ { found = 1 }'
}

# DEVICE lines, as --list prints them.
survey() {
    pci_devices | while read -r slot vendor device class; do
        base=$(printf '%s' "$class" | cut -c1-2); sub=$(printf '%s' "$class" | cut -c1-4)
        what="" pkgs="" note=""
        case "$vendor:$base" in
            10de:03)
                what="NVIDIA graphics"
                p=$(nvidia_package "$device")
                case "$p" in
                    "?") pkgs="nvidia-detect"; note="the driver is chosen once Debian's nvidia-detect is installed" ;;
                    "") pkgs="-"; note="no NVIDIA driver in this Debian release supports it: the open-source driver (nouveau) is used" ;;
                    *) pkgs="$p firmware-misc-nonfree $KERNEL_PAIR"; note="NVIDIA's driver, built for this kernel by DKMS" ;;
                esac ;;
            1002:03) what="AMD graphics"; pkgs="firmware-amd-graphics"; note="firmware" ;;
            8086:03) what="Intel graphics"; pkgs="firmware-intel-graphics"; note="firmware" ;;
            *)
                case "$vendor:$sub" in
                    8086:0280) what="Intel Wi-Fi"; pkgs="firmware-iwlwifi"; note="firmware" ;;
                    10ec:0280|10ec:0200) what="Realtek network"; pkgs="firmware-realtek"; note="firmware" ;;
                    168c:0280|17cb:0280) what="Qualcomm Atheros Wi-Fi"; pkgs="firmware-atheros"; note="firmware" ;;
                    14c3:0280) what="MediaTek Wi-Fi"; pkgs="firmware-mediatek"; note="firmware" ;;
                    14e4:0280)
                        what="Broadcom Wi-Fi"
                        case " $BROADCOM_STA " in
                            *" $device "*) pkgs="broadcom-sta-dkms $KERNEL_PAIR"; note="Broadcom's driver, built by DKMS" ;;
                            *) pkgs="firmware-brcm80211"; note="firmware" ;;
                        esac ;;
                esac ;;
        esac
        [ -n "$what" ] || continue
        printf 'DEVICE %s\t%s:%s\t%s\t%s\t%s\n' "$slot" "$vendor" "$device" "$what" "${pkgs:--}" "$note"
    done
    platform_survey
}

# --- this PC model: hardware support beyond Debian's ---------------------------
DMI=${SG_DRIVERS_DMI:-/sys/class/dmi/id}
APT_DIR=${SG_DRIVERS_APT:-$ROOT/etc/apt}
SURFACE_KEYRING=${SG_DRIVERS_KEYRING:-/usr/share/stained-glass/keyrings/linux-surface.gpg}
SURFACE_SOURCES="$APT_DIR/sources.list.d/sg-linux-surface.sources"
SURFACE_PIN="$APT_DIR/preferences.d/sg-linux-surface.pref"
# The archive's Release "Origin": the pin holds to it.
SURFACE_ORIGIN=com.github.linux-surface.linux-surface
# What that archive may provide; everything else of it is pinned to -1 (never).
SURFACE_ALLOWED='/^(linux-(image|headers)-(surface|[0-9][0-9.]*-surface-[0-9]+)|iptsd|libwacom[0-9]*(-[a-z]+)?-surface)$/'
# Room the boot partition needs for one more kernel and its initrd.
BOOT_ROOM_MB=${SG_DRIVERS_BOOT_ROOM_MB:-250}
KERNEL_ETC=${SG_DRIVERS_KERNEL_ETC:-$ROOT/etc/kernel}

dmi() { tr -d '\n' < "$DMI/$1" 2>/dev/null || :; }

# "surface<TAB>its product name" on a Surface PC; nothing elsewhere.
platform() {
    [ "$(dmi sys_vendor)" = "Microsoft Corporation" ] || return 0
    name=$(dmi product_name)
    case "$name" in
        Surface*) ;;
        *) case "$(dmi product_family)" in Surface*) ;; *) return 0 ;; esac ;;
    esac
    printf 'surface\t%s\n' "$name"
}

surface_packages() {
    p="linux-image-surface iptsd libwacom-surface"
    # a DKMS module (a driver chosen above) is built for the Surface kernel too
    if installed dkms; then p="$p linux-headers-surface"; fi
    printf '%s\n' "$p"
}

platform_survey() {
    platform | while IFS="$(printf '\t')" read -r kind name; do
        case "$name" in Microsoft*) ;; *) name="Microsoft $name" ;; esac
        case "$kind" in surface)
            printf 'DEVICE platform\tdmi:surface\t%s touch screen and pen\t%s\t%s\n' "$name" \
                "$(surface_packages)" "the linux-surface project's kernel and touch daemon; the stock kernel stays as the fallback" ;;
        esac
    done
}

# The Surface archive: its source (its own key only) and the pin that keeps
# it to the Surface packages. Written on a Surface PC only.
surface_archive() {
    [ -s "$SURFACE_KEYRING" ] || die "the Surface archive's key is missing ($SURFACE_KEYRING)"
    mkdir -p "$APT_DIR/sources.list.d" "$APT_DIR/preferences.d"
    cat > "$SURFACE_SOURCES.tmp" <<EOS
# Microsoft Surface hardware support (sg-drivers, on Surface PCs only): the
# linux-surface project's kernel, touch daemon and libwacom. Its key vouches
# for this archive alone, and sg-linux-surface.pref lets it provide only
# those packages.
Types: deb
URIs: https://pkg.surfacelinux.com/debian
Suites: release
Components: main
Signed-By: $SURFACE_KEYRING
EOS
    cat > "$SURFACE_PIN.tmp" <<EOS
# The linux-surface archive (sg-drivers) may provide its kernel, iptsd and
# libwacom -- and nothing else, whatever it carries.
Package: $SURFACE_ALLOWED
Pin: release o=$SURFACE_ORIGIN
Pin-Priority: 500

Package: *
Pin: release o=$SURFACE_ORIGIN
Pin-Priority: -1
EOS
    mv -f "$SURFACE_PIN.tmp" "$SURFACE_PIN"
    mv -f "$SURFACE_SOURCES.tmp" "$SURFACE_SOURCES"
}

boot_path() {
    if [ -n "${SG_DRIVERS_BOOT:-}" ]; then echo "$SG_DRIVERS_BOOT"; return; fi
    # the boot partition kernel-install writes to (sg-boot-layout: /efi or /xbootldr)
    _br=$(sed -n 's/^BOOT_ROOT=//p' /etc/kernel/install.conf 2>/dev/null | tail -1)
    if [ -n "$_br" ] && ls "$_br/loader/entries" >/dev/null 2>&1; then echo "$_br"; return; fi
    ls /boot /efi >/dev/null 2>&1 || :   # automounts
    bootctl --print-boot-path 2>/dev/null || bootctl --print-esp-path 2>/dev/null
}

# Older Surface kernels go: the running one and the newest stay.
surface_kernels_tidy() {
    running=${SG_DRIVERS_UNAME:-$(uname -r)}
    # shellcheck disable=SC2016  # dpkg-query's fields
    all=$(in_root dpkg-query -W -f '${Package} ${Status}\n' 'linux-image-*-surface-*' 2>/dev/null \
        | awk '/install ok installed/ { sub(/^linux-image-/, "", $1); print $1 }' | sort -V)
    newest=$(printf '%s\n' "$all" | tail -1)
    old=""
    for k in $all; do
        [ "$k" = "$newest" ] || [ "$k" = "$running" ] || old="$old linux-image-$k"
    done
    [ -n "$old" ] || return 0
    say "older Surface kernels removed:$old"
    # shellcheck disable=SC2086  # package names
    in_root apt-get -q -y -o DPkg::Lock::Timeout=600 purge $old >/dev/null || say "could not remove:$old"
}

install_platform() {
    [ -z "$ROOT" ] || die "hardware support is installed on the running system, not into --root"
    p=$(platform)
    [ -n "$p" ] || exit 0
    if [ -f "$ETC/hwsupport.off" ]; then say "this PC's hardware support was turned off ($ETC/hwsupport.off)"; exit 0; fi
    want=""
    for pkg in $(surface_packages); do installed "$pkg" || want="$want $pkg"; done
    want=${want# }
    if [ -z "$want" ]; then
        surface_kernels_tidy
        exit 0
    fi
    case " $want " in *" linux-image-surface "*)
        b=$(boot_path || :)
        if [ -n "$b" ] && [ -d "$b" ]; then
            free=$(df -P -m "$b" | awk 'NR == 2 { print $4 }')
            if [ "${free:-0}" -lt "$BOOT_ROOM_MB" ]; then
                say "the boot partition $b has ${free} MB free; the Surface kernel needs ${BOOT_ROOM_MB} MB: not installed"
                exit 0
            fi
        fi ;;
    esac
    surface_archive
    # kernel-install must give the new kernel a boot entry of its own (the
    # boot loader's layout, "bls"). It infers that from loader/entries.srel or
    # a directory named after the entry token, and a machine Setup installed
    # has neither: the kernel would be installed and never started.
    # (and its tries: sg-kernel-entries, as on every machine since 0.1.0-119)
    SG_KE_ETC="$KERNEL_ETC" "${SG_DRIVERS_KERNEL_ENTRIES:-/usr/bin/sg-kernel-entries}" --configure \
        || die "cannot set up kernel-install for boot entries"
    export DEBIAN_FRONTEND=noninteractive
    in_root apt-get -q -o DPkg::Lock::Timeout=600 update >/dev/null || die "cannot reach the package archives"
    say "installing this Surface's hardware support: $want"
    # shellcheck disable=SC2086  # package names
    in_root apt-get -q -y -o DPkg::Lock::Timeout=600 install $want || die "could not install: $want"
    # the new kernel is used from the next start
    { echo "*** System restart required: Surface touch and pen support ***" > /run/reboot-required; } 2>/dev/null || :
    say "installed: $want (from the next restart)"
}

# The packages the kernel's missing firmware is in (isenkram), one per line.
firmware_packages() {
    look=${SG_DRIVERS_FIRMWARE_LOOKUP:-/usr/sbin/isenkram-autoinstall-firmware}
    [ -x "$look" ] && [ -z "$ROOT" ] || return 0
    "$look" -l 2>/dev/null | tr ' ' '\n' | grep -E '^[a-z0-9][a-z0-9.+-]+$' || :
}

# The packages still to install, one line.
recommended() {
    # (the PC model's own support has its own installer: --install-platform)
    { survey | grep -v '^DEVICE platform' | cut -f4 | tr ' ' '\n'; firmware_packages; } \
        | grep -v '^-\?$' | sort -u | while read -r p; do
        installed "$p" || printf '%s\n' "$p"
    done | tr '\n' ' ' | sed 's/ $//'
}

# whether the archive has PKG (apt's candidate)
has_candidate() {
    c=$(LC_ALL=C in_root apt-cache policy "$1" 2>/dev/null | awk '/Candidate:/ { print $2; exit }')
    [ -n "$c" ] && [ "$c" != "(none)" ]
}

# After new firmware: the devices whose drivers failed to load theirs this
# boot, bound again (PCI and USB; never the display, which the session is on).
rebind_failed() {
    { journalctl -k -b -o cat --no-pager 2>/dev/null || dmesg 2>/dev/null; } \
        | sed -n 's/^\([^ ]*\) \([^ ]*\): .*\(firmware: failed to load\|Direct firmware load for\).*/\1 \2/p' \
        | sort -u | while read -r _drv dev; do
        for bus in pci usb; do
            d="/sys/bus/$bus/devices/$dev"
            [ -e "$d/driver" ] || continue
            case "$(cat "$d/class" 2>/dev/null)" in 0x03*) continue ;; esac
            drv=$(readlink -f "$d/driver")
            say "binding $dev ($_drv) again for its new firmware"
            { echo "$dev" > "$drv/unbind" && echo "$dev" > "$drv/bind"; } 2>/dev/null || say "could not bind $dev again"
        done
    done
}

secure_boot_on() {
    case "${SG_SECUREBOOT:-}" in 1) return 0 ;; 0) return 1 ;; esac
    [ -r "$SB_VAR" ] || return 1
    [ "$(od -An -t u1 -j4 -N1 "$SB_VAR" | tr -d ' ')" = 1 ]
}

# modprobe.blacklist=nouveau on this system's kernel command line: its boot
# entries (those naming this root) and /etc/kernel/cmdline for later kernels.
# Debian's package blacklists it in modprobe.d; the command line covers an
# initrd built before the driver came.
keep_nouveau_away() {
    cl="$ROOT/etc/kernel/cmdline"
    [ -f "$cl" ] || return 0
    grep -q 'modprobe.blacklist=nouveau' "$cl" || sed -i 's/$/ modprobe.blacklist=nouveau/' "$cl"
    root_id=$(grep -o 'root=PARTUUID=[^ ]*' "$cl" || :)
    [ -n "$root_id" ] || return 0
    ls /boot /efi /xbootldr >/dev/null 2>&1 || :
    for dir in ${SG_DRIVERS_ENTRIES:-/boot/loader/entries /efi/loader/entries /xbootldr/loader/entries}; do
      for e in "$dir"/*.conf; do
        [ -f "$e" ] || continue
        grep -q "^options .*$root_id" "$e" || continue
        grep -q 'modprobe.blacklist=nouveau' "$e" || sed -i 's/^\(options .*\)$/\1 modprobe.blacklist=nouveau/' "$e"
      done
    done
}

case "$MODE" in
list) survey ;;
install-platform) install_platform ;;
platform-off)
    # Setup's third-party drivers not chosen: on a PC with its own support
    # (a Surface), that support is off too (sg-install, into --root)
    [ -n "$(platform)" ] || exit 0
    mkdir -p "$ETC"
    printf 'Setup: third-party drivers were not chosen\n' > "$ETC/hwsupport.off" ;;
recommended) recommended; echo ;;
status)
    if [ -f "$PENDING_FILE" ]; then echo "STATUS pending"; else echo "STATUS none"; fi
    for p in $(survey | cut -f4 | tr ' ' '\n' | grep -v '^-\?$' | sort -u); do
        installed "$p" && printf 'INSTALLED %s\n' "$p"
    done
    ;;
secure-boot-enroll)
    secure_boot_on || exit 0
    command -v mokutil >/dev/null || die "mokutil is not installed"
    command -v openssl >/dev/null || die "openssl is not installed"
    d="$ROOT/var/lib/dkms"
    mkdir -p "$d" "$ROOT/etc/dkms/framework.conf.d"
    chmod 755 "$d"
    host=$(cat "$ROOT/etc/hostname" 2>/dev/null || echo stained-glass)
    if [ ! -s "$d/mok.key" ]; then
        # This machine's own key, made here and never anywhere else: DKMS
        # signs the modules it builds with it, and the firmware trusts it once
        # the owner has enrolled it.
        (umask 077; openssl req -new -x509 -nodes -newkey rsa:2048 -days 36500 -outform DER \
            -subj "/CN=Stained Glass OS $host DKMS module signing/" \
            -keyout "$d/mok.key" -out "$d/mok.pub" >/dev/null 2>&1) || die "cannot make the module signing key"
        chmod 600 "$d/mok.key"; chmod 644 "$d/mok.pub"
    fi
    printf 'mok_signing_key="/var/lib/dkms/mok.key"\nmok_certificate="/var/lib/dkms/mok.pub"\n' \
        > "$ROOT/etc/dkms/framework.conf.d/50-stained-glass-mok.conf"
    # A one-time password for the enrollment screen: digits, so it types the
    # same on any keyboard layout.
    pw=$(od -An -N8 -tu1 /dev/urandom | tr -s ' ' '\n' | grep . | awk '{ printf "%d", $1 % 10 }')
    hash=$(mktemp)
    mokutil --generate-hash="$pw" > "$hash" 2>/dev/null || { rm -f "$hash"; die "cannot make the enrollment password"; }
    mokutil --import "$d/mok.pub" --hash-file "$hash" >/dev/null 2>&1 || { rm -f "$hash"; die "the firmware did not accept the key for enrollment"; }
    rm -f "$hash"
    printf 'MOKPASSWORD %s\n' "$pw"
    ;;
install-recommended)
    [ -z "$ROOT" ] || die "drivers are installed on the running system, not into --root"
    if [ "$PENDING" = 1 ] && [ ! -f "$PENDING_FILE" ] && [ ! -f "$AUTO_FILE" ]; then exit 0; fi
    # Setup's choice is kept: from now on, at every boot
    if [ -f "$PENDING_FILE" ] && [ ! -f "$AUTO_FILE" ]; then
        printf 'Install the drivers and firmware this PC needs (Setup)\n' > "$AUTO_FILE"
    fi
    export DEBIAN_FRONTEND=noninteractive
    in_root apt-get -q update >/dev/null || die "cannot reach the package archive"
    if survey | cut -f4 | grep -qw nvidia-detect; then
        in_root apt-get -q -y install nvidia-detect >/dev/null || die "cannot install nvidia-detect"
    fi
    want=$(recommended)
    pkgs=""
    for p in $want; do
        if has_candidate "$p"; then pkgs="$pkgs $p"; else say "$p is not in the archive: skipped"; fi
    done
    pkgs=${pkgs# }
    if [ -z "$pkgs" ]; then
        say "this PC needs no third-party drivers or firmware"
    else
        say "installing: $pkgs"
        # shellcheck disable=SC2086  # a list of package names
        if ! in_root apt-get -q -y install $pkgs; then
            # one that cannot be installed does not stop the others
            failed=""
            for p in $pkgs; do in_root apt-get -q -y install "$p" || failed="$failed $p"; done
            [ -z "$failed" ] || say "could not install:$failed"
        fi
        case " $pkgs " in *" nvidia-"*"-driver "*|*" nvidia-driver "*) keep_nouveau_away ;; esac
        say "installed: $pkgs"
        [ ! -x /usr/bin/sg-firmware-initrd ] || /usr/bin/sg-firmware-initrd update || :
        rebind_failed
    fi
    rm -f "$PENDING_FILE"
    ;;
esac
