From 176bd529570fccac9da4b15959d7e347737c9537 Mon Sep 17 00:00:00 2001 From: Rick Peters Date: Thu, 24 Sep 2026 10:22:15 +0200 Subject: [PATCH] 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. --- AGENTS.md | 7 +++ CHANGELOG.md | 8 ++- README.md | 23 ++++---- lib/bios.sh | 121 ++++++++++++++++++++++++++++++++++++++++ lib/menu.sh | 31 +++++++--- setup-gamescope-boot.sh | 19 ++++++- 6 files changed, 187 insertions(+), 22 deletions(-) create mode 100644 lib/bios.sh diff --git a/AGENTS.md b/AGENTS.md index 80da0f5..be4a747 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 39bb60e..b159f61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 215d969..eb1b65c 100644 --- a/README.md +++ b/README.md @@ -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` | diff --git a/lib/bios.sh b/lib/bios.sh new file mode 100644 index 0000000..44e25d5 --- /dev/null +++ b/lib/bios.sh @@ -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." +} diff --git a/lib/menu.sh b/lib/menu.sh index eb2d5e0..c5854df 100644 --- a/lib/menu.sh +++ b/lib/menu.sh @@ -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[@]}") diff --git a/setup-gamescope-boot.sh b/setup-gamescope-boot.sh index 549ca1b..b9f65db 100755 --- a/setup-gamescope-boot.sh +++ b/setup-gamescope-boot.sh @@ -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