feat: Opt-in BIOS update for the Steam Machine

- Menu item (Fremont only, never preselected) with the current and newest BIOS version.
- Two warnings and two confirmations (y/N, then typing UPDATE); checksum-verified download
  of Valve's fremont-hw-support; installed with fwupd, written at the next restart.
- Menu actions: one-off items that are never preselected, re-applied or listed as on/off.
This commit is contained in:
theupriser committed 2026-09-24 10:22:15 +02:00
1 parent b306a72255
commit 176bd52957
6 files changed
+187 -22

No files matched your search

+7
View File
@@ -132,6 +132,13 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
and `ensure-kernel-headers.service` installs missing ones at boot (a
pacman hook can't run pacman), which triggers DKMS's install hook. The module creates
`/sys/class/leds/valve-leds*`.
- `bios` is an *action* (`ACTIONS` in `lib/menu.sh`), not an on/off
component: never preselected (not even on a first run), never re-applied
by `a`, not listed in the state overview, and `bios_status` is always off.
Keep both confirmations (y/N, then typing `UPDATE`) and the warnings; the
firmware comes from the newest `holo-X.Y` repo (`.files` db names the
`.cab`, `.db` gives the SHA-256), and fwupd itself refuses non-Fremont
hardware. It can only be tested up to fwupd's refusal in the VM.
- `Relogin=true` means a gamescope that fails to start is relaunched in a
tight loop; keep that in mind when changing session handling.
+7 -1
View File
@@ -52,10 +52,16 @@ Machine LED driver works on every installed kernel and survives kernel updates.
- Install with `.../releases/latest/download/setup-gamescope-boot.sh`, which
always points to the newest release.
- `1604ed1` **docs: Versioning and release rules in AGENTS.md**
- **ci: The latest tag follows the newest version tag**
- `6afef78` **ci: The latest tag follows the newest version tag**
- When a new version is released, the `latest` tag and release move to it
(bundle replaced), so the older `.../releases/download/latest/...` URL
also always gives the newest version.
- **feat: Opt-in BIOS update for the Steam Machine**
- New menu item (Steam Machine only, never ticked by default) showing the
current BIOS version and the newest from Valve's `fremont-hw-support`.
- Two large warnings and two confirmations (y/N, then typing `UPDATE`),
checksum-verified download, installed with fwupd; the wizard then offers
the restart that writes it, with a warning to keep the power on.
## 0.6.2 - 2026-09-23
+13 -10
View File
@@ -283,17 +283,19 @@ cat /var/lib/dkms/leds-valve-dkms/0.1/build/make.log
journalctl --user -b | grep -i led
```
**BIOS updates** are not part of the wizard. Valve ships the Steam Machine
BIOS as `F7F0108.cab` in its `fremont-hw-support` package for fwupd. To
install it by hand (keep the machine on mains power and don't interrupt it):
**BIOS updates** (opt-in, never ticked by default). On a Steam Machine the menu
has an **Update BIOS** item that shows the current BIOS version and the newest
one Valve ships (`F7F0108.cab` in its `fremont-hw-support` package, looked up
on Valve's SteamOS mirror). Ticking it shows a large warning, asks for
confirmation, shows the warning again and only continues when you type
`UPDATE`. It then downloads the package (checksum verified) and hands the
firmware to fwupd; the BIOS is written during the next restart.
```bash
sudo dmidecode -s bios-version # current version
sudo pacman -S fwupd
curl -LO https://steamdeck-packages.steamos.cloud/archlinux-mirror/holo-3.9/os/x86_64/fremont-hw-support-20260807.1-1-any.pkg.tar.zst
mkdir fhw && tar -I zstd -xf fremont-hw-support-*.pkg.tar.zst -C fhw
sudo fwupdmgr install fhw/usr/share/fwupd/remotes.d/fremont/firmware/F7F0108.cab
```
**At your own risk:** a failed or interrupted BIOS update can leave the machine
unable to start. Keep it on mains power, and never turn off the power, unplug
it or press the power button while it updates, including during the restart
afterwards; the screen can stay black for several minutes. fwupd refuses the
file on anything that isn't a Steam Machine.
### Manual session control
@@ -328,6 +330,7 @@ gamescope-session, ...) stay installed.
| `lib/vapor-theme.sh` | SteamOS theme: installs and switches to `cachyos-vapor` |
| `lib/steamos-extras.sh` | SteamOS desktop extras from Valve's package (Add to Steam, Nested Desktop, icon, keyboard rule, KWallet) |
| `lib/single-user.sh` | Single user mode: no lock screen, user switching or log out |
| `lib/bios.sh` | Update BIOS (Steam Machine, opt-in): current/newest version, double confirmation, fwupd |
| `lib/steam-machine.sh` | Steam Machine support: LED driver, LED access, steamos-manager |
| `.github/tools/bundle.sh` | Builds the single-file version (`dist/setup-gamescope-boot.sh`) |
| `.github/workflows/bundle.yml` | Builds and checks it on every push; publishes it on `main` |
+121
View File
@@ -0,0 +1,121 @@
#!/bin/bash
# "Update BIOS" menu item, only on a Steam Machine and always opt-in: flashes
# the newest Steam Machine BIOS from Valve's fremont-hw-support package with
# fwupd. It's an action, not an on/off component: never preselected, never
# re-applied, and there is nothing to turn off afterwards.
# Sourced by setup-gamescope-boot.sh; not meant to be run on its own.
BIOS_REPO_PREFIX=holo
bios_available() { detect_valve_fremont; }
bios_status() { return 1; }
bios_disable() { return 0; }
bios_current() {
cat /sys/class/dmi/id/bios_version 2>/dev/null || echo unknown
}
bios_lookup_newest() {
# Sets BIOS_REPO, BIOS_PKG, BIOS_SHA256 and BIOS_NEWEST (e.g. F7F0108)
# from the newest holo-X.Y repository on Valve's mirror: its .files
# database names the firmware file, its .db gives the package checksum.
# Only once per run, and with short timeouts so an offline machine
# doesn't hold up the menu.
[[ -n "${BIOS_LOOKED_UP:-}" ]] && return 0
BIOS_LOOKED_UP=1; BIOS_NEWEST=""
local tmp desc
BIOS_REPO="$(curl -fsL --max-time 10 "$VALVE_MIRROR/" | grep -oE "$BIOS_REPO_PREFIX-[0-9]+\.[0-9]+/" | tr -d / | sort -uV | tail -n 1)"
[[ -n "$BIOS_REPO" ]] || return 1
tmp="$(mktemp -d)"
if curl -fsL --max-time 30 "$VALVE_MIRROR/$BIOS_REPO/os/x86_64/$BIOS_REPO.files" -o "$tmp/files" &&
curl -fsL --max-time 30 "$VALVE_MIRROR/$BIOS_REPO/os/x86_64/$BIOS_REPO.db" -o "$tmp/db"; then
desc="$(tar -tf "$tmp/files" 2>/dev/null | grep -E '^fremont-hw-support-[0-9][^/]*/files$' | head -n 1)"
[[ -n "$desc" ]] && BIOS_CAB="$(tar -xOf "$tmp/files" "$desc" | grep -E '^usr/share/fwupd/.*\.cab$' | head -n 1)"
desc="${desc%/files}/desc"
BIOS_PKG="$(tar -xOf "$tmp/db" "$desc" 2>/dev/null | awk '/^%FILENAME%$/ { getline; print }')"
BIOS_SHA256="$(tar -xOf "$tmp/db" "$desc" 2>/dev/null | awk '/^%SHA256SUM%$/ { getline; print }')"
[[ -n "${BIOS_CAB:-}" && -n "$BIOS_PKG" && -n "$BIOS_SHA256" ]] && BIOS_NEWEST="$(basename "$BIOS_CAB" .cab)"
fi
rm -rf "$tmp"
[[ -n "$BIOS_NEWEST" ]]
}
bios_label() {
# Menu label with the current and the newest version.
local newest="newest unknown (offline?)"
bios_lookup_newest && newest="newest $BIOS_NEWEST"
[[ "$BIOS_NEWEST" == "$(bios_current)" ]] && newest="up to date"
echo "Update BIOS (at your own risk): now $(bios_current), $newest"
}
bios_disclaimer() {
local r="$c_red$c_bold" n="$c_reset"
echo
echo -e "${r} ###################################################################${n}"
echo -e "${r} ## ##${n}"
echo -e "${r} ## WARNING: BIOS UPDATE - ENTIRELY AT YOUR OWN RISK ##${n}"
echo -e "${r} ## ##${n}"
echo -e "${r} ###################################################################${n}"
echo -e "${r} ##${n} $1"
echo -e "${r} ##${n}"
echo -e "${r} ##${n} - A failed or interrupted BIOS update can leave the machine"
echo -e "${r} ##${n} unable to start (bricked). This wizard, CachyOS and Valve"
echo -e "${r} ##${n} take no responsibility for that."
echo -e "${r} ##${n} - ${c_bold}NEVER turn off the power, unplug the machine or press the${n}"
echo -e "${r} ##${n} ${c_bold}power button while the update runs${n}, including during the"
echo -e "${r} ##${n} restart(s) afterwards, when the firmware is actually written."
echo -e "${r} ##${n} - The screen can stay black for several minutes. Wait."
echo -e "${r} ##${n} - Use this only on a Valve Steam Machine, on mains power, and"
echo -e "${r} ##${n} close all other programs first."
echo -e "${r} ###################################################################${n}"
echo
}
bios_enable() {
if ! bios_lookup_newest; then
err "Couldn't find the newest Steam Machine BIOS on Valve's mirror ($VALVE_MIRROR)."
return 1
fi
local current
current="$(bios_current)"
if [[ "$current" == "$BIOS_NEWEST" ]]; then
ok "The BIOS is already the newest version ($current); nothing to do."
return 0
fi
bios_disclaimer "Current BIOS: ${c_bold}$current${c_reset} -> new BIOS: ${c_bold}$BIOS_NEWEST${c_reset} ($BIOS_PKG)"
ask_yn "Do you understand the risks and want to continue?" n ||
{ info "BIOS update cancelled; nothing was changed."; return 0; }
bios_disclaimer "LAST CHANCE: this flashes BIOS $BIOS_NEWEST onto this machine."
local reply
read -rp "$(echo -e "${c_red}${c_bold}Type UPDATE (in capitals) to flash the BIOS, anything else cancels:${c_reset} ")" reply
[[ "$reply" == UPDATE ]] || { info "BIOS update cancelled; nothing was changed."; return 0; }
pacman -Q fwupd >/dev/null 2>&1 || sudo pacman -S --needed --noconfirm fwupd ||
{ err "Installing fwupd failed."; return 1; }
local tmp
tmp="$(mktemp -d)"
info "Downloading $BIOS_PKG ($BIOS_REPO)..."
if ! curl -fL "$VALVE_MIRROR/$BIOS_REPO/os/x86_64/$BIOS_PKG" -o "$tmp/pkg.tar.zst" ||
! echo "$BIOS_SHA256 $tmp/pkg.tar.zst" | sha256sum -c --quiet - ||
! tar -I unzstd -xf "$tmp/pkg.tar.zst" -C "$tmp" "$BIOS_CAB"; then
err "Downloading or verifying $BIOS_PKG failed; the BIOS was not touched."
rm -rf "$tmp"
return 1
fi
info "Handing BIOS $BIOS_NEWEST to fwupd. Do NOT turn off the power from now on."
# -y: we already asked twice; --no-reboot-check: the wizard's own
# restart question comes at the end.
if ! sudo fwupdmgr install -y --no-reboot-check "$tmp/$BIOS_CAB"; then
err "fwupd could not install the BIOS update (see above); the BIOS was not changed."
rm -rf "$tmp"
return 1
fi
rm -rf "$tmp"
BIOS_NEEDS_RESTART=1
ok "BIOS $BIOS_NEWEST is staged. It is written during the next restart:"
warn "keep the power on and don't touch the machine until it has fully started again."
}
+23 -8
View File
@@ -5,7 +5,10 @@
# Menu order. Components are turned on in this order and off in reverse;
# gaming must come first (single user builds on it).
COMPONENTS=(gaming theme glyphs single machine)
COMPONENTS=(gaming theme glyphs single machine bios)
# One-off actions rather than on/off components: never preselected, never
# re-applied, not listed as on or off.
ACTIONS=(bios)
declare -A LABEL=(
[gaming]="SteamOS conversion: boot into gaming mode, Steam on the desktop"
@@ -13,13 +16,19 @@ declare -A LABEL=(
[glyphs]="Install Steam Deck/Machine icons: Deck button icons in gaming mode"
[single]="Single user mode: no password, lock screen or log out (SDDM)"
[machine]="Steam Machine support: LED bar driver, hardware settings in Steam"
[bios]="Update BIOS"
)
declare -A CURRENT WANTED
component_available() {
[[ "$1" != machine ]] || machine_available
case "$1" in
machine) machine_available ;;
bios) bios_available ;;
esac
}
is_action() { [[ " ${ACTIONS[*]} " == *" $1 "* ]]; }
detect_components() {
local c any=false
for c in "${COMPONENTS[@]}"; do
@@ -27,10 +36,12 @@ detect_components() {
if "${c}_status"; then CURRENT[$c]=1; any=true; else CURRENT[$c]=0; fi
WANTED[$c]=${CURRENT[$c]}
done
# First run: preselect the full SteamOS experience.
# Shows the current and newest BIOS version.
bios_available && { bios_lookup_newest; LABEL[bios]="$(bios_label)"; }
# First run: preselect the full SteamOS experience (never an action).
if [[ "$any" == false ]]; then
for c in "${COMPONENTS[@]}"; do
component_available "$c" && WANTED[$c]=1
component_available "$c" && ! is_action "$c" && WANTED[$c]=1
done
fi
}
@@ -52,6 +63,7 @@ show_menu() {
component_available "$c" || continue
i=$((i + 1)); MENU_ITEMS[$i]=$c
now="off"; [[ "${CURRENT[$c]}" == 1 ]] && now="${c_green}on${c_reset} "
is_action "$c" && now="-"
want="[ ]"; [[ "${WANTED[$c]}" == 1 ]] && want="[x]"
printf " %-3s %-6b %-6s %s\n" "$i" "$now " "$want" "${LABEL[$c]}"
done
@@ -84,13 +96,14 @@ draw_menu_tui() {
component_available "$c" || continue
MENU_ITEMS[$i]=$c
box="[ ]"; [[ "${WANTED[$c]}" == 1 ]] && box="[${c_green}x${c_reset}]"
state="off"; [[ "${CURRENT[$c]}" == 1 ]] && state="${c_green}on${c_reset}"
state=" (now: off)"; [[ "${CURRENT[$c]}" == 1 ]] && state=" (now: ${c_green}on${c_reset})"
is_action "$c" && state=" (opt-in, runs once)"
line="$box ${LABEL[$c]}"
if (( i == cursor )); then
# The green x's reset would end the bold too: re-enable it after.
echo -e " ${c_cyan}>${c_reset} ${c_bold}${line//"$c_reset"/"$c_reset$c_bold"}${c_reset} (now: ${state})"
echo -e " ${c_cyan}>${c_reset} ${c_bold}${line//"$c_reset"/"$c_reset$c_bold"}${c_reset}${state}"
else
echo -e " ${line} (now: ${state})"
echo -e " ${line}${state}"
fi
i=$((i + 1))
done
@@ -160,6 +173,7 @@ plan_changes() {
for c in "${COMPONENTS[@]}"; do
component_available "$c" || continue
[[ "${WANTED[$c]}" == 1 ]] || continue
if is_action "$c"; then TO_ENABLE+=("$c"); continue; fi
if [[ "${CURRENT[$c]}" == 0 || "$REAPPLY" == true ]] ||
[[ "$c" == gaming && "${CURRENT[single]}" != "${WANTED[single]}" ]]; then
TO_ENABLE+=("$c")
@@ -177,7 +191,8 @@ apply_changes() {
"${c}_disable" || failed+=("$c")
done
for c in "${TO_ENABLE[@]}"; do
echo; echo -e "${c_bold}Turning on: ${LABEL[$c]}${c_reset}"
if is_action "$c"; then echo; echo -e "${c_bold}Running: ${LABEL[$c]}${c_reset}"
else echo; echo -e "${c_bold}Turning on: ${LABEL[$c]}${c_reset}"; fi
"${c}_enable" || failed+=("$c")
done
FAILED=("${failed[@]}")
+16 -3
View File
@@ -19,7 +19,7 @@ VERSION=0.7.0
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
for lib in common state packages login-manager single-user steam-desktop steam-machine vapor-theme steamos-extras desktop-shortcut menu; do
for lib in common state packages login-manager single-user steam-desktop steam-machine vapor-theme steamos-extras bios desktop-shortcut menu; do
# shellcheck source=/dev/null
source "$SCRIPT_DIR/lib/$lib.sh"
done
@@ -52,7 +52,8 @@ echo
echo -e "${c_bold}This will:${c_reset}"
for c in "${TO_DISABLE[@]}"; do echo " - turn off: ${LABEL[$c]}"; done
for c in "${TO_ENABLE[@]}"; do
if [[ "${CURRENT[$c]}" == 1 ]]; then echo " - re-apply: ${LABEL[$c]}"; else echo " - turn on: ${LABEL[$c]}"; fi
if is_action "$c"; then echo " - run: ${LABEL[$c]} (asks two more confirmations)"
elif [[ "${CURRENT[$c]}" == 1 ]]; then echo " - re-apply: ${LABEL[$c]}"; else echo " - turn on: ${LABEL[$c]}"; fi
done
ask_yn "Go ahead?" y || { info "Nothing changed."; exit 0; }
@@ -67,7 +68,7 @@ echo
detect_components
echo -e "${c_bold}Done. Current state:${c_reset}"
for c in "${COMPONENTS[@]}"; do
component_available "$c" || continue
component_available "$c" && ! is_action "$c" || continue
if [[ "${CURRENT[$c]}" == 1 ]]; then echo -e " ${c_green}on ${c_reset} ${LABEL[$c]}"; else echo " off ${LABEL[$c]}"; fi
done
if [[ ${#FAILED[@]} -gt 0 ]]; then
@@ -75,6 +76,18 @@ if [[ ${#FAILED[@]} -gt 0 ]]; then
fi
echo
# A staged BIOS update is written during the restart.
if [[ -n "${BIOS_NEEDS_RESTART:-}" ]]; then
warn "The BIOS update is written during the next restart. Keep the power on and"
warn "don't touch the machine until it has fully started again, even if the screen stays black."
if ask_yn "Restart now to install the BIOS update?" n; then
sudo reboot
else
info "The BIOS update installs at your next restart."
fi
exit 0
fi
# Login manager changes only take effect after a restart.
if [[ " ${TO_DISABLE[*]} ${TO_ENABLE[*]} " == *" gaming "* || " ${TO_DISABLE[*]} ${TO_ENABLE[*]} " == *" single "* ]]; then
if ask_yn "Restart now so the changes take effect?" n; then