ci / bun-nix (pull_request) Successful in 29s
ci / docs-site (pull_request) Successful in 1m37s
ci / web (pull_request) Successful in 2m39s
ci / rust-arm64 (pull_request) Successful in 4m10s
ci / rust (pull_request) Successful in 6m50s
nix / flake (pull_request) Failing after 23m28s
60-punktfunk.rules chgrp's the usbip vhci attach/detach nodes to a dedicated
`punktfunk` group (security-review 2026-08-05 M-4: writing `attach` materialises
an arbitrary emulated USB device, so it must not ride on `input`). Four of the
six install paths shipped that rule in 0.25.0 without ever creating the group.
chgrp then failed, the nodes stayed root:root 0644, and the virtual Steam Deck
pad silently never attached — while `usermod -aG punktfunk` failed outright with
"group 'punktfunk' does not exist".
Affected and fixed:
* arch — post_upgrade() called only _ensure_update_group, so every box that
reached 0.25.0 by `pacman -Syu` missed it; post_install was correct.
* nix — no users.groups.punktfunk at all, though host.users' own description
already promised the usbip/vhci pad. Declares it now and adds
host.users to both groups.
* bazzite sysext — a group is host state and cannot ride an image, and the
deb/rpm scriptlets that would create it never run there.
* steamdeck install.sh/update.sh — handled `input` only. Both now create the
group and join it: running that script IS the statement "make my
Deck a host with native pad passthrough".
deb and rpm were correct throughout (one postinst/%post for install + upgrade).
Also on the Deck path: web.env secret hygiene. install.sh's `chmod 600` sat
inside the create-only branch despite a comment calling it "the idempotent belt
for a pre-existing file", and update.sh never touched the config dir at all — so
an install set up once and only updated since kept web.env world-readable
(0644) with the console password and session secret in it. Both scripts now
harden ~/.config/punktfunk to 0700 and web.env to 0600 on every run, and say so
loudly, because a chmod does not un-leak an already-readable secret: the
password still needs rotating.
Both group blocks are `if ensure_group ...` rather than `ensure_group || true`:
a failed groupadd must not fall through to a usermod against a nonexistent
group, which under `set -e` aborted install.sh after the long build and
update.sh before the service restart (verified: exit 6, no restart).
Docs: the group is now documented where people actually look — the per-distro
guides, install.md, steamos-host.md, a new troubleshooting entry for "pad
arrives as an Xbox 360 controller", and the uninstall pages. The 0.25.0 notes
gain the "group does not exist" caveat and turn the password bullet from
"consider rotating" into a real instruction, and CHANGELOG records the known
issue against the breaking change that introduced it.
Verified: bash -n on all four scripts; the arch scriptlet's post_upgrade driven
in a container (creates the group, idempotent on re-run); the ensure_group
helper and both membership branches, including a control that reproduces the
original bug (chgrp to a missing group leaves the node root:root 0644); the
find -perm /0077 probe across 0644/0640/0604/0600/0400 on GNU findutils;
`nix flake check --no-build` (the exact CI gate) and a NixOS eval showing
alice.extraGroups == ["input","punktfunk"]; docs-site build + typecheck.
206 lines
12 KiB
Bash
Executable File
206 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# punktfunk — Steam Deck HOST update: rebuild from the current source + restart the services.
|
|
# Run on the Deck after pulling/rsyncing new source. Pairings, config, and the web login persist.
|
|
#
|
|
# bash scripts/steamdeck/update.sh # rebuild host (+web if installed) and restart
|
|
# bash scripts/steamdeck/update.sh --pull # `git pull` first (if the source is a git checkout)
|
|
#
|
|
set -euo pipefail
|
|
log() { printf '\033[1;36m==>\033[0m %s\n' "$*"; }
|
|
ok() { printf '\033[1;32m ok\033[0m %s\n' "$*"; }
|
|
# warn was USED below but never defined — under `set -e` the first warn call ("command not
|
|
# found") aborted the whole update before the service restarts.
|
|
warn() { printf '\033[1;33m !!\033[0m %s\n' "$*" >&2; }
|
|
die() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; }
|
|
# Create a system group if it is missing (needs sudo). Idempotent, and mirrors what the
|
|
# deb/rpm/arch scriptlets do — a udev rule that chgrp's to a group nobody created fails silently.
|
|
ensure_group() {
|
|
getent group "$1" >/dev/null 2>&1 && return 0
|
|
sudo groupadd --system "$1" 2>/dev/null || return 1
|
|
ok "created the '$1' system group"
|
|
}
|
|
|
|
SRC="${PUNKTFUNK_SRC:-$HOME/punktfunk}"
|
|
BOX="${PUNKTFUNK_BOX:-pf2}"
|
|
TARGET_DIR="$SRC/target-steamos"
|
|
[ -d "$SRC/crates/punktfunk-host" ] || die "no punktfunk source at $SRC (set PUNKTFUNK_SRC)"
|
|
WEB=0; [ -f "$HOME/.config/systemd/user/punktfunk-web.service" ] && WEB=1
|
|
|
|
if [ "${1:-}" = "--pull" ]; then
|
|
if [ -d "$SRC/.git" ]; then log "git pull"; git -C "$SRC" pull --ff-only; ok "pulled"; else die "$SRC is not a git checkout — rsync new source then run without --pull"; fi
|
|
fi
|
|
|
|
log "Rebuilding host (release)"
|
|
# vulkan-encode matches the packaged builds (deb/arch) — see install.sh.
|
|
distrobox enter "$BOX" -- bash -lc "set -e; export PATH=\$HOME/.cargo/bin:\$PATH CARGO_TARGET_DIR='$TARGET_DIR'; cd '$SRC' && cargo build -r -p punktfunk-host --features punktfunk-host/vulkan-encode"
|
|
ok "host rebuilt"
|
|
if [ "$WEB" = 1 ]; then
|
|
log "Rebuilding web console"
|
|
distrobox enter "$BOX" -- bash -lc "set -e; export PATH=\$HOME/.bun/bin:\$PATH; cd '$SRC/web' && bun install --frozen-lockfile && bun run build"
|
|
ok "web rebuilt"
|
|
fi
|
|
|
|
# Plugin runner (scripting): rebuild the user-scoped runner payload (install.sh §2b) — also
|
|
# RETROFITS it onto older installs that predate it (the "plugin runner isn't installed" console
|
|
# state on SteamOS).
|
|
log "Rebuilding plugin runner (scripting)"
|
|
mkdir -p "$HOME/.local/bin" "$HOME/.local/lib/punktfunk-scripting" "$HOME/.local/share/punktfunk-scripting"
|
|
distrobox enter "$BOX" -- bash -lc "set -e; export PATH=\$HOME/.bun/bin:\$PATH; cd '$SRC/sdk' && bun install --frozen-lockfile --ignore-scripts && bun build src/runner-cli.ts --target=bun --outfile \"\$HOME/.local/share/punktfunk-scripting/runner-cli.js\" && install -m0755 \"\$(command -v bun)\" \"\$HOME/.local/lib/punktfunk-scripting/bun\""
|
|
grep -q 'attempt=' "$HOME/.local/share/punktfunk-scripting/runner-cli.js" \
|
|
|| die "runner bundle missing the dynamic plugin import — wrong build"
|
|
cat > "$HOME/.local/bin/punktfunk-scripting" <<'WRAP'
|
|
#!/bin/sh
|
|
# Generated by scripts/steamdeck/update.sh — user-scoped punktfunk-scripting (see install.sh §2b).
|
|
exec "$HOME/.local/lib/punktfunk-scripting/bun" "$HOME/.local/share/punktfunk-scripting/runner-cli.js" "$@"
|
|
WRAP
|
|
chmod 0755 "$HOME/.local/bin/punktfunk-scripting"
|
|
sed 's|^ExecStart=.*|ExecStart=%h/.local/bin/punktfunk-scripting|' \
|
|
"$SRC/scripts/punktfunk-scripting.service" > "$HOME/.config/systemd/user/punktfunk-scripting.service"
|
|
systemctl --user daemon-reload
|
|
ok "plugin runner rebuilt (opt-in service: systemctl --user enable --now punktfunk-scripting)"
|
|
|
|
# HDR gamescope (punktfunk-gamescope): rebuild when the packaging tree changed or the installed
|
|
# binary stopped working — also RETROFITS it onto older installs that predate it (fast no-op
|
|
# otherwise). Best-effort; on failure the host streams SDR (see build-gamescope.sh).
|
|
log "HDR gamescope (punktfunk-gamescope)"
|
|
PUNKTFUNK_SRC="$SRC" PUNKTFUNK_BOX="$BOX" bash "$SRC/scripts/steamdeck/build-gamescope.sh"
|
|
|
|
# Retrofit the post-OS-update rebuild check (install.sh §5) onto older installs: probes the host
|
|
# binary with ldd at session start and re-runs this script when a SteamOS update broke its links.
|
|
if [ ! -f "$HOME/.config/systemd/user/punktfunk-rebuild-check.service" ]; then
|
|
cat > "$HOME/.config/systemd/user/punktfunk-rebuild-check.service" <<EOF
|
|
# Generated by scripts/steamdeck/update.sh — rebuild the host if a SteamOS update broke its libs.
|
|
[Unit]
|
|
Description=punktfunk SteamOS post-update rebuild check
|
|
Before=punktfunk-host.service
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
ExecStart=$SRC/scripts/steamdeck/rebuild-check.sh
|
|
TimeoutStartSec=1800
|
|
|
|
[Install]
|
|
WantedBy=default.target
|
|
EOF
|
|
chmod +x "$SRC/scripts/steamdeck/rebuild-check.sh" 2>/dev/null || true
|
|
systemctl --user daemon-reload
|
|
systemctl --user enable punktfunk-rebuild-check.service 2>/dev/null || true
|
|
ok "punktfunk-rebuild-check.service installed (auto-rebuild after SteamOS updates)"
|
|
fi
|
|
|
|
CONFIG="$HOME/.config/punktfunk"
|
|
|
|
# Secret hygiene, retrofitted. install.sh §3 does this for fresh installs — but only install.sh
|
|
# ever did, so a Deck that was set up once and only ever *updated* since kept the old modes
|
|
# forever. This directory holds web.env (console login password + session secret), the mgmt token
|
|
# and the host key; a plain `mkdir -p` left it 0755 at the Deck's ambient umask and web.env itself
|
|
# 0644, i.e. readable by every local account (2026-08-05 review L-19). Both chmods are idempotent.
|
|
[ -d "$CONFIG" ] && chmod 700 "$CONFIG" 2>/dev/null || true
|
|
if [ -f "$CONFIG/web.env" ] && find "$CONFIG/web.env" -maxdepth 0 -perm /0077 2>/dev/null | grep -q .; then
|
|
chmod 600 "$CONFIG/web.env"
|
|
warn "web.env was group/world-readable — an older install wrote it at the default umask."
|
|
warn "Tightened to 0600, but that does NOT un-expose the password it already leaked to every"
|
|
warn "local account. Rotate it: edit PUNKTFUNK_UI_PASSWORD in $CONFIG/web.env, then"
|
|
warn " systemctl --user restart punktfunk-web"
|
|
fi
|
|
|
|
# Retrofit config that install.sh now writes but older installs predate (both idempotent):
|
|
# RADV_PERFTEST — Van Gogh RADV still gates VK_KHR_video_encode_* behind it; without it the
|
|
# Vulkan backend can't open and sessions silently fall back to libav VAAPI. The KWin .desktop —
|
|
# KWin only grants the restricted capture/input globals to the exe a .desktop authorizes.
|
|
HOST_ENV="$CONFIG/host.env"
|
|
if [ -f "$HOST_ENV" ] && ! grep -q '^RADV_PERFTEST=' "$HOST_ENV"; then
|
|
printf '\n# Van Gogh RADV gates VK_KHR_video_encode_* behind this (Vulkan Video encode).\nRADV_PERFTEST=video_encode\n' >> "$HOST_ENV"
|
|
ok "host.env: added RADV_PERFTEST=video_encode"
|
|
fi
|
|
mkdir -p "$HOME/.local/share/applications"
|
|
sed "s|^Exec=.*|Exec=$TARGET_DIR/release/punktfunk-host|" "$SRC/packaging/linux/io.unom.Punktfunk.Host.desktop" \
|
|
> "$HOME/.local/share/applications/io.unom.Punktfunk.Host.desktop"
|
|
ok "KWin desktop-capture authorization refreshed"
|
|
|
|
# Retrofit the system bits install.sh now sets up but older installs predate (idempotent). vhci-hcd =
|
|
# usbip transport for the native Steam Deck pad; 60-punktfunk.rules = /dev/uhid + vhci access; input
|
|
# group = uhid write; the kde-authorized grant (per-user, no root) = Desktop-mode input. A stock Deck
|
|
# needs a sudo PASSWORD, so PROMPT for it rather than silently skipping (skipping = gamepads stay dead).
|
|
SUDO_OK=0
|
|
if sudo -n true 2>/dev/null; then
|
|
SUDO_OK=1
|
|
elif [ -t 0 ]; then
|
|
warn "sudo needs your password to (re)apply the gamepad udev rule, vhci-hcd, input group, and UDP buffers:"
|
|
sudo -v && SUDO_OK=1 || true
|
|
fi
|
|
if [ "$SUDO_OK" = 1 ]; then
|
|
if [ -f "$SRC/scripts/60-punktfunk.rules" ]; then
|
|
sudo install -m644 "$SRC/scripts/60-punktfunk.rules" /etc/udev/rules.d/60-punktfunk.rules
|
|
sudo udevadm control --reload-rules >/dev/null 2>&1 || true
|
|
sudo udevadm trigger >/dev/null 2>&1 || true
|
|
ok "gamepad udev rule ensured"
|
|
fi
|
|
if [ -f "$SRC/scripts/punktfunk-modules.conf" ]; then
|
|
sudo install -m644 "$SRC/scripts/punktfunk-modules.conf" /etc/modules-load.d/punktfunk.conf
|
|
sudo modprobe vhci-hcd 2>/dev/null || true
|
|
ok "vhci-hcd autoload ensured (native Steam Deck controller)"
|
|
fi
|
|
# UDP buffers: older installs (or sudo-skipped ones) still run the stock 416 KB cap.
|
|
if [ ! -f /etc/sysctl.d/99-punktfunk-net.conf ]; then
|
|
printf 'net.core.wmem_max=33554432\nnet.core.rmem_max=33554432\n' | sudo tee /etc/sysctl.d/99-punktfunk-net.conf >/dev/null
|
|
sudo sysctl -q -p /etc/sysctl.d/99-punktfunk-net.conf >/dev/null 2>&1 || true
|
|
ok "UDP socket buffers raised to 32 MB (persisted)"
|
|
fi
|
|
if id -nG "$USER" | grep -qw input; then :; else
|
|
sudo usermod -aG input "$USER"
|
|
warn "added $USER to the 'input' group — REBOOT (or log out/in) for it to apply"
|
|
fi
|
|
# 'punktfunk' owns the usbip vhci attach/detach nodes (60-punktfunk.rules), deliberately NOT
|
|
# 'input' — writing 'attach' materialises an arbitrary emulated USB device, a root-only kernel
|
|
# primitive that must not ride on the group every gamepad guide tells you to join
|
|
# (security-review 2026-08-05 M-4). No Deck install ever created it, so the rule's chgrp failed
|
|
# and the native Steam Deck pad silently never attached. Retrofit both group and membership.
|
|
# `if ensure_group` (not `ensure_group || true`): a failed groupadd must not fall through to a
|
|
# usermod against a group that does not exist — under `set -e` that would abort the update
|
|
# before the service restarts at the bottom, leaving the host down.
|
|
if ensure_group punktfunk; then
|
|
if id -nG "$USER" | grep -qw punktfunk; then :; else
|
|
sudo usermod -aG punktfunk "$USER"
|
|
warn "added $USER to the 'punktfunk' group (usbip vhci — the native Steam Deck pad needs it)"
|
|
warn " — REBOOT (or log out/in) for it to apply. That group can emulate arbitrary USB"
|
|
warn " devices; 'sudo gpasswd -d $USER punktfunk' drops it if you do not want the native pad."
|
|
fi
|
|
else
|
|
warn "could not create the 'punktfunk' group — the native Steam Deck pad will not attach."
|
|
warn "By hand: sudo groupadd --system punktfunk; sudo usermod -aG punktfunk $USER"
|
|
fi
|
|
# Register the tuning on Valve's atomic-update preserve list (see install.sh §4): without
|
|
# this, every SteamOS A/B update strips the three files above again (verified live —
|
|
# gamepads silently degrade to Xbox 360, UDP buffers back to 208 KB).
|
|
if [ -f "$SRC/scripts/punktfunk-atomic-keep.conf" ]; then
|
|
sudo install -Dm644 "$SRC/scripts/punktfunk-atomic-keep.conf" /etc/atomic-update.conf.d/punktfunk.conf
|
|
ok "system tuning registered to survive SteamOS updates (atomic-update.conf.d)"
|
|
fi
|
|
else
|
|
warn "no usable sudo — SKIPPED gamepad/udev/vhci/UDP tuning (all root-only; no user-space alternative)."
|
|
warn "A stock SteamOS 'deck' account has NO password — set one with 'passwd', then re-run. Gamepads stay"
|
|
warn "Xbox-360 until this runs and you reboot."
|
|
fi
|
|
echo
|
|
warn "If the controller still shows as an Xbox 360 pad, REBOOT the Deck once — the 'input' group and the"
|
|
warn "vhci-hcd module only become live for the host service on a fresh login."
|
|
GRANT_SRC="$SRC/scripts/headless/kde-authorized"
|
|
GRANT_DST="$HOME/.local/share/flatpak/db/kde-authorized"
|
|
if [ ! -s "$GRANT_DST" ] && [ -s "$GRANT_SRC" ]; then
|
|
mkdir -p "$(dirname "$GRANT_DST")"
|
|
install -m644 "$GRANT_SRC" "$GRANT_DST"
|
|
ok "seeded KDE RemoteDesktop grant (Desktop-mode input)"
|
|
fi
|
|
|
|
log "Restarting services"
|
|
# --no-block: when this script runs INSIDE punktfunk-rebuild-check.service (ordered
|
|
# Before=punktfunk-host), a blocking restart would deadlock — the restart job waits for the
|
|
# check unit, which waits for this script, which waits for the restart. Enqueue and move on;
|
|
# systemd starts the service the moment the ordering allows.
|
|
systemctl --user restart --no-block punktfunk-host.service
|
|
ok "punktfunk-host restart queued"
|
|
if [ "$WEB" = 1 ]; then systemctl --user restart --no-block punktfunk-web.service; ok "punktfunk-web restart queued"; fi
|
|
echo
|
|
log "Updated. Status: systemctl --user status punktfunk-host"
|