Compare commits

..
4 Commits
47 changed files with 221 additions and 0 deletions

No files matched your search

+2
View File
@@ -22,6 +22,7 @@ share/*
!share/vminstall-post.sh
!share/bootloader-test/
!share/vmtest/
!share/uitest/
# Belt and braces, anywhere in the tree
*.iso
@@ -34,3 +35,4 @@ share/*
*.tmp
host-keys.pub
__pycache__/
!share/golden/
+4
View File
@@ -4,6 +4,10 @@ All notable changes, per version and per commit. Versions follow
[Semantic Versioning](https://semver.org/) and were numbered from the start
of the history.
## Unreleased
- **feat: `share/golden/`: a no-VM sandbox (bubblewrap, fake `/etc` `/sys` `$HOME`, shim `pacman` `systemctl` `sudo` `uname` `modinfo`) and golden `--backend status` and apply `plan` output of the bash 2.11.1 backend for an AMD PC, an NVIDIA PC and a Steam Machine; `record.sh` records, `record.sh check` compares (the Go refactor's safety net)**
## 0.5.0 - 2026-10-02
The whole automated test (boot loaders plus suites), the Steamify ISO pipeline,
@@ -0,0 +1 @@
{"event":"plan","disable":[],"enable":["gaming","boot"]}
@@ -0,0 +1 @@
{"event":"plan","disable":[],"enable":["gaming"]}
@@ -0,0 +1 @@
{"event":"plan","disable":[],"enable":[]}
@@ -0,0 +1 @@
{"event":"plan","disable":[],"enable":[]}
@@ -0,0 +1 @@
{"event":"plan","disable":[],"enable":[]}
@@ -0,0 +1 @@
{"event":"plan","disable":[],"enable":["gaming","single"]}
+1
View File
@@ -0,0 +1 @@
{"version":"2.12.0","firstRun":true,"steamMachine":false,"kernel":"7.0.0-fixture-cachyos","pinnedKernel":"7.1.6-1","cecDevices":"","leds":0,"bios":null,"items":[{"id":"gaming","label":"SteamOS conversion","hint":"boot into gaming mode, Steam on the desktop","kind":"toggle","parent":"","needs":"","on":false,"wanted":true,"update":false,"new":false,"selectable":true},{"id":"boot","label":"Boot into the desktop instead of gaming mode","hint":"","kind":"choice","parent":"gaming","needs":"","on":false,"wanted":false,"update":false,"new":false,"selectable":true},{"id":"silent","label":"Start Steam silently in desktop mode","hint":"in the tray at login on the desktop, no window","kind":"toggle","parent":"gaming","needs":"","on":false,"wanted":false,"update":false,"new":false,"selectable":true},{"id":"glyphs","label":"Install Steam Deck/Machine icons","hint":"Deck button icons in gaming mode","kind":"toggle","parent":"gaming","needs":"","on":false,"wanted":true,"update":false,"new":false,"selectable":true},{"id":"theme","label":"Install SteamOS theme","hint":"Vapor look (cachyos-vapor)","kind":"toggle","parent":"","needs":"","on":false,"wanted":true,"update":false,"new":false,"selectable":true},{"id":"single","label":"Single user mode","hint":"no password, lock screen or log out (SDDM)","kind":"toggle","parent":"","needs":"","on":false,"wanted":true,"update":false,"new":false,"selectable":true},{"id":"launcher","label":"Steamify shortcut","hint":"the app on the desktop, Steamify Terminal in the launcher","kind":"toggle","parent":"","needs":"","on":false,"wanted":true,"update":false,"new":false,"selectable":true},{"id":"notify","label":"Update notifications","hint":"a notification when there's a new Steamify, never updates by itself","kind":"toggle","parent":"","needs":"","on":false,"wanted":true,"update":false,"new":false,"selectable":true},{"id":"extended_controller_support","label":"Extended controller support","hint":"Xbox wireless dongle (xone), Xbox controllers over Bluetooth (xpadneo)","kind":"toggle","parent":"","needs":"","on":false,"wanted":false,"update":false,"new":false,"selectable":true},{"id":"cec","label":"HDMI-CEC","hint":"use Steam with the TV remote, TV on/off with the PC (experimental)","kind":"toggle","parent":"","needs":"","on":false,"wanted":false,"update":false,"new":false,"selectable":true}]}
@@ -0,0 +1 @@
{"event":"plan","disable":[],"enable":[]}
@@ -0,0 +1 @@
{"event":"plan","disable":[],"enable":[]}
@@ -0,0 +1 @@
{"event":"plan","disable":[],"enable":[]}
@@ -0,0 +1 @@
{"event":"plan","disable":[],"enable":["nvidia","bigpicture"]}
@@ -0,0 +1 @@
{"event":"plan","disable":[],"enable":[]}
@@ -0,0 +1 @@
{"event":"plan","disable":[],"enable":["single"]}
@@ -0,0 +1 @@
{"version":"2.12.0","firstRun":true,"steamMachine":false,"kernel":"7.0.0-fixture-cachyos","pinnedKernel":"7.1.6-1","cecDevices":"","leds":0,"bios":null,"items":[{"id":"nvidia","label":"Gaming on NVIDIA","hint":"Steam on the desktop, started at login","kind":"toggle","parent":"","needs":"","on":false,"wanted":true,"update":false,"new":false,"selectable":true},{"id":"bigpicture","label":"Steam starts in Big Picture","hint":"the controller-friendly Steam (untick: Steam's normal window)","kind":"toggle","parent":"nvidia","needs":"","on":false,"wanted":true,"update":false,"new":false,"selectable":true},{"id":"nvsilent","label":"Start Steam silently","hint":"in the tray at login, no window (untick: Steam's normal window)","kind":"toggle","parent":"nvidia","needs":"","on":false,"wanted":false,"update":false,"new":false,"selectable":true},{"id":"theme","label":"Install SteamOS theme","hint":"Vapor look (cachyos-vapor)","kind":"toggle","parent":"","needs":"","on":false,"wanted":true,"update":false,"new":false,"selectable":true},{"id":"single","label":"Single user mode","hint":"no password, lock screen or log out (SDDM)","kind":"toggle","parent":"","needs":"","on":false,"wanted":true,"update":false,"new":false,"selectable":true},{"id":"launcher","label":"Steamify shortcut","hint":"the app on the desktop, Steamify Terminal in the launcher","kind":"toggle","parent":"","needs":"","on":false,"wanted":true,"update":false,"new":false,"selectable":true},{"id":"notify","label":"Update notifications","hint":"a notification when there's a new Steamify, never updates by itself","kind":"toggle","parent":"","needs":"","on":false,"wanted":true,"update":false,"new":false,"selectable":true},{"id":"extended_controller_support","label":"Extended controller support","hint":"Xbox wireless dongle (xone), Xbox controllers over Bluetooth (xpadneo)","kind":"toggle","parent":"","needs":"","on":false,"wanted":false,"update":false,"new":false,"selectable":true},{"id":"cec","label":"HDMI-CEC","hint":"use Steam with the TV remote, TV on/off with the PC (experimental)","kind":"toggle","parent":"","needs":"","on":false,"wanted":false,"update":false,"new":false,"selectable":true}]}
@@ -0,0 +1 @@
{"event":"plan","disable":[],"enable":["gaming","boot"]}
@@ -0,0 +1 @@
{"event":"plan","disable":[],"enable":["gaming"]}
@@ -0,0 +1 @@
{"event":"plan","disable":[],"enable":[]}
@@ -0,0 +1 @@
{"event":"plan","disable":[],"enable":[]}
@@ -0,0 +1 @@
{"event":"plan","disable":[],"enable":["machine","poweroff"]}
@@ -0,0 +1 @@
{"event":"plan","disable":[],"enable":["gaming","single"]}
@@ -0,0 +1 @@
{"version":"2.12.0","firstRun":true,"steamMachine":true,"kernel":"7.0.0-fixture-cachyos","pinnedKernel":"7.1.6-1","cecDevices":"","leds":0,"bios":{"current":"unknown","newest":"","selectable":false,"dryRun":false},"items":[{"id":"gaming","label":"SteamOS conversion","hint":"boot into gaming mode, Steam on the desktop","kind":"toggle","parent":"","needs":"","on":false,"wanted":true,"update":false,"new":false,"selectable":true},{"id":"boot","label":"Boot into the desktop instead of gaming mode","hint":"","kind":"choice","parent":"gaming","needs":"","on":false,"wanted":false,"update":false,"new":false,"selectable":true},{"id":"silent","label":"Start Steam silently in desktop mode","hint":"in the tray at login on the desktop, no window","kind":"toggle","parent":"gaming","needs":"","on":false,"wanted":false,"update":false,"new":false,"selectable":true},{"id":"glyphs","label":"Install Steam Deck/Machine icons","hint":"Deck button icons in gaming mode","kind":"toggle","parent":"gaming","needs":"","on":false,"wanted":true,"update":false,"new":false,"selectable":true},{"id":"theme","label":"Install SteamOS theme","hint":"Vapor look (cachyos-vapor)","kind":"toggle","parent":"","needs":"","on":false,"wanted":true,"update":false,"new":false,"selectable":true},{"id":"single","label":"Single user mode","hint":"no password, lock screen or log out (SDDM)","kind":"toggle","parent":"","needs":"","on":false,"wanted":true,"update":false,"new":false,"selectable":true},{"id":"launcher","label":"Steamify shortcut","hint":"the app on the desktop, Steamify Terminal in the launcher","kind":"toggle","parent":"","needs":"","on":false,"wanted":true,"update":false,"new":false,"selectable":true},{"id":"notify","label":"Update notifications","hint":"a notification when there's a new Steamify, never updates by itself","kind":"toggle","parent":"","needs":"","on":false,"wanted":true,"update":false,"new":false,"selectable":true},{"id":"extended_controller_support","label":"Extended controller support","hint":"Xbox wireless dongle (xone), Xbox controllers over Bluetooth (xpadneo)","kind":"toggle","parent":"","needs":"","on":false,"wanted":false,"update":false,"new":false,"selectable":true},{"id":"cec","label":"HDMI-CEC","hint":"use Steam with the TV remote, TV on/off with the PC (experimental)","kind":"toggle","parent":"","needs":"","on":false,"wanted":true,"update":false,"new":false,"selectable":true},{"id":"machine","label":"Steam Machine support","hint":"LED bar driver, hardware settings in Steam","kind":"toggle","parent":"","needs":"","on":false,"wanted":true,"update":false,"new":false,"selectable":true},{"id":"poweroff","label":"Power-off fix","hint":"the Steam Machine stays off after shutting down","kind":"toggle","parent":"machine","needs":"","on":false,"wanted":true,"update":false,"new":false,"selectable":true},{"id":"bios","label":"Update BIOS","hint":"now unknown, newest unknown (offline?)","kind":"action","parent":"machine","needs":"","on":false,"wanted":false,"update":false,"new":false,"selectable":false}]}
@@ -0,0 +1,3 @@
NAME="CachyOS Linux"
ID=cachyos
ID_LIKE=arch
+2
View File
@@ -0,0 +1,2 @@
sddm
plasma-desktop
@@ -0,0 +1 @@
PRIME B650
@@ -0,0 +1 @@
ASUSTeK
@@ -0,0 +1,3 @@
NAME="CachyOS Linux"
ID=cachyos
ID_LIKE=arch
+1
View File
@@ -0,0 +1 @@
nvidia_drm
+3
View File
@@ -0,0 +1,3 @@
sddm
plasma-desktop
nvidia-utils
@@ -0,0 +1 @@
PRIME B650
@@ -0,0 +1 @@
ASUSTeK
@@ -0,0 +1 @@
0x030000
@@ -0,0 +1 @@
0x10de
@@ -0,0 +1,3 @@
NAME="CachyOS Linux"
ID=cachyos
ID_LIKE=arch
@@ -0,0 +1,2 @@
sddm
plasma-desktop
@@ -0,0 +1 @@
Fremont
@@ -0,0 +1 @@
Valve
+33
View File
@@ -0,0 +1,33 @@
#!/bin/bash
# Record what the bash backend (steamify.sh in $REPO) prints, per fixture, into share/golden/expected/.
# share/golden/record.sh record all | share/golden/record.sh check compare with the recorded files
set -uo pipefail
here="$(cd "$(dirname "$0")" && pwd)"
export REPO="${REPO:-$here/../../../steamify-cachyos}"; REPO="$(cd "$REPO" && pwd)"
backend="${BACKEND:-bash $REPO/steamify.sh}"
mode="${1:-record}"; failures=0; work="$(mktemp -d)"; trap 'rm -rf "$work"' EXIT
# case name -> backend arguments
# status: the whole output. plan-*: only the first line of the apply stream (the plan event, where the rules live);
# the steps after it need the real system (the VM suite), they are not stable with a fake sudo.
cases=("status:--backend status"
"plan-gaming:--backend apply gaming"
"plan-gaming-desktop:--backend apply --boot desktop gaming boot"
"plan-single:--backend apply single"
"plan-poweroff:--backend apply poweroff"
"plan-nothing:--backend apply"
"plan-nvidia:--backend apply nvidia bigpicture")
for fixture in "$here"/fixtures/*/; do
name="$(basename "$fixture")"
for entry in "${cases[@]}"; do
label="${entry%%:*}"; arguments="${entry#*:}"
expected="$here/expected/$name.$label.json"; mkdir -p "$here/expected"
if [[ "$label" == plan-* ]]; then # the plan event comes first; then the steps run for real, so stop after a moment
timeout -s KILL 5 "$here/sandbox.sh" "$fixture" $backend $arguments > "$work/stream" 2>&1
actual="$(head -n 1 "$work/stream")"
else actual="$("$here/sandbox.sh" "$fixture" $backend $arguments 2>&1)"; fi
if [[ "$mode" == record ]]; then printf '%s\n' "$actual" > "$expected"; echo "recorded $expected"
elif ! diff -u "$expected" <(printf '%s\n' "$actual"); then echo "FAIL $name $label"; failures=$((failures+1))
else echo "PASS $name $label"; fi
done
done
exit $failures
+32
View File
@@ -0,0 +1,32 @@
#!/bin/bash
# Run a command in a throw-away copy of a machine described by a fixture folder (no VM, no root).
# share/golden/sandbox.sh <fixture> <command>...
# <fixture>/etc/**, <fixture>/sys/**, <fixture>/usr/** : files shown at those places (the rest of /etc and /sys is empty)
# <fixture>/home/ : the user's home (fresh copy each run)
# <fixture>/packages, enabled-units, active-units : answers of the fake pacman and systemctl (shims/)
# <fixture>/env : optional VAR=value lines for the command
# Needs bubblewrap. $GOLDEN_LOG collects the writes the shims were asked to make.
set -euo pipefail
here="$(cd "$(dirname "$0")" && pwd)"
fixture="$(cd "${1:?usage: $0 <fixture> <command>...}" && pwd)"; shift
work="$(mktemp -d)"; trap 'rm -rf "$work"' EXIT
mkdir -p "$work/home" "$work/fixture"
cp -a "$fixture/." "$work/fixture/"
[[ -d "$fixture/home" ]] && cp -a "$fixture/home/." "$work/home/"
arguments=(--die-with-parent --unshare-pid --ro-bind / / --dev /dev --proc /proc --tmpfs /tmp --tmpfs /sys --tmpfs /etc
--bind "$work/home" "$HOME" --bind "$work/fixture" /mnt --ro-bind "$here/shims" /media)
[[ -n "${REPO:-}" ]] && arguments+=(--ro-bind "$REPO" "$REPO")
for keep in passwd group nsswitch.conf ld.so.cache ld.so.conf ld.so.conf.d localtime alternatives resolv.conf hostname; do
[[ -e "/etc/$keep" ]] && arguments+=(--ro-bind "/etc/$keep" "/etc/$keep")
done
for root in etc sys; do
[[ -d "$fixture/$root" ]] || continue
while IFS= read -r -d '' file; do
arguments+=(--ro-bind "$work/fixture/${file#"$fixture/"}" "/${file#"$fixture/"}")
done < <(find "$fixture/$root" -type f -print0)
done
environment=(--setenv FIXTURE /mnt --setenv PATH "/media:$PATH" --setenv GOLDEN_LOG "${GOLDEN_LOG:-/mnt/writes.log}")
[[ -f "$fixture/env" ]] && while IFS= read -r line; do
[[ "$line" == *=* ]] && environment+=(--setenv "${line%%=*}" "${line#*=}")
done < "$fixture/env"
exec bwrap "${arguments[@]}" "${environment[@]}" "$@"
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
# Fake modinfo: modules listed in $FIXTURE/modules exist.
for module in "$@"; do [[ "$module" == -* ]] || grep -qx -- "$module" "$FIXTURE/modules" 2>/dev/null || exit 1; done
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
# Fake pacman for the golden sandbox: the installed packages are listed in $FIXTURE/packages (one name per line).
# Only the queries the status code makes: -Q/-Qq <pkg>, -T <pkg>..., -Qi <pkg>. Writes (-S, -U, -R) are logged and succeed.
list="$FIXTURE/packages"; touch "$list" 2>/dev/null
case "$1" in
-T) shift; for package in "$@"; do grep -qx -- "$package" "$list" || echo "$package"; done; [[ $# -gt 0 ]] ;;
-Q|-Qq|-Qi|-Qs) shift; status=0; for package in "$@"; do [[ "$package" == -* ]] && continue; grep -qx -- "$package" "$list" || status=1; done; exit $status ;;
-Ss|-Si) exit 1 ;;
*) echo "pacman $*" >> "${GOLDEN_LOG:-/dev/null}"; exit 0 ;;
esac
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
# Fake sudo: logs the command and succeeds; nothing runs.
while [[ "$1" == -* ]]; do shift; done
echo "sudo $*" >> "${GOLDEN_LOG:-/dev/null}"
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
# Fake systemctl: enabled units listed in $FIXTURE/enabled-units, active ones in $FIXTURE/active-units.
# Queries answer from those lists; everything else is logged and succeeds.
verb=""; units=()
for argument in "$@"; do
[[ "$argument" == -* ]] && continue
if [[ -z "$verb" ]]; then verb="$argument"; else units+=("$argument"); fi
done
case "$verb" in
is-enabled) for unit in "${units[@]}"; do grep -qx -- "$unit" "$FIXTURE/enabled-units" 2>/dev/null || { echo disabled; exit 1; }; done; echo enabled ;;
is-active) for unit in "${units[@]}"; do grep -qx -- "$unit" "$FIXTURE/active-units" 2>/dev/null || { echo inactive; exit 3; }; done; echo active ;;
*) echo "systemctl $*" >> "${GOLDEN_LOG:-/dev/null}"; exit 0 ;;
esac
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
if [[ "$1" == -r ]]; then echo 7.0.0-fixture-cachyos; else command uname "$@"; fi
+42
View File
@@ -0,0 +1,42 @@
import sys, os, importlib.machinery, importlib.util, json
os.environ["QT_QPA_PLATFORM"] = "offscreen"
sys.argv = ["steamify-ui"]
loader = importlib.machinery.SourceFileLoader("ui", sys.argv[0] if False else os.environ["UI"])
spec = importlib.util.spec_from_loader("ui", loader); ui = importlib.util.module_from_spec(spec); loader.exec_module(ui)
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtCore import QTimer, QObject, QMetaObject, Q_ARG, Qt
app = QGuiApplication(sys.argv)
backend = ui.Backend(ui.find_script()); pad = ui.Gamepad(); engine = QQmlApplicationEngine()
engine.setInitialProperties({"backend": backend, "gamepad": pad, "fullscreen": False, "fromSteam": False, "iconUrl": ""})
engine.load(os.path.join(ui.HERE, "qml", "Main.qml"))
assert engine.rootObjects(), "QML did not load"
root = engine.rootObjects()[0]; backend.refresh()
def find_state():
for o in root.findChildren(QObject):
if o.property("want") is not None and o.property("resolution") is not None: return o
state = None
def step():
global state
state = find_state(); assert state, "no AppState"
def want(): return {k for k, v in state.property("want").toVariant().items() if v} if hasattr(state.property("want"),"toVariant") else {k for k,v in state.property("want").items() if v}
def toggle(i): QMetaObject.invokeMethod(state, "toggle", Qt.DirectConnection, Q_ARG("QVariant", i))
def vis():
r = state.property("resolution"); r = r.toVariant() if hasattr(r,"toVariant") else r
return set(r["visible"])
def plan():
r = state.property("resolution"); r = r.toVariant() if hasattr(r,"toVariant") else r
return [(p["id"], p["action"]) for p in r["plan"]]
print("start want", want(), "plan", plan(), "canApply", state.property("canApply"))
assert want() == {"theme"}
toggle("single"); print("after single", want(), plan())
assert want() == {"theme", "single", "gaming"} and plan() == [("gaming","on"),("single","on")]
assert "silent" in vis() and "poweroff" not in vis()
toggle("machine"); print("after machine", want(), vis())
assert "poweroff" in want() and "poweroff" in vis()
toggle("gaming"); print("after gaming off", want())
assert want() == {"theme", "machine", "poweroff"}
assert state.property("canApply") is True
print("UI OK"); app.quit()
QTimer.singleShot(4000, step)
sys.exit(app.exec())
@@ -0,0 +1,30 @@
# Go core (REFACTOR-2.12 phase 0/1): `steamify --backend apply` through the Go binary must emit the
# same event stream as the bash backend: enable two small items, then turn them off again, once with
# each backend, in the same VM. Needs the binary built in the repo (make build -> bin/steamify).
GO=/mnt/bin/steamify
[[ -x $GO ]] || { fail "no $GO (run make build in steamify-cachyos)"; exit 0; }
export STEAMIFY_NO_DOWNLOAD=1
backend_run() { # backend_run <label> <command...>: the event stream of one apply, log lines dropped
# Only the structure (plan, start, done, finished): the log lines of the first run hold pacman's install output, which the
# second run on the same machine does not repeat.
"$@" 2>&1 < /dev/null | sed 's/\x1b\[[0-9;]*m//g' | grep -v '^{"event":"log"'
}
status_json() { "$@" --backend status 2>/dev/null; }
info "status through both backends"
bash_status=$(bash /mnt/steamify.sh --backend status 2>/dev/null); go_status=$($GO --backend status 2>/dev/null)
[[ -n $bash_status && "$bash_status" == "$go_status" ]] && pass "status identical" || fail "status differs"
for step in "launcher notify" ""; do
# shellcheck disable=SC2086
bash_events=$(backend_run bash /mnt/steamify.sh --backend apply $step)
# put the machine back for the Go run
# shellcheck disable=SC2086
if [[ -n $step ]]; then backend_run bash /mnt/steamify.sh --backend apply >/dev/null; else backend_run bash /mnt/steamify.sh --backend apply launcher notify >/dev/null; fi
# shellcheck disable=SC2086
go_events=$(backend_run $GO --backend apply $step)
label="apply ${step:-nothing (turn off)}"
if [[ "$bash_events" == "$go_events" ]]; then pass "$label: same event stream ($(wc -l <<< "$bash_events") lines)"
else fail "$label: streams differ: $(diff <(echo "$bash_events") <(echo "$go_events") | head -6 | tr '\n' '|')"; fi
done
grep -q '"finished"' <<< "$go_events" && pass "Go run finished" || fail "no finished event: $(tail -n 2 <<< "$go_events")"