Files
omarchy-pkgs/pkgbuilds/qcom-firmware-extract/qcom-firmware-extract
T
9ea749835f Add safe Windows firmware extraction for Snapdragon laptops (#221)
* 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.

* Select matching Qualcomm firmware variants

* Trim firmware extractor comments

* Keep firmware selection and initramfs updates safe on partial installs

* Run package regression suites in existing CI

* Match kernel firmware compression and search order

* Limit the CI change to the package test scripts

* Match firmware names exactly when updating the manifest

* Count compressed firmware only when the kernel can load it

* Scope the README's firmware claim to this package

* Find DSP device-tree images under their Windows names

The Surface Pro 11 device tree asks for adsp_dtb.mbn and cdsp_dtb.mbn,
but Windows ships them as adsp_dtbs.elf and cdsp_dtbs.elf, so
extraction left both DSP device-tree images missing. Look for the
Windows name when the device-tree name is not found. Exact names still
take precedence, and the existing companion matching still selects
between variants.

Validated with test.sh and on a Surface Pro 11 against its own Windows
driver store: 5 of 5 firmware files staged, up from 3, and the ADSP and
CDSP boot with the device-tree images taken from the .elf files.

* Let later firmware sources fill in what a stage lacks

Any stage manifest, even an empty one, made --stage exit as already
staged and made --install use only the stage, so Windows was never read
again. A --stage rerun now keeps the staged files and looks for the
rest, and --install scans Windows only for what the stage lacks. -d
still replaces both. A rerun without root, which cannot read Windows,
keeps the stage and succeeds as before. Each run reads the stage
manifest once.

Mount points honour QCOM_FW_ROOT so the tests can stub the partition
scan. Both root checks now take the user ID from QCOM_FW_TEST_EUID when
the tests set it, and the install-mode check no longer skips when
QCOM_FW_ROOT is set.

* Let board packages list firmware that must not come from Windows

Some device trees name firmware that should not be copied from Windows:
files a board package produces or holds back until they are tested, and
files the machine's boot firmware loads before Linux, which Windows
drivers do not carry.
On the Surface Laptop 8 these are its USB4 router image and SOCCP pair.

A board package can now list such names in
/usr/share/qcom-firmware-extract/provided.d/<compatible>.list. On a
machine with that compatible they are never searched for, installed or
reported by --list-missing, and the log names the list for each one. An
unreadable list or an entry containing a space is skipped with a
warning.

A copy an earlier --install made of a newly listed name stays in place:
the same bytes would not show that the file is still this tool's copy,
since a board's own tool may keep the same file there. The next
--install stops recording it and prints where it is, for the board
package or the user to remove.

Since --list-missing omits listed names, a board must not list firmware
that a hardware check relies on unless its package installs it before
the check runs.

* Find staged firmware by file name when its device-tree path changed

Since the stage became the first source, --install takes a staged file
only by its exact device-tree path. After a full-disk install the stage
is the only copy, so a device tree that files an image under another
path could no longer be served from it. After Windows, fall back to a
staged file with the same name that the stage manifest lists, but not
when Windows refused differing variants of it as ambiguous.

* Test that listed firmware never sends the extractor to the disks

With every name installed or listed, --list-missing prints nothing,
--stage and --install neither list nor mount partitions, and listed
names are not installed from Windows or the stage. A control run
without the list does scan. Listing the names again leaves the copies
it installed in place but stops recording them, and another missing
file still sends the extractor to Windows for that file alone.

* Read only internal disks from an installed system

--install mounts the Windows partitions whenever the stage lacks a
file, which after a full-disk install can be every later run, and it
mounted every NTFS partition, USB disks included. It now reads only
partitions on disks that lsblk reports as neither removable, hotplug
nor USB. --stage in the live session still reads all of them, internal
disks first, and -d takes a driver store from anywhere.

---------

Co-authored-by: Jimmy Van Veen <jimmy.van.veen@gmail.com>
Co-authored-by: Brandon Williams <turbine.bmw@gmail.com>
Co-authored-by: Jim Martin <jimmartin@gmail.com>
2026-10-09 10:13:34 -04:00

580 lines
20 KiB
Bash
Executable File

#!/bin/bash
#
# 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
# partitions and save the missing files under DIR (the
# installer copies DIR into the target). Missing firmware
# is reported without aborting staging. A rerun adds the
# files earlier runs did not find.
# --install Installed system or the installer's chroot: take each
# file from the stage (--stage-dir, default
# /var/lib/omarchy/firmware-stage), else from a Windows
# partition still on an internal 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 and not listed for this board (below),
# 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 the Windows
# partitions. --install then ignores the stage.
#
# Files land in /usr/lib/firmware/updates to avoid package ownership conflicts.
#
# A board package lists named firmware that must not come from Windows on its
# board, one name per line, in /usr/share/qcom-firmware-extract/provided.d/
# <device-tree compatible>.list: files the package handles itself, and files
# the machine's own boot firmware loads before Linux starts. Names listed
# for one of this machine's compatibles are never searched for, installed or
# reported missing. When nothing else is missing, the disks are not touched.
# A copy that an earlier run installed before the name was listed is left in
# place; the next --install stops recording it and prints where it is. Since
# --list-missing leaves them out, a board must not list firmware that a
# hardware check relies on, such as the ADSP image Omarchy checks before it
# enables the DSP driver, unless its package installs that file before the
# check runs.
#
# Derived from Canonical's qcom-firmware-extract (Tobias Heider, GPL-2+):
# https://salsa.debian.org/debian/qcom-firmware-extract
# 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}
KERNEL_CONFIG=${QCOM_FW_KERNEL_CONFIG:-/proc/config.gz}
PROVIDED_DIR=${QCOM_FW_PROVIDED_DIR:-/usr/share/qcom-firmware-extract/provided.d}
# Destination prefix for the installed-system and mount paths (tests only).
ROOT=${QCOM_FW_ROOT:-}
# The effective user ID the root checks see (tests only).
CHECKED_EUID=${QCOM_FW_TEST_EUID:-$EUID}
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 "<node-name> <firmware path>" 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=${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
done
}
# The kernel tries plain firmware in every directory, then zstd and xz if built
# with them. gzip is not an automatic firmware-loader fallback.
firmware_suffixes=('' .zst .xz)
if kernel_config=$(gzip -dc -- "$KERNEL_CONFIG" 2>/dev/null); then
firmware_suffixes=('')
if grep -qx 'CONFIG_FW_LOADER_COMPRESS_ZSTD=y' <<<"$kernel_config"; then
firmware_suffixes+=(.zst)
fi
if grep -qx 'CONFIG_FW_LOADER_COMPRESS_XZ=y' <<<"$kernel_config"; then
firmware_suffixes+=(.xz)
fi
fi
firmware_file() {
local name=$1 dir ext
for ext in "${firmware_suffixes[@]}"; 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
}
# Names that board packages list for this machine as not from Windows.
declare -A provided=()
load_provided_names() {
local compatible list line
[[ -r $DT_ROOT/compatible && -d $PROVIDED_DIR ]] || return 0
while IFS= read -r compatible; do
[[ -n $compatible && $compatible != */* ]] || continue
list="$PROVIDED_DIR/$compatible.list"
[[ -f $list ]] || continue
if [[ ! -r $list ]]; then
warn "ignoring $list: it cannot be read"
continue
fi
while IFS= read -r line || [[ -n $line ]]; do
# A comment starts at a '#' that begins the line or follows a space.
line=" $line"
line=${line%%[[:space:]]#*}
line=${line#"${line%%[![:space:]]*}"}
line=${line%"${line##*[![:space:]]}"}
[[ -n $line ]] || continue
if [[ $line == *[[:space:]]* ]]; then
warn "ignoring '$line' in $list: a firmware name has no spaces"
continue
fi
provided[$line]=$compatible
done <"$list"
done < <(tr '\0' '\n' <"$DT_ROOT/compatible")
return 0
}
load_provided_names
declare -a missing_nodes=() missing_names=() provided_names=()
while read -r node name; do
if [[ -n ${provided[$name]-} ]]; then
provided_names+=("$name")
continue
fi
firmware_file "$name" >/dev/null && 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
for name in "${provided_names[@]}"; do
log "skipping $name: listed in $PROVIDED_DIR/${provided[$name]}.list"
done
if [[ $mode != install ]] && ((${#missing_names[@]} == 0)); then
log "every firmware file the device tree names is already installed${provided_names[*]:+ or listed for this board}"
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 [[ $CHECKED_EUID != 0 && $mode == install ]]; then
warn "must run as root"
exit 1
fi
# ---------------------------------------------------------------------------
# 2. Where can they come from?
# ---------------------------------------------------------------------------
declare -a sources=() # driver stores to search together (-d or Windows)
declare -a mounts=() # temporary mounts to undo
stage_source="" # the installer's stage, used before the driver stores
declare -A manifest=() # the manifest this run updates, lines by firmware name
declare -A staged=() # names the stage manifest lists
cleanup() {
local m
for m in "${mounts[@]-}"; do
[[ -n $m ]] || continue
umount "$m" 2>/dev/null || true
rmdir "$m" 2>/dev/null || true
done
}
trap cleanup EXIT
# Mount Windows driver stores read-only, preferring internal disks. With
# "internal-only", removable and external disks are left alone.
scan_windows_partitions() {
local scope=${1:-all} dev removable hotplug fstype tran mp bitlocker="" skipped=""
if [[ $CHECKED_EUID != 0 ]]; then
warn "must run as root to read the Windows partitions"
exit 1
fi
local -a internal=() external=()
# read merges empty columns, so the always-set RM and HOTPLUG come first
# and an empty TRAN comes last.
while read -r dev removable hotplug fstype tran; do
[[ $fstype == ntfs || $fstype == BitLocker ]] || continue
if [[ $removable == 0 && $hotplug == 0 && $tran != usb ]]; then
if [[ $fstype == ntfs ]]; then internal+=("$dev"); else bitlocker+=" $dev"; fi
elif [[ $scope == internal-only ]]; then
skipped+=" $dev"
elif [[ $fstype == ntfs ]]; then
external+=("$dev")
else
bitlocker+=" $dev"
fi
done < <(lsblk -rpno PATH,RM,HOTPLUG,FSTYPE,TRAN 2>/dev/null)
[[ -z $skipped ]] || log "not reading removable or external disk partition(s):$skipped; use -d for a driver store on one"
for dev in "${internal[@]}" "${external[@]}"; do
mp=$(mktemp -d "$ROOT/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
}
firmware_sha256() {
local file=$1
case "$file" in
*.zst) zstd -qdc -- "$file" ;;
*.xz) xz -dc -- "$file" ;;
*) cat -- "$file" ;;
esac | sha256sum | cut -d' ' -f1
}
# Match ambiguous DTBs to an installed sibling firmware blob from the same node.
matching_companion_candidate() {
local node=$1
shift
local -a candidates=("$@")
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
matched=1
else
compatible=0
break
fi
done < <(dt_firmware_names)
if ((matched && compatible)); then
matches+=("$candidate")
fi
done
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"
}
# Windows can ship DSP device-tree images under another name: a device tree
# may ask for adsp_dtb.mbn where Windows has adsp_dtbs.elf (e.g. Surface Pro 11).
windows_firmware_names() {
printf '%s\n' "$1"
if [[ $1 == *_dtb.mbn ]]; then
printf '%s\n' "${1%_dtb.mbn}_dtbs.elf"
fi
}
# Require a companion match when several firmware variants exist. Returns 1
# when the sources have no candidate and 2 when it refuses ambiguous ones.
find_in_sources() {
local base=$1 node=$2 candidate name
local -a candidates=()
while IFS= read -r name; do
while IFS= read -r -d '' candidate; do
candidates+=("$candidate")
done < <(find "${sources[@]}" -type f -iname "$name" -print0 2>/dev/null)
((${#candidates[@]})) && break
done < <(windows_firmware_names "$base")
((${#candidates[@]})) || return 1
if unique_candidate "${candidates[@]}"; then
return 0
fi
if matching_companion_candidate "$node" "${candidates[@]}"; then
return 0
fi
warn "refusing ambiguous $base: no unique variant matches its installed companion firmware"
return 2
}
# A manifest has one line per firmware name: the name, the SHA-256 of the file
# and where it came from. Loads FILE as the manifest this run updates.
read_manifest() {
local line name
manifest=()
while IFS= read -r line || [[ -n $line ]]; do
read -r name _ <<<"$line"
if [[ -n $name ]]; then
manifest[$name]=$line
fi
done <"$1"
}
# Sets NAME's manifest line to NAME followed by FIELDS; without FIELDS, removes
# it.
set_manifest_line() {
local name=$1
shift
if (($#)); then
manifest[$name]="$name $*"
else
unset 'manifest[$name]'
fi
}
write_manifest() {
if ((${#manifest[@]})); then
printf '%s\n' "${manifest[@]}" | sort >"$1.new"
else
: >"$1.new"
fi
mv "$1.new" "$1"
}
# A file in DIR counts as staged when the stage manifest lists it. In --stage
# mode, that manifest is also the one the run updates.
use_stage() {
local name
stage_source=$1
read_manifest "$1/manifest"
for name in "${!manifest[@]}"; do
staged[$name]=1
done
}
is_staged() {
[[ -n ${staged[$1]-} && -f $stage_source/$1 ]]
}
# True if the stage lacks any of the missing files.
stage_incomplete() {
local name
for name in "${missing_names[@]}"; do
is_staged "$name" || return 0
done
return 1
}
# The stage comes first; the driver stores supply what it lacks. A staged file
# listed under another device-tree path with the same file name (a later device
# tree may move it) is the last resort, since after a full-disk install the
# stage is the only copy.
locate_firmware() {
local name=$1 node=$2 src status=0
if is_staged "$name"; then
printf '%s\n' "$stage_source/$name"
return 0
fi
if ((${#sources[@]})); then
src=$(find_in_sources "$(basename "$name")" "$node") || status=$?
if ((status == 0)); then
printf '%s\n' "$src"
return 0
fi
# The stage does not override a refusal of ambiguous variants.
((status != 2)) || return 1
fi
[[ -n $stage_source ]] || return 1
local -a sources=("$stage_source")
src=$(find_in_sources "$(basename "$name")" "$node") || return 1
is_staged "${src#"$stage_source"/}" || return 1
printf '%s\n' "$src"
}
# ---------------------------------------------------------------------------
# 3. Stage: live session, before the disk is written
# ---------------------------------------------------------------------------
if [[ $mode == stage ]]; then
# Files staged by an earlier run are kept; a rerun, for example with -d,
# looks only for the rest, so an empty or partial stage blocks nothing.
if [[ -f "$stage_dir/manifest" ]]; then
use_stage "$stage_dir"
fi
if ! stage_incomplete; then
log "already staged in $stage_dir"
exit 0
fi
if [[ -n $driverstore ]]; then
sources=("$driverstore")
elif [[ -n $stage_source && $CHECKED_EUID != 0 ]]; then
# Without root the partitions cannot be read; keep the stage as it is.
log "already staged in $stage_dir; run as root to read the Windows partitions for the rest"
exit 0
else
scan_windows_partitions
fi
mkdir -p "$stage_dir"
copied=0
kept=0
for i in "${!missing_names[@]}"; do
node=${missing_nodes[$i]}
name=${missing_names[$i]}
if is_staged "$name"; then
kept=$((kept + 1))
continue
fi
src=""
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
fi
install -Dm644 "$src" "$stage_dir/$name"
set_manifest_line "$name" "$(sha256sum "$src" | cut -d' ' -f1)" "$src"
copied=$((copied + 1))
done
write_manifest "$stage_dir/manifest"
log "staged $copied of ${#missing_names[@]} firmware files in $stage_dir ($kept already staged)"
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")
else
# Windows supplies what the stage lacks, so a stage never hides it.
if [[ -f "$stage_dir/manifest" ]]; then
log "using the firmware staged by the installer in $stage_dir"
use_stage "$stage_dir"
fi
# After installation, only internal disks are read; -d reads any other.
if stage_incomplete; then
scan_windows_partitions internal-only
fi
fi
mkdir -p "$STATE_DIR"
touch "$MANIFEST"
read_manifest "$MANIFEST"
# A board may list a name after an earlier run installed it. That copy still
# loads first, but this tool never deletes it: matching bytes would not prove
# that the file is still the one it installed, since a board's own tool may
# keep the same file there. Stop recording it and say where it is.
for name in "${provided_names[@]}"; do
[[ -n ${manifest[$name]-} ]] || continue
set_manifest_line "$name"
write_manifest "$MANIFEST"
file="$FW_ROOT/updates/$name"
if [[ -e $file || -L $file ]]; then
log "note: an earlier run installed $file, but $name is now listed in $PROVIDED_DIR/${provided[$name]}.list; the file is left in place and no longer recorded"
log "note: while it is there it loads before any other copy. The board package may remove it, or you can by hand. This tool does not, because matching bytes would not prove that the file is still the one it installed: a board's own tool may keep the same file there."
fi
done
installed=0
for i in "${!missing_names[@]}"; do
node=${missing_nodes[$i]}
name=${missing_names[$i]}
src=$(locate_firmware "$name" "$node") || true
if [[ -z $src ]]; then
warn "not found: $name"
continue
fi
install -Dm644 "$src" "$FW_ROOT/updates/$name"
# Record each file as soon as it is in place.
set_manifest_line "$name" "$(sha256sum "$src" | cut -d' ' -f1)" "$src" \
"$(date -u +%Y-%m-%dT%H:%M:%SZ)"
write_manifest "$MANIFEST"
log "installed $name"
installed=$((installed + 1))
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 || ${node##*/} == zap-shader@* ]] || continue
if file=$(firmware_file "$name"); then
initramfs_files+=("${file#"$ROOT"}")
fi
done < <(dt_firmware_names)
initramfs_changed=0
if ((${#initramfs_files[@]})); then
initramfs_config=$(
echo "# Generated by qcom-firmware-extract for early DRM firmware loading."
printf 'FILES+=(%s)\n' "$(printf '%q ' "${initramfs_files[@]}" | sed 's/ $//')"
)
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 ((still_missing > 0 && ${#sources[@]} == 0)); then
warn "no Windows installation found to copy from; run again with -d <driver store> when one is available"
fi
log "nothing installed; $still_missing firmware file(s) still missing"
((initramfs_changed)) || exit 0
else
log "installed $installed firmware file(s), $still_missing still missing"
fi
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