Compare commits

...
Author SHA1 Message Date
theupriser 17ae88a4ad feat(ci): sync the git.upriser.nl mirror on every push to main and every tag
Sync git.upriser.nl mirror / sync (push) Skipped
GitHub workflow calls the mirror-sync API so the Gitea build starts at once instead of at the mirror's 10 minute interval (secrets GIT_UPRISER_URL, GIT_UPRISER_TOKEN; retried; skipped on Gitea itself). The steamify-iso-release skill uses the renamed secret and workflows.
2026-09-30 08:11:24 +02:00
theupriser a9f20d69bf docs: rename the local ISO checkout to steamify-cachyos-live-iso
- scripts and skills use ../steamify-cachyos-live-iso
- TODO item 4: the ISO comes from the git.upriser.nl release
2026-09-30 07:47:37 +02:00
theupriser 9a351ae639 docs: use the renamed steamify-cachyos-live-iso repo in vmtest-remote.sh and TODO
- vmtest-remote.sh: name the ISO repo checkout steamify-cachyos-live-iso
- TODO.md: gh run download points at theupriser/steamify-cachyos-live-iso
2026-09-30 07:45:35 +02:00
theupriser 0d0279bd20 docs: skills and AGENTS.md after 2026-09-29: the ISO release pipeline (new skill), boot entry check and install gotchas, monitor rules, freeing WSL disk, deleting merged branches 2026-09-29 23:05:29 +02:00
theupriser 195518f0da docs: wsl-build-host: a hand-built ISO is named local (releases come from a GitHub tag) 2026-09-29 22:19:51 +02:00
theupriser 0447f35e7d fix: boot check: the power-off fix is loaded, or was tried at boot and refused (journal); it grepped dmesg for systemd's error, which only sometimes lands there 2026-09-29 20:17:27 +02:00
theupriser d614539d9e docs: full run with Steamify release/2.9.1: 341 pass; the CEC driver cache done 2026-09-29 19:46:08 +02:00
theupriser 847b024803 fix: test prelude sets SCRIPT_DIR (the libs read patches/ from it); hw block 50 checks every run for errors 2026-09-29 19:24:25 +02:00
theupriser 8df2e23314 fix: hw block 50: the private cache folder is readable like /var/cache/steamify (mktemp's 700 hid it from the user) 2026-09-29 19:22:16 +02:00
theupriser e6119ca194 feat: hw block 50: the CEC driver cache (new pin downloads and removes the old file, cache hit, damaged file replaced), offline via file:// 2026-09-29 19:19:52 +02:00
theupriser 6aad17f55c feat: the VMs share the host's cache for Steamify's own downloads (/var/cache/steamify: the CEC driver once for every VM, GitHub rate-limits it) 2026-09-29 19:16:45 +02:00
theupriser e8c57827b1 docs: B1 boot entry check and the ISO's /var/log fix done; CEC driver download (GitHub 429) as the next item 2026-09-29 19:09:16 +02:00
theupriser 4209f9625b docs: TODO: WSL starting with Windows is not Steamify's work (removed R1d) 2026-09-29 18:58:10 +02:00
theupriser 07a7da1d67 docs: TODO: R1d (WSL with Windows) left for now 2026-09-29 18:57:57 +02:00
theupriser a34efb0547 fix: vminstall.sh stops waiting as soon as the live system reports a failed install (it waited 90 minutes) 2026-09-29 18:48:27 +02:00
theupriser 20373c9f05 fix: vmprogress.sh reports a failed install (it stayed at 'install' for 18 minutes) 2026-09-29 18:48:14 +02:00
theupriser 27b7445f63 feat: B1 check: booted through the loader's own EFI boot entry (BootCurrent, a real partition), after every reboot in the boot loader test 2026-09-29 18:13:16 +02:00
theupriser a82053d6bc docs: the first full run on the PC (328 pass, 18 min): TESTPLAN, TODO, AGENTS, wsl-build-host; vmsuite.sh writes its summary into its log 2026-09-29 18:05:10 +02:00
theupriser 37d65fc79d fix: vmprogress.sh: only a new step, a finished job or a FAIL is a change (not every PASS) 2026-09-29 17:50:41 +02:00
theupriser 5b867f8bd2 docs: progress-report: a stopped local command leaves its remote loop running (known_hosts during an install) 2026-09-29 17:31:35 +02:00
theupriser e583494aa6 fix: vmprogress.sh: a new install percentage alone is no change (one line per stage) 2026-09-29 17:23:42 +02:00
theupriser fd9ecf4b9f feat: progress-report skill and scripts/vmprogress.sh (a status line per change); referenced from the other skills and AGENTS.md 2026-09-29 17:07:52 +02:00
theupriser 4cc4046716 feat: vmview.py: a window with a headless VM's screen (QMP screendump every 2 s, view only) 2026-09-29 17:05:17 +02:00
theupriser 9d537406bb fix: vminstall.sh sets up the ISO's loop device under a lock (parallel installs collided) 2026-09-29 16:28:19 +02:00
theupriser 7dd659dc09 fix: vminstall.sh in parallel: a poweroff that drops ssh is no failure, wait only for its own VM 2026-09-29 16:17:53 +02:00
theupriser d5cfadf3bb fix: vminstall.sh never makes root the guest user (WSL runs as root): theupriser then 2026-09-29 15:49:29 +02:00
theupriser cb247b9cc3 docs: wsl-build-host skill, vminstall.sh without udisks (WSL), AGENTS.md/TODO WSL notes corrected
The WSL box has a desktop after all (WSLg windows on Windows), closing the
Arch window shuts it down, and vminstall.sh loop-mounts the ISO with losetup
as root when udisksctl is missing (R1c).
2026-09-29 15:40:03 +02:00
theupriser 6a53c957ba docs: AGENTS.md: state, where the tests run (PC, WSL2) and the ISO problems found 2026-09-29 15:31:57 +02:00
theupriser 63fd11829d docs: the agent hand-off file is AGENTS.md (was CLAUDE.md) 2026-09-29 15:29:49 +02:00
theupriser f03ad74295 docs: CLAUDE.md: this repo takes pushes straight to main 2026-09-29 15:29:08 +02:00
theupriser 0c84b8b926 feat: faster and parallel tests, a remote runner, and a CLAUDE.md hand-off
vmsuite.sh: the ssh-ready snapshot becomes one base image, every block gets a new qcow2 overlay
(no snapshot restore), each suite has its own VM directory and ssh port (2301-2305, boot loaders
2401-2403), the host's package cache is bound over pacman's, VM_MEM defaults to 4G for tests
(run.sh VM_MEM, default 8G). vmtest.sh runs up to MAX_PARALLEL=3 jobs at once, slowest first.
scripts/vmtest-remote.sh runs vmtest.sh on another machine (the PC) over ssh and prints the
summary. CLAUDE.md and the branch cleanup skill now live in the repo.

Not proven as a whole: three VMs at once and the base image work, but a complete parallel run to
the end has not finished yet (P1/P2 in TODO.md); the remote runner and WSL2 are untried (R1-R2).
2026-09-29 15:26:57 +02:00
theupriser ee20bd9825 feat: vmtest.sh runs everything: boot loaders plus the cli, menu, hw, installer and toggles suites
vmtest.sh [--install] [--window] [--screen] [boot|loader|suite...], a detached screen
session with --screen, the summary in ~/vms/last-test.txt, the rows that need a person
listed as SKIP. New: the installer suite (R2.1), the toggles suite (R1.8) and H3 (BIOS
dry-run, per-block '# env:' for the VM start). vmsuite.sh and vmbootloadertest.sh are
executable again.
2026-09-29 14:41:37 +02:00
theupriser 6d264acaf2 feat: the hw suite (H1, H4, H5, R1.7 CEC) and host-side suite blocks
vmsuite.sh runs NN-name.host.sh blocks on the host (reboot, scripts/vmcec.sh) with
prelude-host.sh; every block that applies Steamify says so first (INFO) so the shared
console is not silent for minutes. 68 checks pass.
2026-09-29 14:12:39 +02:00
theupriser b91ef9172e feat: the menu suite (R1.1-R1.6, R1.10) and one shared log Konsole for every test run
Scripted menu input and Steamify's own status functions, journals free of duplicates,
--backend status as JSON, an older item shown as (update). The test scripts append to
$TEST_LOG and share one Konsole (common.sh view_start) instead of opening one each;
vmsuite.sh streams its output (sed -u). 36 checks pass.
2026-09-29 13:53:35 +02:00
theupriser fe5251f758 feat: vmsuite.sh and the cli suite: G1-G3, F1-F7 of TESTPLAN.md as unattended checks
Each block in share/vmtest/<suite>/ starts from a fresh ssh-ready snapshot, gets a
Plasma session environment over ssh (the theme item needs one) and prints PASS/FAIL
lines. ONLY=<prefix> runs one block. 55 checks pass on the plain CachyOS VM.
2026-09-29 11:45:02 +02:00
theupriser 508eb68ed5 docs: vm-install skill, TESTPLAN section B and TODO for the boot loader test and the full automated suite 2026-09-29 11:23:50 +02:00
theupriser c91277abf0 feat: vmtest.sh: the boot loader test for every loader the ISO advertises
One VM per loader, Steam Machine (--fremont): Steamify's state as its menu sees it,
OS name untouched, loader identity, the DKMS modules for every kernel, Steamify's
loader-specific code (hdmi_remove_boot_param), a kernel update, and a reboot into
each of the two kernels. Headless by default with a log Konsole (--window shows
the VMs). Fails when an advertised loader has no test. Limine 41, systemd-boot 40,
GRUB 40 checks pass.
2026-09-29 11:23:50 +02:00
theupriser 8646b3c30a feat: vminstall.sh installs any boot loader and the Steamify ISO's Steamify step, with a package cache
VM_BOOTLOADER=limine|systemd-boot|grub, VM_STEAMIFY (steamify-install runs after
the headless installer, like Calamares does), VM_CACHE (host pacman cache shared
as 9p, default ~/vms/pkg-cache). run.sh --headless / VM_HEADLESS=1: no window.

Fixes found on the way: the live script deadlocked on is-system-running (it is
the running kernel-command-line.service) and now waits for pacman-init; the post
script kept forcing the Plasma login over Steamify's gaming-mode SDDM, gave
systemd-boot no timeout/default (it waited in the menu for ever), and left ufw
blocking the host's ssh; the first-boot check wanted plasmashell in gaming mode.
2026-09-29 11:23:50 +02:00
theupriser fa776b1761 docs: steam-machine-iso: vmreset.sh also stops the ISO VM 2026-09-28 18:27:52 +02:00
theupriser 0ae7cce488 fix: vmreset.sh works when the VM lives in the repo itself (cp of run.sh onto itself failed) 2026-09-28 18:27:27 +02:00
theupriser 7db82b6494 docs: steam-machine-iso: read an installed VM's disk while it runs; serial output 2026-09-28 18:12:18 +02:00
theupriser 52d5021d88 docs: steam-machine-iso: one log Konsole for every build 2026-09-28 17:45:00 +02:00
theupriser f19bcb9764 docs: steam-machine-iso: a package cache for the podman ISO build 2026-09-28 17:42:37 +02:00
theupriser 5954548247 fix: qmptype.py types > < ? { } 2026-09-28 17:37:51 +02:00
theupriser bc06bc2534 feat: vmisobuild.sh and the steam-machine-iso skill run build-calamares-modules.sh 2026-09-28 16:41:22 +02:00
theupriser 0052f1ba22 docs: note token-cheap ISO-VM testing (serial log, no screenshot polling) 2026-09-28 15:35:25 +02:00
theupriser 61442d7ef8 docs: packagechooser multi-select needs Shift/Ctrl-click 2026-09-28 13:45:09 +02:00
theupriser 6d73d49477 docs: steam-machine-iso: run Calamares in its own Konsole tab; watch for the live keyboard layout switching away from US 2026-09-28 13:42:23 +02:00
theupriser fe249e5108 docs: correction - packagechooser mode:optionalmultiple DOES multi-select (verified from global storage, not misleading label text) 2026-09-28 13:30:09 +02:00
theupriser 4398a2b85f docs: packagechooserq isn't installed on this Calamares build; packagechooser's mode:optionalmultiple is single-select regardless; QMP/Calamares live-testing gotchas 2026-09-28 13:24:36 +02:00
theupriser 081080b5f5 docs: ISO builds use 75% of host cores, not all-but-2 2026-09-28 13:04:04 +02:00
theupriser b6b35d41f1 docs: TESTPLAN: 2.8.0 --defaults --list tested 2026-09-28 12:44:50 +02:00
theupriser 3ccbb62bd2 feat: vmisobuild.sh (ISO build in the VM, package cache per VM), vmisoboot.sh (ISO VM for Calamares), qmpshot.py/qmpclick.py; run.sh VM_CACHE/VM_CPUS/VM_NOGL/VM_PORT 2026-09-28 11:40:21 +02:00
theupriser fec0ce5b8e docs: TESTPLAN: 2.7.0's Steam Machine checks carried over from 2.6.0 (code unchanged) 2026-09-28 10:56:17 +02:00
theupriser d0038916e9 docs: 2.7.0 regression results on the new VM; the first login's app is the newest release 2026-09-28 10:53:44 +02:00
theupriser 4905bba1f6 docs: vminstallsim.sh example uses --options 2026-09-28 10:08:27 +02:00
theupriser 60a2698d02 feat: scripts/vminstall.sh installs an ssh-ready test VM unattended; TESTPLAN.md; vm-install skill 2026-09-28 10:07:13 +02:00
theupriser 21547dd7bc docs: steam-machine-iso: one build at a time; vminstallsim.sh passes --defaults options 2026-09-27 22:00:32 +02:00
theupriser 96cae35c6b docs: steam-machine-iso: build-live-modules.sh in the build, the ISO's new name 2026-09-27 21:26:45 +02:00
theupriser e1dd75875b docs: steam-machine-iso: the Steamify step is added by calamares-online.sh 2026-09-27 20:06:26 +02:00
theupriser f8aac02d0a docs: steam-machine-iso: live-session quirks; scripts/qmptype.py types into the VM 2026-09-27 20:01:41 +02:00
theupriser 8d5cdaea99 docs: steam-machine-iso: the harmless chown error, the ISO test VM 2026-09-27 19:54:30 +02:00
theupriser f8e2473afa feat: steam-machine-iso skill (ISO build in podman on the Steam Machine, installer simulation, first-login test) and scripts/vminstallsim.sh 2026-09-27 19:53:23 +02:00
theupriser 088ee83350 VM on the Steam Machine: Arch OVMF paths, visible tests, and the steam-machine-testing skill 2026-09-27 17:00:10 +02:00
theupriser d626aafd84 docs: VM skill up to date with the menu (sub-options, HDMI-CEC, kernel pin) and today's pitfalls 2026-09-25 13:07:02 +02:00
theupriser 6da46fc10f docs: The test VM needs an x86_64 Linux host 2026-09-25 13:05:15 +02:00
theupriser 0464ede893 chore: Remove the Windows scripts; the test VM runs on Linux 2026-09-25 13:04:20 +02:00
theupriser 4aeb3de43f docs: run.sh --amd (venus on the AMD iGPU); gaming mode can't render in the VM, all tried 2026-09-25 12:54:27 +02:00
theupriser 04204f1b9d feat: vmcec.sh tests HDMI-CEC against a fake TV; vmwake.sh wakes the VM over QMP
run.sh opens a QMP socket; the skill documents the CEC test and its gotchas.
2026-09-25 11:14:00 +02:00
theupriser e04db1ec5f docs: VM skill: menu with sub-options, HDMI-CEC and faking it with vivid 2026-09-25 09:12:12 +02:00
theupriser a8d7554dce Merge pull request #1 from theupriser/feat/visible-vm-runs
Feat/visible vm runs
2026-09-24 13:09:16 +02:00
63 changed files with 3511 additions and 176 deletions

No files matched your search

+226 -21
View File
@@ -9,14 +9,25 @@ The wizard (`steamify.sh` + `lib/*.sh`) opens a menu that detects
which components are on and toggles them to match the user's choice. Menu order:
1. SteamOS conversion (boot into gaming mode via autologin, Return to Gaming Mode shortcut, Steam desktop autostart)
2. SteamOS theme (installs `cachyos-vapor` and applies the Vapor global theme with its desktop and window layout; needs a running Plasma session)
3. Steam Deck/Machine icons (`STEAM_GAMEPADUI_ARGS -steamos3`)
4. Single user mode (SDDM, no lock screen/user switching/log out; enabling it enables 1, disabling 1 disables it)
5. Steamify shortcut (desktop icon + launcher entry that `curl | bash` the newest release; its gear icon is a release asset)
6. Steam Machine support (only on DMI Valve/Fremont: leds-valve-dkms-git built for every kernel via `/etc/dkms/leds-valve-dkms.conf`, `ensure-kernel-headers.service`, udev rule, steamos-manager, powerdevilrc)
7. Update BIOS (only on Fremont, an opt-in action, never pre-ticked; tickable only when Valve has a newer BIOS)
2. └ Boot into: gamescope / desktop (sub-option of 1, shown while 1 is ticked; Left/Right or the number switches it)
3. SteamOS theme (installs `cachyos-vapor` and applies the Vapor global theme with its desktop and window layout; needs a running Plasma session)
4. Steam Deck/Machine icons (`STEAM_GAMEPADUI_ARGS -steamos3`)
5. Single user mode (SDDM, no lock screen/user switching/log out, Valve's empty KDE wallet; enabling it enables 1, disabling 1 disables it)
6. Steamify shortcut (desktop icon + launcher entry that `curl | bash` the newest release; its gear icon is a release asset)
7. HDMI-CEC (any PC; pre-ticked on a first run only on Fremont: Valve's cecd/cec-audio-control/inputattach-cec-units from the holo repo, the steam-launcher drop-in that overrides `STEAM_ENABLE_CEC=0`; see "Fake HDMI-CEC")
8. Steam Machine support (only on DMI Valve/Fremont: leds-valve-dkms-git built for every kernel via `/etc/dkms/leds-valve-dkms.conf`, `ensure-kernel-headers.service`, udev rule, steamos-manager, powerdevilrc)
9. └ Power-off fix (sub-option of 8, ticked along with it, opt-out: DKMS module `steamify-fremont-poweroff` for every kernel; in the VM it loads but finds no real GPIO wake bit to clear)
10. └ Update BIOS (only on Fremont, an opt-in action, never pre-ticked; tickable only when Valve has a newer BIOS)
Without `--fremont` the menu has only 1-5.
Since 2.2.0 "Pin the kernel" and "HDMI refresh boost" are only shown while
something of them is left, unticked, so a normal run removes them. The menu
marks components a normal run will update with "(update)" (feature versions
in `~/.local/state/cachyos-gamescope-boot/features.state`; set an entry to an
older version, e.g. `machine=2.1.0`, to test the update flow in the menu and
the app).
Without `--fremont` the menu has only 1-7. Numbers shift when a parent is
unticked (its sub-option is hidden), so read the ticks with `'q\n'` first.
Undo journals live in `~/.local/state/cachyos-gamescope-boot/` in the guest.
@@ -29,13 +40,81 @@ The VM's disk, `vars*.fd` and `run.sh` copy may live outside the repo
snapshot commands below run in that directory. `scripts/vmreset.sh` takes
`VM_DIR` and defaults to the repo if it holds `disk.qcow2`, else `~/vms/cachyos-test`.
## Always let the user watch
The user follows the tests in the VM's window (on the Steam Machine, over
Moonlight). **Every run is visible**: run the wizard and every other test
command in the tmux-backed Konsole on the VM's desktop ("Visible terminal"
below), never hidden over SSH. When a run was started hidden anyway, open a
Konsole that follows its log (`tail -n +1 -f <log>`) right away. **Show the
app too**: start it on the VM's desktop (`systemd-run --user
/mnt/ui/steamify-ui`) for the screens you test, and screenshot it.
Long unattended jobs (installs, `vmtest.sh`): report them as the
`progress-report` skill says.
## The VM on the Steam Machine
The Mac has no KVM, so the VM runs on the Steam Machine
(`~/projects/steamify-cachyos-dev`, `REPO=~/projects/steamify-cachyos`; see
the steam-machine-testing skill). From the Mac:
- Connect with agent forwarding (`ssh -A steammachine`), then
`ssh -p 2222 -o UserKnownHostsFile=~/projects/steamify-cachyos-dev/known_hosts theupriser@localhost`.
The Steam Machine has no key of its own: `share/host-keys.pub` holds the
Mac's key (`ssh-add -L`), and the VM's host key stays in `~/projects`, not
`~/.ssh`.
- Inner `ssh` in a `bash -s` heredoc reads the rest of the heredoc as its
stdin: use `ssh -n` for single commands, or the rest of the script silently
never runs.
- Start the VM as a user unit so it survives the SSH session:
`systemd-run --user --collect -u steamify-vm --working-directory=$PWD --setenv=REPO=$HOME/projects/steamify-cachyos ./run.sh --fremont`.
- A fresh install has no `/media`: the guest setup is
`sudo mkdir -p /media && sudo mount -t 9p -o trans=virtio,version=9p2000.L vmtools /media && /media/guest-ssh-setup.sh`.
## No VM yet
Create one with `scripts/vminstall.sh` (the vm-install skill): unattended,
ends with the snapshots `clean` and `ssh-ready`. Pass its `VM_DIR` to every
script. Login by hand: your host username, password `steamify`.
## Test plan
`TESTPLAN.md` (repo root) lists what to test per release (regression,
features, Steam Machine only) and logs every run. Follow it, add a section
for every new feature, and add a line to its results log after each run.
## Start of every session: update the snapshot first
A snapshot's package databases age quickly: the mirrors drop the versions it
knows (404s, then "signature is invalid" on partial downloads) and the
conversion's package install fails, which looks like a wizard bug. So once
per session, before any test, bring `ssh-ready` up to date and overwrite it
(visibly, in a Konsole on the VM's desktop):
```bash
scripts/vmreset.sh --fremont
# in the guest (Konsole): keyrings first, then everything else
sudo pacman -Sy --noconfirm archlinux-keyring cachyos-keyring
sudo pacman -Su --noconfirm --needed tmux shellcheck
sudo pacman -Scc --noconfirm
sudo systemctl poweroff
# on the host, in the VM dir, once QEMU is gone:
qemu-img snapshot -d ssh-ready disk.qcow2
qemu-img snapshot -c ssh-ready disk.qcow2 && cp vars.fd vars.ssh-ready.fd
```
Keep `/etc/plasmalogin.conf.d/00-test-autologin.conf` in the snapshot: the
VM then logs in at boot. After a plasmalogin update, restarting it from a
booted greeter fails (`HELPER_TTY_ERROR`, start-limit-hit), so `vmreset.sh`
only restarts it when Plasma isn't up yet.
## Quick start (the usual loop)
```bash
scripts/vmreset.sh --fremont # restore ssh-ready, boot, mount repo, autologin, wait for Plasma
# (also skips the broken krfoss mirror, installs shellcheck)
scripts/cmp.sh save # baseline of the KDE configs
scripts/vmwatch.sh '1\n3\n\ny\n' "Theme only" # wizard in a visible Konsole in the VM
scripts/vmwatch.sh 'q\n' "Menu" # wizard in a visible Konsole in the VM (read the ticks first)
scripts/vmwatch.sh --release '\ny\nm\nq\n' "Full run" # the newest GitHub release instead of /mnt
scripts/vmstate.sh # component state
scripts/vmshot.sh --clean /path/shot.png # screenshot (--clean: close Steam/Hello/Konsole first)
@@ -58,7 +137,7 @@ window: `vmrun.sh` runs invisibly over SSH, so they see nothing happen.
Start in the background:
```bash
./run.sh --fremont > /tmp/vm.log 2>&1 & # flags: [install] [--fremont] [--nvidia] [--vulkan]
./run.sh --fremont > /tmp/vm.log 2>&1 & # flags: [install] [--fremont] [--nvidia] [--vulkan [--amd]]
```
Wait for SSH:
@@ -86,7 +165,8 @@ qemu-img snapshot -c my-state disk.qcow2 && cp vars.fd vars.my-state.fd #
```
Existing snapshots: `clean` (fresh install) and `ssh-ready` (sshd + host key +
passwordless sudo). Reset to `ssh-ready` before each full test run.
passwordless sudo, test autologin; brought up to date at the start of each
session, see above). Reset to `ssh-ready` before each full test run.
## First-time guest setup
@@ -127,6 +207,46 @@ echo "$HOME"
EOF
```
## Visible terminal (let the user watch)
When the user watches the VM's window, run commands in a Konsole window on
the guest's desktop instead of hidden over SSH: they see every command and
its output. A tmux session makes that window scriptable (needs a Plasma
session; `sudo pacman -S --needed tmux` once, or put it in the snapshot).
```bash
ssh -p 2222 -o BatchMode=yes theupriser@localhost bash -s << 'EOF'
export XDG_RUNTIME_DIR=/run/user/1000 WAYLAND_DISPLAY=wayland-0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
tmux kill-session -t claude 2>/dev/null
# Plain bash, not fish: fish rejects bash syntax ($?) and swallows Enter
# keys sent while it's still drawing its prompt.
tmux new-session -d -s claude -x 200 -y 50 'bash --norc --noprofile -i'; sleep 1
tmux send-keys -t claude "PS1='[claude] \\w\\$ '; clear" Enter
systemd-run --user -q --setenv=XDG_CURRENT_DESKTOP=KDE konsole --separate -e tmux attach -t claude
EOF
```
Run a command and wait for it: end it with a marker, then poll the pane.
```bash
ssh -p 2222 -o BatchMode=yes theupriser@localhost bash -s << 'EOF'
tmux send-keys -t claude "clear; cd /mnt; printf 'q\\n' | ./steamify.sh; echo '== EXIT'" Enter
for i in $(seq 200); do tmux capture-pane -p -t claude -S -400 | grep -q '^== EXIT' && break; sleep 3; done
tmux capture-pane -p -t claude -S -400 | grep -v '^\s*$' | tail -30
EOF
```
- `clear` first, or an old marker still in the pane ends the wait at once;
with several runs, count the markers (`grep -c`).
- Longer scripts: write them to a file in the heredoc (not `/tmp` if a
reboot comes in between), then `tmux send-keys -t claude 'clear; bash <file>' Enter`.
- For long runs (packages, yay, DKMS), poll from a `run_in_background` Bash
call and report when it finishes.
- A reboot or snapshot restore ends the session and the window: set it up
again before sending anything, or `send-keys` fails
(`error connecting to /tmp/tmux-1000/default`) while a wait loop spins.
- `vmshot.sh --clean` closes Konsole windows, including this one.
## Getting a Plasma session
On a fresh snapshot the plasmalogin greeter is shown. Enable test autologin:
@@ -163,19 +283,22 @@ line at the restart question.
| Input | Meaning |
|---|---|
| `'\ny\nm\nq\n'` | first run, accept all (not the BIOS item), back to the menu, quit |
| `'2\n\ny\n'` | toggle the theme |
| `'\ny\nm\nq\n'` | first run, accept what's pre-ticked (not Boot into desktop, not BIOS; HDMI-CEC only on Fremont), back to the menu, quit |
| `'3\n\ny\n'` | toggle the theme (while 1 is ticked) |
| `'7\n\ny\nm\nq\nn\n'` | toggle HDMI-CEC (while 1 is ticked) |
| `'a\ny\nm\nq\n'` | re-apply what is on (includes the conversion, so a restart is pending) |
| `'q\n'` | just show the menu |
| `'1\n3\n5\n\ny\n'` | from all-off: theme only (with `--fremont` also add `6\n` to drop Steam Machine support) |
| `'1\n\ny\n'` | from a state where 1 is off: turn the conversion on |
**Know the starting ticks before choosing input.** When *everything* is off
(fresh snapshot, or after turning the last component off), the menu treats it
as a first run and pre-ticks **all** components, so `2` means "theme off,
rest on". Otherwise the ticks show what is on now. Unticking 1 also unticks 4,
and ticking 4 ticks 1 again, so `'1\n3\n4\n...'` ends with the conversion on.
When unsure, run `'q\n'` first and read the ticks.
as a first run and pre-ticks the components (not Boot into desktop, BIOS, or
HDMI-CEC off Fremont). Otherwise the ticks show what is on now. Unticking 1
also unticks single user mode (5) and **hides** the Boot into row (2), so
every later number moves up one; the same for Steam Machine support (8) and
its kernel pin row (9). Ticking 5 ticks 1 again. The numbers apply to the
menu as it is when you type them, one line at a time. When unsure, run
`'q\n'` first and read the ticks.
`a` only re-applies components that are already on: it does not retry one that
failed. Re-applying the conversion also resets the autologin session to
@@ -183,6 +306,20 @@ gamescope, so set it back to plasma before the next reboot (below).
## Gamescope does not render in this VM
Everything was tried (2026-09-25); don't spend time on it again:
- virgl (default): no Vulkan, black.
- `--vulkan --amd` (venus on the host 780M, RADV): gamescope and Steam run,
but virtio-gpu rejects every framebuffer gamescope makes ("Cannot import FB
to DRM ... not supported for scan-out", AR24 and XR24, also with
`--force-composition --disable-layers`): black. KWin crashes on venus
("Illegal command buffer"), so Plasma freezes too.
- nested gamescope with lavapipe (`vulkan-swrast`): refuses to start, lavapipe
lacks VK_KHR_present_id/present_wait.
- Other hypervisors (VirtualBox, VMware) have no Vulkan at all.
Check gaming mode (Steam's settings, HDMI-CEC section, refresh rates) on the
real Steam Machine. The VM covers everything else.
(Also after any re-apply that includes the conversion.)
No suitable Vulkan (venus is unstable with the host NVIDIA driver). After
@@ -242,13 +379,20 @@ autologin, then (all passed last run; `scripts/vmstate.sh` after every step):
1. Fresh run turning everything on (`'\ny\nm\nq\n'`), set session to plasma, reboot:
SDDM logs straight in, Vapor desktop, three desktop icons (Return to Gaming
Mode, Steam, Steamify CachyOS with the gear icon), LEDs loaded (17 nodes).
Mode, Steam, Steamify CachyOS with the gear icon), LEDs loaded (17 nodes),
`uname -r` 7.1.6-1-cachyos and `pacman -Qu` shows linux-cachyos `[ignored]`,
`~/.local/share/kwalletd/kdewallet.kwl.bak-steamify` next to Valve's empty wallet,
`steamos-manager-configure-cecd` enabled and `~/.config/cecd/config.d/00-steamos-manager.toml` written.
2. Rerun: all shown on, "Everything is already the way you want it".
3. `a` re-apply: no duplicate journal entries.
4. Theme on: Steam Deck wallpaper, full-width 46px panel, `distributor-logo-steamdeck` launcher icon, `dark-lnf=com.valve.vapor.desktop` (Brightness & Color's Dark Mode toggle is on, hint "Switch to Breeze"); with single user on, kickoff keeps `primaryActions=3`. Theme off: CachyOS wallpaper, floating 30px panel, CachyOS launcher icon, BreezeDark colors (not light), `cachyos-vapor` removed, `cmp.sh` clean.
5. Single user off: switches to plasmalogin + sync bridge + sudoers; shortcut Exec uses `sudo -n`.
6. Single user on: back to SDDM.
7. Icons + Steam Machine support off: driver, udev rule, modules-load, steamos-manager removed; yay kept.
7. Icons + Steam Machine support off: driver, udev rule, modules-load, steamos-manager removed; yay kept;
kernel pin removed (`IgnorePkg` empty, `pacman -Syu` back to the current kernel), files kept in
`/var/cache/steamify/kernel`. On again (even with the CachyOS servers blocked in
`/etc/hosts`): installs 7.1.6 from that directory; DKMS builds the LED driver once per kernel.
7c. HDMI-CEC: load vivid, then `scripts/vmcec.sh` (all PASS), off/on via the menu.
7b. DKMS: `vmstate.sh` shows `installed` for every kernel. Headers at boot:
`sudo pacman -R --noconfirm linux-cachyos-lts-headers` (DKMS drops the LTS
build), set the session to plasma, reboot, then `journalctl -b -u
@@ -264,12 +408,55 @@ differs from Valve's newest. Fake an older one with
the environment through). fwupd correctly refuses the firmware in the VM
("not for this machine's hardware"), so walk the whole flow with
`WIZARD_BIOS_DRY_RUN=1` (skips only that check, never flashes). Scripted:
`printf '7\n\ny\ny\nUPDATE\nm\nq\nn\n' | WIZARD_BIOS_DRY_RUN=1 /mnt/steamify.sh`
(BIOS is item 7 with `--fremont`; the final `n` answers the restart question).
`printf '10\n\ny\ny\nUPDATE\nm\nq\nn\n' | WIZARD_BIOS_DRY_RUN=1 /mnt/steamify.sh`
(BIOS is item 10 with `--fremont`; the final `n` answers the restart question).
A successful dry run counts as staged, so `[m]`/`[r]` and the restart question
on `q` show up too; in dry-run mode "restart" only prints.
## Fake HDMI-CEC (vivid): `scripts/vmcec.sh`
The VM has no CEC hardware; the kernel's `vivid` test driver emulates it: a
virtual HDMI input (`/dev/cec0`, plays the TV via `cec-ctl`/`cec-follower`)
and output (`/dev/cec1`, the PC, where cecd runs). Needs the HDMI-CEC item on;
the Steam-settings checks need Steam Machine support (steamos-manager).
```bash
scripts/vmcec.sh # all checks + a real suspend/resume (~5 min)
scripts/vmcec.sh --no-sleep # without suspending the VM
scripts/vmcec.sh --sleep-only # only the suspend/resume check
scripts/vmwake.sh # wake a sleeping VM (QMP socket from run.sh)
```
It prints PASS/FAIL/SKIP for: identity (address 1.0.0.0, name "Steam
Machine", Valve vendor ID), every TV remote key and the key it becomes, the
Steam settings over D-Bus (remote control, WakeTv, SuspendTv, SuspendDevice
and cecd's config), PC to TV (make active, wake, volume, audio status,
standby), TV standby putting the PC to sleep (blocked by an inhibitor), and
sleep/wake turning the TV off and on.
Gotchas it handles, learned the hard way:
- The cecd service (`-e`) grabs every CEC device, the fake TV too: the test
uses a temporary drop-in (`~/.config/systemd/user/cecd.service.d/vmcec.conf`)
with `-d /dev/cec1`, removed at the end.
- The TV's power button (KEY_POWER) and TV standby with SuspendDevice on
really suspend the VM (Steam Machine support makes the power button sleep):
block with `sudo systemd-inhibit --what=sleep:handle-power-key` (as user it's
refused). If the VM sleeps anyway: `scripts/vmwake.sh` (the QEMU window's
keyboard doesn't wake it; a VM started before run.sh had `-qmp` needs a hard restart).
- `rtcwake` skips logind, so cecd never sees the sleep: suspend with
`systemctl suspend` and wake over QMP.
- `steamosctl get/set-hdmi-cec-suspend-tv|suspend-device` don't work
(steamos-manager 26.4.1); Steam uses the D-Bus properties of
`com.steampowered.SteamOSManager1.HdmiCec2`, and so does the test.
- The kernel answers "give OSD name" itself, even without cecd: it's no proof
cecd runs. cecd logs "Putting TV in standby" only at `RUST_LOG=debug`.
- CecDevice1 methods take arguments (`VolumeUp y 0`, Daemon1 `Standby b true`).
- After the test (or a hard restart) CachyOS may put autologin back on
gamescope: set the session to plasma and restart the display manager.
The menu item is 7 (8 = Steam Machine support, 9 = kernel pin, 10 = BIOS with `--fremont`).
## Steamify shortcut
Test a double-click with `systemd-run --user kioclient exec
@@ -301,6 +488,24 @@ them with `systemd-run --user <app>`.
## Pitfalls
- To see the plan without applying it, answer "n" at "Go ahead?"
(`'\nn\nq\n'`); a "y" applies it. Never wrap a run that may call pacman in
`timeout`: killing pacman after its transaction but before its hooks leaves
a stale boot entry and `db.lck` (restore the snapshot, or remove the lock
and reinstall the same packages so the hooks run).
- After a reboot or hard restart the autologin may be back on gamescope
(black screen): `sudo /usr/lib/steamos/steam-set-session plasma.desktop &&
sudo systemctl restart display-manager`.
- `vmreset.sh` sometimes stops at "Plasma did not start" (the greeter stays up
although the test autologin file is there): restart the display manager.
- `pkill -f '<pattern>'` from the host shell also matches the command running
it and kills it (exit 144): match something narrower, or kill by PID.
- Counting sudo password prompts (the VM has NOPASSWD): move
`/etc/sudoers.d/99-test-vm` aside, give the user a password, run the wizard
in a Python `pty` that answers `[sudo] password` and logs each prompt, then
put the file back. `makepkg -i` runs `sudo -k` and always asks again.
- Broken mirror: `mirror5.krfoss.org` served a bad `.sig` ("Maximum file size
exceeded"), failing the conversion's package install. `vmreset.sh` comments
it out; by hand: `sudo sed -i '/krfoss/s/^Server/#Server/' /etc/pacman.d/*mirrorlist*`.
+80
View File
@@ -0,0 +1,80 @@
---
name: progress-report
description: Use whenever a long VM job runs for the user (vminstall.sh, vmbootloadertest.sh, vmsuite.sh, vmtest.sh, an ISO build), locally or on the WSL PC, and the user wants to follow it - how to report progress (a table with a bar per VM and a total, only when something changes), show the log in the user's terminal and the VM's screen on their desktop, and look into a job that seems stuck.
---
# Progress report for VM jobs
What the user asked for (2026-09-29): see what is going on, without noise.
## The rules
- **Report only on a change**: an install stage, a test step, a job finishing, a FAIL. Never a
timed "no change" message. One monitor, not polling from the conversation.
- **Every report is the same table**, one row per VM/job plus a total row:
| VM | Step | Checks | Progress |
|---|---|---|---|
| grub | ✅ done | 40 / 40, 0 failed | `████████████████████` 100% |
| systemd-boot | test: kernel update | 27 / 40, 0 failed | `█████████████░░░░░░░` 68% |
| limine | install: desktop environment (40%) | 0 / 41 | `░░░░░░░░░░░░░░░░░░░░` 0% |
| **Total** | | **67 / 121**, 0 failed | `███████████░░░░░░░░░` 55% |
Bar: 20 cells, `█` per 5% of checks passed out of the expected count. Step in plain words
("install: base system (9%)", "test: reboot into the other kernel"), ✅ when done, ❌ with the
failed check's name when something fails. Under the table at most two lines: what is odd or next.
- **A FAIL or a job that stops moving is reported at once**, with the cause looked up first (below),
not only the numbers.
## The monitor
`scripts/vmprogress.sh [job...]` (jobs: limine systemd-boot grub cli menu hw installer toggles)
prints one line only when something changes and `ALLDONE` at the end; it reads the latest run in
`~/vms/bl-<loader>.test.log` / `~/vms/run-<suite>.log` and the install percentage from
`$VM_DIR/install-www/install.log`. Expected check counts live at its top: update them when a
suite gains checks. `--once` prints the current state.
On the WSL PC, from the laptop:
```
Monitor(command: "ssh wsl '~/projects/steamify-cachyos-dev/scripts/vmprogress.sh grub systemd-boot limine'",
description: "boot loader tests: only on change", timeout_ms: 1800000)
```
Re-arm it when it expires while jobs still run. Turn each event into the table above.
Stopping a background command or monitor on the laptop (TaskStop) does not stop what it started on
the PC: its remote loop keeps running (`pkill -f` it there). A leftover `until vm_ssh ...` loop
against a VM that gets reinstalled saves the live ISO's host key in `$VM_DIR/known_hosts`, and the
install then hangs at its first-boot ssh check ("REMOTE HOST IDENTIFICATION HAS CHANGED"). Never
ssh to a VM with its `VM_DIR` settings while it installs; fix it with `ssh-keygen -R "[localhost]:<port>" -f $VM_DIR/known_hosts`.
Monitor rules learned on 2026-09-29: **one monitor per job, stop the old one** (`TaskStop`) when a new run
starts, or every run is reported twice; a job that ends by itself (the ISO build on Gitea, `vmtest.sh`) is
best watched with a loop that exits on its terminal states and prints only *changes* (a new step, a finished
job, a FAIL, a new HTTP status), never on a percentage or a PASS count alone. For a Gitea Actions run, poll its
public log (`.../actions/runs/<run>/jobs/<job>/logs`, see `steamify-iso-release`) and grep for the step names.
A chain across systems (Steamify release -> ISO tag -> mirror sync -> Gitea build -> download link) is one
monitor that says each link when it happens and ends on the real check (the download URL answers 200).
When the tool that runs commands stops answering (auto-mode classifier "no verdict"), retry once, then say so
and carry on with reading/local work: nothing running on the PC is affected.
## What the user sees on the PC
- **Their terminal**: stream the running job's log into it (the `showlog` unit, `wsl-build-host`
skill); switch it when the job changes (install log, then the test log, then `~/vms/test.log`).
- **The VM's screen**: automated runs are headless. To show one: `scripts/vmview.py ~/vms/<vm>` in
a unit with the WSLg env (a window refreshed every 2 s, view only). When the user wants to watch
or step in, restart the job with a real window (`--window`, WSLg env) instead.
- One screenshot for yourself (`scripts/qmpshot.py <qmp.sock> out.png`, copied back and Read) only
to diagnose a stuck boot, not as a test.
## A job that seems stuck
1. `--once` and the log's last `#####` step: how long has it been there, compared with the others?
2. Is the VM up: `pgrep -af '[q]emu'` (its `hostfwd` port), ssh to it with the test's own options
(`. scripts/common.sh` with its `VM_DIR`/`VM_PORT`): a known_hosts mismatch or "Permission
denied" is a test setup problem, not the guest.
3. No ssh banner: take one screenshot. Firmware PXE/HTTP boot means no usable boot entry: read the
UEFI variables from a copy of `vars.fd` (`virt-fw-vars -i <copy> --print`, package
virt-firmware) and compare with a loader that works.
+300
View File
@@ -0,0 +1,300 @@
---
name: steam-machine-iso
description: Use when working on the Steam Machine CachyOS ISO (repo steamify-cachyos-live-iso) or Steamify's install-time mode (steamify.sh --defaults, --first-login) - building the ISO in podman (in the test VM, or on the Steam Machine), the Calamares Steamify step, simulating the installer in the test VM, and testing the first desktop login.
---
# Steam Machine ISO and Steamify's install-time mode
Releasing the ISO (GitHub tag, built and attached on the Gitea mirror, naming, runner and server settings) is
the `steamify-iso-release` skill; this one is for building and testing by hand.
A CachyOS live ISO for the Steam Machine that installs Steamify's default
setup during the install. Two repos:
- `steamify-cachyos` (Steamify): `steamify.sh --defaults` applies what the
menu would preselect, no prompts, passwordless sudo required. Without a
session (installer) `user_systemctl` only changes unit files, the theme
replaces an untouched `/etc/skel` Plasma layout so Plasma builds Vapor's at
first login, and `lib/first-login.sh` leaves a one-time autostart
(`--first-login`: single user's launcher on the new layout, then the app,
next to CachyOS Hello). See AGENTS.md there.
- `steamify-cachyos-live-iso` (fork of CachyOS-Live-ISO), branch work
never on master:
- `archiso/airootfs/usr/local/bin/calamares-online.sh` (the installer
launcher) copies CachyOS's `settings_online.conf` over
`/etc/calamares/settings.conf` on every start, so a shipped settings.conf
is lost: it `sed`s `shellprocess@steamify` (+ its instance) in before
`cleanup_calamares` right after that copy.
- `modules/shellprocess_steamify.conf` runs
`/usr/local/bin/steamify-install ${ROOT} ${USER}` outside the chroot; that
gives the user a temporary NOPASSWD rule, runs the bundle with
`arch-chroot ... runuser -u <user> -- env -i ... --defaults`, logs to
`/var/log/steamify-install.log` in the target, never fails the install.
- `steamify-prepare.sh [steamify checkout]` puts the bundle on the ISO
(`archiso/airootfs/usr/local/share/steamify/steamify.sh`, git-ignored):
from a checkout, else the newest release; refuses one without `--defaults`.
## Everything visible
The user watches the Steam Machine over Moonlight: run builds and tests in a
Konsole on its desktop (or open one that follows the log), never hidden.
Progress in the conversation: the `progress-report` skill (a table, only on a change).
Open that Konsole only when none follows the log yet: `tail -F` picks up
each new build's log by itself, so one window serves every build (check with
`pgrep -af "tail -n 50 -F"` on the Steam Machine; never `pkill -f` that
pattern from a remote command, it kills its own shell).
```bash
ssh steammachine bash -c "'pgrep -f \"tail -n 50 -F\" >/dev/null || { export XDG_RUNTIME_DIR=/run/user/1000 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus; systemd-run --user -q konsole --hold -e bash -c \"tail -n 50 -F ~/projects/iso-build.log\"; }'"
```
## Steam Machine gotchas
- Login shells there and in the VM are **fish**: wrap loops in
`bash -c '...'` or pipe a script to `bash -s`.
- Never `pkill -f <pattern>` from a remote command containing that pattern:
it kills its own shell (exit 255).
- The VM's host key lives in `~/projects/steamify-cachyos-dev/known_hosts`.
Scripts calling plain `ssh` (vmreset.sh) need a wrapper first in `PATH`:
`/tmp/steamify-sshwrap/ssh` =
`exec /usr/bin/ssh -o UserKnownHostsFile=$HOME/projects/steamify-cachyos-dev/known_hosts -o StrictHostKeyChecking=accept-new "$@"`
(recreate after a reboot).
- `vmreset.sh` shuts down *any* running QEMU (it checks `pgrep ^qemu-system`),
including the ISO VM with someone's live session: ask before resetting the
test VM while the ISO VM runs.
- Run vmreset.sh as a systemd user unit with `-p KillMode=process`, a unique
unit name, `--setenv=REPO=$HOME/projects/steamify-cachyos`, the wrapper
`PATH` and `SSH_AUTH_SOCK` (connect with `ssh -A`, keep the session open with
`--wait`). Without KillMode=process QEMU dies with the unit; without REPO
QEMU fails on the old repo path and the script waits 5 minutes for SSH.
## Adding a Calamares page/module for Steamify: check it's real first
Before writing any QML/config for a Calamares page, or building an ISO to
test one: confirm the module type is actually installed on this Calamares
build. A `.conf` file existing under
`archiso/airootfs/etc/calamares/modules/` does **not** mean the module
exists — CachyOS's branding ships some vestigial config files with no
matching `.so`. Check in a live session (boot the ISO,
`scripts/vmisoboot.sh`, no install needed):
```bash
ls /usr/lib/calamares/modules # real module types, e.g. packagechooser (not packagechooserq)
pacman -Ql cachyos-calamares-next | grep viewmodule
```
Found on this build (2026-09-28): `packagechooserq` (arbitrary custom QML,
`qmlFilename`) does **not exist** — only `packagechooser` does. Its
`mode: optionalmultiple` supports multi-select, but only with Shift/Ctrl-click (plain clicks
replace the selection; unusable for a real user) despite the misleading static label text
("Choose a product from the list. The selected product will be
installed." shows in both single- and multi-select modes — don't trust
it). Confirm real multi-select from the actual global-storage write in the
foregrounded debug log (`pkexec-wrapper calamares -D6`), not the label
text: `Config::updateGlobalStorage(const QStringList&)` logs
`"<instance>" selected "id1,id2,id3"` (comma-separated) when it works.
Test a page live, without any ISO rebuild: in the booted live session,
edit `/etc/calamares/modules/<module>_<instance>.conf` and the matching
module reference in `/etc/calamares/settings.conf`, then relaunch
(`pkexec-wrapper calamares -D6`). If Calamares is already running (e.g. an
earlier failed launch left its window open), a second launch just prints
"Calamares is already running." — close the first window
(Afbreken/Cancel, confirm) before relaunching. Only spend the 13+ minute
ISO build once the page's module and rendering are confirmed this way.
## Building the ISO in the test VM (preferred)
## Building the ISO in the test VM (preferred)
The build needs no real hardware: run it in the test VM (CachyOS; create it
with the vm-install skill), not on the Steam Machine. Not tried yet in the
VM: note what differs here. In the VM (`vm_ssh`, visibly in a Konsole there):
```bash
sudo pacman -S --needed --noconfirm podman git
git clone https://github.com/theupriser/steamify-cachyos-live-iso ~/projects/steamify-cachyos-live-iso
sudo mount -t 9p -o trans=virtio,version=9p2000.L repo /mnt # the Steamify checkout (REPO)
cd ~/projects/steamify-cachyos-live-iso && git checkout feat/steamify && ./steamify-prepare.sh /mnt
```
then the same `podman run` as below (paths in the VM; `~/projects/iso-build.log`).
The ISO (~3.2 GB) fits the 60G disk; copy it out with
`vm_scp "$VM_USER@$VM_HOST:~/projects/steamify-cachyos-live-iso/out/desktop/*.iso" .`
(`scripts/common.sh`), then install it with `scripts/vminstall.sh --iso`.
Give the VM more room with a bigger disk if `out/`, `build/` and the package
cache grow over several builds (`sudo rm -rf build out` between builds).
## Building the ISO (podman on the Steam Machine)
Only podman is installed on the Steam Machine itself; the build tools live in
the container. Rootful (loop devices, mounts), so `sudo podman ps` shows it,
not a rootless podman GUI. Clone: `~/projects/steamify-cachyos-live-iso`.
```bash
cd ~/projects/steamify-cachyos-live-iso
git checkout feat/steamify && ./steamify-prepare.sh ~/projects/steamify-cachyos
sudo rm -rf build out
systemd-run --user --collect -q -u isobuild-$(date +%s) --working-directory=$PWD bash -c \
"sudo podman run --rm -t --pids-limit=-1 --ulimit nofile=65536:65536 --privileged --network=host \
-v $HOME/projects/iso-cache:/var/cache/pacman/pkg \
-v $PWD:/iso -w /iso docker.io/cachyos/cachyos:latest bash -c \
'pacman-key --init && pacman-key --populate && pacman -Syu --noconfirm --needed archiso mkinitcpio-archiso git squashfs-tools grub sudo && ./build-live-modules.sh && ./build-calamares-modules.sh && { ./buildiso.sh -p desktop -w || ./buildiso.sh -p desktop -c -w; }' \
> $HOME/projects/iso-build.log 2>&1"
```
`-v ~/projects/iso-cache:/var/cache/pacman/pkg` keeps the downloaded packages
(~3 GB) between builds (`mkdir -p ~/projects/iso-cache` once): the container's
own tools and mkarchiso's `pacstrap -c` both use that cache, so only updated
packages are downloaded. Clear it with `sudo rm -rf ~/projects/iso-cache/*`
if it grows too large.
`build-calamares-modules.sh` builds Calamares' `packagechooserq` (the Steamify
page's module; CachyOS's package leaves it out) from CachyOS's Calamares
source against the installed `cachyos-calamares-next`, into airootfs, with
the version it was built for in `built-for` (the prebuilt `.so` is no longer
committed). `build-live-modules.sh` builds the power-off fix for the ISO's kernels
(live session; needs the source steamify-prepare.sh puts on the ISO).
Output: `out/desktop/steamify-cachyos-*.iso` (~3.2 GB, ~10 min). The build ends with
`chown: missing operand` + `ERROR: An unknown error`: harmless, buildiso.sh
chowns to `$SUDO_USER`, unset as root; the ISO is already written. Each flag fixes a failure seen before:
- `--network=host`: the default network has no DNS here (every mirror
"Resolving timed out").
- `--pids-limit=-1`: otherwise the last ~9 packages alphabetically fail with
`GPGME error: Inappropriate ioctl for device` / "missing required signature".
- `pacman-key --init/--populate`: the image's keyring isn't initialised.
- The retry with `-c` keeps the work dir and cache for a flaky pass.
Never start a build while one runs (`sudo podman ps`): both use `build/`
and `out/`, and `rm -rf build out` breaks the running one. Don't stop a running build to try something else without checking its log
first (`grep -ac GPGME ~/projects/iso-build.log`). The repo's CI recipe
(`archlinux:base-devel`, `.github/workflows/build.yml`) works too but pulls
from one slow mirror.
## Testing Steamify's install-time mode in the VM
`scripts/vminstallsim.sh [--fresh]` (VM up via vmreset.sh `--fremont`, branch
rsynced to `~/projects/steamify-cachyos`): creates `isotest` (never logged
in), runs `/mnt/steamify.sh --defaults` for it without any session, visibly.
Every component should print OK and `exit: 0`. Use `--fresh` after changes:
an earlier run's single user mode edits the skel layout, and the theme then
(rightly) refuses.
First desktop login as that user:
1. The first-login step (`~/.local/share/steamify/bin/first-login`, removes
itself) starts the app through `run-app`, which always downloads the
newest *release* (`releases/latest/download/steamify-app.sh`): before a
release that shows the old version. For the branch's app, start it by
hand afterwards as that user: copy `/mnt` to `/opt/steamify-test`
(`chmod -R a+rX`), then `sudo -u isotest XDG_RUNTIME_DIR=/run/user/1001
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1001/bus systemd-run --user
/opt/steamify-test/ui/steamify-ui`.
2. Gamescope doesn't run in the VM: `Session=plasma.desktop` in
`/etc/sddm.conf.d/zz-steamos-autologin.conf`, remove
`/etc/plasmalogin.conf.d/00-test-autologin.conf`, reboot.
3. Expect: Steam Deck wallpaper (Vapor layout), `primaryActions=3` in
isotest's `plasma-org.kde.plasma.desktop-appletsrc`, the autostart entry
gone, `steamify-ui` running next to `cachyos-hello`.
4. Screenshot: QMP `screendump` gives "no surface" with virgl; use
`sudo -u isotest env XDG_RUNTIME_DIR=/run/user/1001 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1001/bus WAYLAND_DISPLAY=wayland-0 spectacle -b -n -f -o /tmp/shot.png`.
## Testing the ISO itself
Unattended: `VM_DIR=~/projects/iso-vm scripts/vminstall.sh --iso <built iso> --fremont`
(vm-install skill) installs it with the ISO's headless `cachyos-installer`;
Calamares and its Steamify step don't run then, so run `steamify-install`
from the live script for that part. Through Calamares by hand, as below.
Install the built ISO in a **new** VM (`~/projects/iso-vm`: copy of run.sh,
`share` symlink, `cachyos.iso` symlink to the build, own disk/vars; power the
test VM off first, both use port 2222; `run.sh install --fremont` as a user
unit), through Calamares, then check
`/var/log/steamify-install.log` in the installed system. In the chroot
`uname -r` is the live kernel: module loads and checks against the running
kernel may fail even when DKMS built for the installed kernels. Real
hardware (Steam Machine, USB stick) is the final test: gamescope, LEDs, CEC,
power-off.
## Live-session quirks
- Copy/paste doesn't reach QEMU over Moonlight: type commands with
`python3 scripts/qmptype.py <vm dir>/qmp.sock "<command>"` (US layout, adds
Enter; the VM window's terminal must have focus). QMP `screendump` gives "no
surface" with virgl: screenshot the Steam Machine's desktop instead
(`spectacle -b -n -f -o /tmp/host.png` in its session).
- CachyOS Hello refuses to start the installer on a self-built ISO ("testing
ISO"): run `calamares-online.sh` in Konsole.
- `cachyos-calamares-next 3.4.2-13` needs `libboost_*.so.1.91.0` while the
repos ship Boost 1.92: steamify-prepare.sh puts 1.91's libraries on the ISO
(from archive.archlinux.org) until CachyOS rebuilds it.
- Driving the ISO VM's Calamares with `scripts/qmpkey.py`/`qmpclick.py`/
`qmptype.py` (`scripts/vmisoboot.sh` for a VM without virgl, so
`qmpshot.py` screendumps work): `qmptype.py`'s character map has no `>`
or `&` — a typed command using either raises "no key for" and, worse,
can leave an **unterminated quote** in the shell's input buffer (a
stray `"` from a half-typed command), after which every later command
you type gets silently absorbed into that one open string instead of
running. Symptom: the prompt shows a bare `>` continuation instead of
`[user@host ~]$`. Fix: send Ctrl+C first (`send-key` with `ctrl`+`c`),
then retype. Prefer `| tee` over `> file`, and multiple `head`/`tail`
calls over one long redirected command. Calamares page buttons
(Volgende/Terug) move vertically as page content grows/shrinks: re-shot
and re-locate the button after every page change, don't reuse a fixed
y-coordinate. A second `pkexec-wrapper calamares -D6` while an earlier
failed instance's window is still open just prints "Calamares is already
running.": close that window first (its Cancel/Afbreken button, then
confirm the "really cancel?" dialog).
- Run `pkexec-wrapper calamares -D6` in its **own Konsole tab**: it stays
in the foreground and blocks that tab's shell. Typing into the same
tab afterwards just echoes the text inertly into Calamares' stdin
(no error, nothing runs). Open "New Tab" in Konsole for commands.
## Keeping this cheap: don't poll screenshots
Repeated `qmpshot.py` + image analysis to detect "is it booted yet" burns a
lot of tokens per image. Prefer cheap, text-only signals instead:
- **Boot readiness**: pass `VM_SERIAL=<dir>/serial` to `run.sh`/`vmisoboot.sh`
(it logs `console=ttyS0` to a plain text file via a QMU chardev socket).
Wait with a background `tail -F <dir>/serial | grep -E "Reached target
Graphical Interface|sddm|plasmashell|login:|Failed|panic"` — one line of
text per event, no image tokens at all.
- **Driving the wizard/Calamares**: script a fixed sequence of
`qmpkey.py`/`qmptype.py`/`qmpclick.py` calls from a known page layout
instead of screenshotting after every step to "see where we are". Only
take a screenshot at real decision points (to show the user, or to verify
a page layout changed) — not as a polling mechanism.
- **When the user is driving by hand** (as in a live install), just ask them
what they see / what step they're on instead of screenshotting the VM
yourself — they're already looking at it.
- If you must poll for "is the screen non-black yet", a single low-res
screenshot at the end of a serial-log-driven wait is enough; don't loop
screenshots every few seconds.
- Calamares' Welcome page applies its keyboard layout to the live session
immediately (a live typing preview). If it's not US (e.g. Dutch after
picking Nederlands), `qmptype.py`'s US-layout key map silently sends
the wrong characters afterwards: `/` becomes `-`, `(`/`)` get garbled,
with no "no key for" error. `setxkbmap us` does nothing on Wayland.
Probe with a symbol-free command (`echo test123`) after any
language/keyboard step, and fix the layout from the live session's own
settings before typing paths or shell syntax again.
## Reading an installed VM's disk (no screenshots)
Also while the VM runs, read-only, without touching it:
```bash
sudo modprobe nbd max_part=8
sudo qemu-nbd -r -c /dev/nbd0 --image-opts driver=qcow2,force-share=on,file.driver=file,file.filename=$PWD/disk.qcow2
sudo mount -o ro,rescue=nologreplay,subvol=@log /dev/nbd0p2 /mnt/x # logs; subvol=@ for the rest
sudo cat /mnt/x/steamify-install.log
sudo umount /mnt/x; sudo qemu-nbd -d /dev/nbd0
```
(`qemu-nbd -U` doesn't exist here; `force-share=on` in `--image-opts` does
the same.) For a running live session, start the VM with `VM_SERIAL=<file>`
and end typed commands with `| sudo tee /dev/ttyS0`: the output lands in
that file as text.
@@ -0,0 +1,279 @@
---
name: steam-machine-testing
description: Use when testing or developing Steamify CachyOS (repo steamify-cachyos, steamify.sh) directly on the real Steam Machine (Valve Fremont running CachyOS) - either in a Claude Code session on the machine itself or over SSH - running the wizard with scripted menu input, checking each component's state, gaming mode, HDMI-CEC, LEDs, kernel pin, BIOS and reboot checks.
---
# Testing Steamify CachyOS on the real Steam Machine
This is the real-hardware twin of the `cachyos-vm-testing` skill. The big
difference: **there are no snapshots**. Every change is on the actual
machine, so undo what you turn on, never flash the BIOS without the user
saying so, and ask before anything that could leave it unbootable (kernel
pin, removing kernel headers, DKMS).
What the Steam Machine gives you that the VM can't: gaming mode that
actually renders (Steam's settings, HDMI-CEC section, refresh rates), real
LEDs, a real TV over HDMI-CEC, and the real BIOS version check.
## Keep everything in ~/projects
The machine is the user's own. Everything you put on it lives in
`~/projects` and nowhere else in the home folder: the repo copy
(`~/projects/steamify-cachyos`), the dev repo with the test VM
(`~/projects/steamify-cachyos-dev`), module builds, scratch checkouts
(`~/projects/<name>`). Temporary files go in `/tmp` and are removed again.
Never create folders like `~/steamify-test` or `~/s4mod`. What Steamify
itself installs (units, `~/.local/...`) is fine: that's the product. When
you're done, remove what you left in `~/projects` that isn't a repo.
## Test VM on the Steam Machine
The Mac can't run the VM (no KVM); the Steam Machine can (31 GB RAM, 12
threads, KVM). `qemu-desktop` and `edk2-ovmf` are installed; `run.sh` finds
Arch's OVMF in `/usr/share/edk2/x64`. Run it from
`~/projects/steamify-cachyos-dev` with
`REPO=~/projects/steamify-cachyos` (the synced branch) and follow the
`cachyos-vm-testing` skill, with `ssh steammachine` in front of its
commands where they run on the host.
## Two ways to work
**A. Claude Code on the Steam Machine itself** (preferred for development):
run commands directly, no SSH. Clone the repo there, e.g.
`~/projects/steamify-cachyos`, and run the wizard from it. Where this skill
says `sm bash -s << 'EOF'`, just run the body locally.
**B. From another machine over SSH.** Never guess the host: find the
candidates, then ask the user (next section). Once chosen, set per shell:
```bash
export SM_HOST=<chosen host> SM_USER=<chosen user>
sm() { ssh -o BatchMode=yes "$SM_USER@$SM_HOST" "$@"; }
```
### Finding the Steam Machine
Skip if the user already named a host, or `~/.ssh/config` has a
`Host steammachine` entry (use that). Otherwise scan the LAN for SSH hosts,
all read-only and quick:
```bash
# mDNS: hosts announcing SSH (macOS: dns-sd; Linux: avahi-browse)
if command -v dns-sd >/dev/null; then dns-sd -B _ssh._tcp local > /tmp/sm-mdns 2>&1 & p=$!; sleep 3; kill $p; awk 'NR>4{print $NF}' /tmp/sm-mdns | sort -u
elif command -v avahi-browse >/dev/null; then avahi-browse -rtp _ssh._tcp 2>/dev/null | awk -F';' '/^=/{print $7" "$8}' | sort -u; fi
# hosts with port 22 open on the local /24 (nmap if present, else nc; macOS has no `timeout`)
net=$( (ipconfig getifaddr en0 || ip -4 route get 1 | awk '{print $7;exit}') 2>/dev/null | cut -d. -f1-3)
if command -v nmap >/dev/null; then nmap -p22 --open -oG - "$net.0/24" | awk '/22\/open/{print $2" "$3}'
else for i in $(seq 1 254); do (nc -z -w1 -G1 "$net.$i" 22 2>/dev/null || nc -z -w1 "$net.$i" 22 2>/dev/null) && echo "$net.$i" & done; wait; fi
```
A CachyOS Steam Machine usually shows up by its hostname (e.g. `*.local`
with "steam", "fremont" or "cachyos" in it). Put the best matches first
and ask with AskUserQuestion: up to 3 candidates as options (label = host
name or IP, description = what was found), "Other" lets the user type one.
Ask the user name the same way (default: the local `$USER`).
Then check key login: `ssh -o BatchMode=yes -o ConnectTimeout=5 "$SM_USER@$SM_HOST" true`.
If it fails, tell the user to run `! ssh-copy-id $SM_USER@$SM_HOST` (it needs
their password; sshd must be on: `sudo systemctl enable --now sshd` on the
machine). Offer to add a `Host steammachine` entry to `~/.ssh/config` so the
next session skips the scan.
The dev repo's `scripts/*.sh` (vmstate, vmrun, cmp, vmshot, vmcec) all go
through `vm_ssh`, so they also work against the Steam Machine with
`VM_HOST=$SM_HOST VM_PORT=22 VM_USER=$SM_USER scripts/vmstate.sh`.
Don't use `vmreset.sh`, `vmwake.sh` or `run.sh`: those are QEMU-only.
`vmcec.sh` needs the `vivid` fake TV; on real hardware test CEC with the
real TV (below) instead.
The user's shell may be fish: pass remote commands as bash heredocs, never
as inline strings containing `$`.
Keep the repo in sync with the working copy on the machine (A: `git pull`;
B: `rsync -a --exclude .git ./ "$SM_USER@$SM_HOST:~/projects/steamify-cachyos/"`).
Below, `$REPO` is that checkout on the Steam Machine.
## Visible terminal (let the user watch)
When the user is watching the Steam Machine's screen (in person or through
Moonlight/Sunshine), run commands in a Konsole window on its desktop
instead of hidden over SSH: they see every command and its output. A tmux
session makes that window scriptable.
Set it up (Plasma desktop running; `sudo pacman -S --needed tmux` once):
```bash
sm bash -s << 'EOF'
export XDG_RUNTIME_DIR=/run/user/$(id -u) WAYLAND_DISPLAY=wayland-0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus
tmux kill-session -t claude 2>/dev/null
# Plain bash, not the user's login shell: fish rejects bash syntax ($?) and
# swallows Enter keys sent while it's still drawing its prompt.
tmux new-session -d -s claude -x 200 -y 50 'bash --norc --noprofile -i'; sleep 1
tmux send-keys -t claude "PS1='[claude] \\w\\$ '; clear" Enter
systemd-run --user -q --setenv=XDG_CURRENT_DESKTOP=KDE konsole --separate -e tmux attach -t claude
EOF
```
Run a command and wait for it: end it with a marker, then poll the pane.
```bash
sm bash -s << 'EOF'
tmux send-keys -t claude 'clear; sudo pacman -S --needed --noconfirm foo; echo "== EXIT $?"' Enter
for i in $(seq 200); do tmux capture-pane -p -t claude -S -400 | grep -q '^== EXIT' && break; sleep 3; done
tmux capture-pane -p -t claude -S -400 | grep -v '^\s*$' | tail -30
EOF
```
- `clear` first, or an old marker still in the pane ends the wait at
once; with several runs, count the markers (`grep -c`).
- Longer scripts: write them to `/tmp/<name>.sh` in the heredoc, then
`tmux send-keys -t claude 'clear; bash /tmp/<name>.sh' Enter`.
- For runs of 10+ minutes, poll from a `run_in_background` Bash call and
report when it finishes.
- A reboot ends the session (and the window): after every reboot, set it
up again before sending anything, or `send-keys` fails
(`error connecting to /tmp/tmux-1000/default`) while a wait loop spins.
- Watching remotely: Sunshine on the machine (`cachyos` repo, user unit
`app-dev.lizardbyte.app.Sunshine.service`, `sunshine --creds` for the web
UI, ufw: TCP 47984-47990 and 48010, UDP 47998-48010 from the LAN only) and
Moonlight on the user's computer. Pairing: the user reads Moonlight's PIN,
then `curl -k -u admin:<pw> https://<host>:47990/api/pin` (GET lists the
pending `pairing_id`) and POST `{"pin","name","pairing_id"}` to it. The
"Desktop" app streams any session; avoid "Low Res Desktop" (xrandr).
## Menu
1. SteamOS conversion (boot into gaming mode via autologin, Return to Gaming Mode shortcut, Steam desktop autostart)
2. └ Boot into: gamescope / desktop (sub-option of 1)
3. SteamOS theme (`cachyos-vapor` + Vapor global theme; needs a running Plasma session)
4. Steam Deck/Machine icons (`STEAM_GAMEPADUI_ARGS -steamos3`)
5. Single user mode (SDDM, no lock screen/user switching/log out, Valve's empty KDE wallet)
6. Steamify shortcut
7. HDMI-CEC (pre-ticked on a first run on Fremont)
8. Steam Machine support (leds-valve-dkms-git, `ensure-kernel-headers.service`, udev rule, steamos-manager, powerdevilrc)
9. └ Power-off fix (DKMS `steamify-fremont-poweroff`; ticked with 8, opt-out)
10. └ Update BIOS (opt-in, never pre-ticked; tickable only when Valve has a newer BIOS)
Since 2.2.0 "Pin the kernel" and "HDMI refresh boost" are only shown while
something of them is left, unticked, so a normal run removes them. The menu
marks components a normal run will update with "(update)" (feature versions
in `~/.local/state/cachyos-gamescope-boot/features.state`; set an entry to an
older version, e.g. `machine=2.1.0`, to test the update flow).
Numbers shift when a parent is unticked (its sub-option is hidden), so read
the ticks with `'q\n'` first. Undo journals: `~/.local/state/cachyos-gamescope-boot/`.
## Running the wizard
Visible, in the desktop session (best when the user is watching the screen):
```bash
sm bash -s << 'EOF'
export XDG_RUNTIME_DIR=/run/user/$(id -u) WAYLAND_DISPLAY=wayland-0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus
printf 'q\n' | "$HOME/projects/steamify-cachyos/steamify.sh" 2>&1 | tee /tmp/wizard.log
EOF
```
Newest release instead of the checkout:
`curl -fsSL https://github.com/<owner>/steamify-cachyos/releases/latest/download/steamify.sh -o /tmp/steamify.sh`
then `printf '<input>' | WIZARD_KEEP_STDIN=1 bash /tmp/steamify.sh`.
Menu input: a number toggles, an empty line continues, `a` re-applies what is
on, `q` quits; `y` answers "Go ahead?". After a run: Enter (menu) or
`[m]`/`[r]` when a restart is needed. `q` with a restart pending asks
"Restart now? [Y/n]" (default **yes** - on real hardware that really
reboots). End scripted input right after the last `q`, or with `n`.
| Input | Meaning |
|---|---|
| `'q\n'` | just show the menu (always do this first) |
| `'\ny\nm\nq\nn\n'` | first run, accept the pre-ticked items, don't reboot |
| `'3\n\ny\n'` | toggle the theme (while 1 is ticked) |
| `'7\n\ny\nm\nq\nn\n'` | toggle HDMI-CEC (while 1 is ticked) |
| `'a\ny\nm\nq\nn\n'` | re-apply what is on |
When everything is off the menu treats it as a first run and pre-ticks.
`a` doesn't retry a component that failed.
Long runs (packages, yay, DKMS) take 10+ minutes: use `run_in_background`
and wait with an until-loop.
## Checking state
`scripts/vmstate.sh` (mode B with the env above), or in mode A run the body
of its heredoc locally: display manager and autologin, session sync bridge,
shortcut, Steam autostart, theme keys, lock restrictions, panel, kickoff,
LED driver/DKMS/`/sys/class/leds/valve-leds*` owner, steamos-manager,
journals and duplicate journal keys. `scripts/cmp.sh save` / `cmp.sh` diff
the KDE configs.
## Real-hardware checks (what the VM can't do)
- **Gaming mode**: reboot with Boot into gamescope; Steam starts in Big
Picture/gamepad UI, Steam Deck icons with item 4, "Switch to Desktop"
lands in Plasma and "Return to Gaming Mode" goes back.
- **HDMI-CEC with the real TV**: `cec-ctl -d /dev/cec0 --playback -S`
(topology), TV remote keys reach Steam, Steam's settings > Display >
HDMI-CEC toggles (remote, WakeTv, SuspendTv, SuspendDevice) change
`com.steampowered.SteamOSManager1.HdmiCec2` properties, sleep turns the TV
off, wake turns it on, TV standby suspends the machine when SuspendDevice is on.
`steamosctl get/set-hdmi-cec-*` don't work (steamos-manager 26.4.1).
- **LEDs**: `leds_valve` loaded, `/sys/class/leds/valve-leds*` owned by the
user, writing `brightness` visibly changes them.
- **Power-off fix**: `dmesg | grep 'GPIO 18'` shows the pin's register and
whether the S4/S5 wake bit is set (recent kernels: set, "cleared at
power-off"); `/sys/kernel/debug/gpio` has an S4/S5 column. The real test
needs the user at the machine: shut down and check it stays off (30 s+),
2-3 times; control run: `sudo rmmod steamify_fremont_poweroff`, shut down,
it should boot again right away. Do this for every new major kernel.
Install a test kernel next to the current one under another package name
(e.g. `linux-cachyos-bore`) so the user can pick the old one in Limine.
- **BIOS**: item 10 is tickable only if Valve has a newer BIOS than
`cat /sys/class/dmi/id/bios_version`. Walk the flow with
`WIZARD_BIOS_DRY_RUN=1` (never flashes). **Never run a real flash unless
the user explicitly asks in this session**, and only on AC power.
## Reboot checks
The session reboots; in mode A the Claude session ends. Tell the user
before rebooting and what to check afterwards.
- Single user mode on: SDDM autologin without greeter (`pgrep sddm-greeter` empty).
- Everything off: plasmalogin greeter shown.
- Stuck in a gamescope relogin loop: over SSH
`sudo /usr/lib/steamos/steam-set-session plasma.desktop && sudo systemctl restart display-manager`.
## Restoring the machine
No snapshots, so before a test run note the starting state (`vmstate.sh`,
`cmp.sh save` into `~/.cache/vm-baseline`) and afterwards turn back off
what the test turned on through the menu (the undo journals do the rest).
Check `cmp.sh` is clean except KDE's own default-valued keys.
## Pitfalls
- To see the plan, answer "n" at "Go ahead?" (`'\nn\nq\n'`). Piping a "y"
applies it, on real hardware; never wrap a run that may call pacman in
`timeout`: killing pacman after its transaction but before its hooks
leaves a stale boot entry and `db.lck` (fix: remove the lock, reinstall the
same packages so the hooks run).
- The user's login shell is fish: inline `ssh host '...'` commands with `$`
or `\$` break; always send bash heredocs (`sm bash -s << 'EOF'`).
- pacman 404 on a `.sig`: the local sync database is stale (the mirror moved
on to a newer version). A full `pacman -Syu` fixes it; don't fetch
signatures by hand.
- CachyOS's `linux-cachyos` is clang-built, `-bore` GCC-built: when building
a module by hand, pass `LLVM=1` only for clang kernels (DKMS picks itself).
- `pkill -f '<pattern>'` also matches the shell running it; kill by PID.
- Apps started straight from SSH lack the session's Qt theme or core-dump
(spectacle): start them with `systemd-run --user`.
- Screenshots: `systemd-run --user --wait -q spectacle -b -n -f -o /tmp/x.png`
with the session env above; in gaming mode spectacle can't see gamescope,
use Steam's own screenshot (Steam + R1) instead.
- `makepkg -i` runs `sudo -k` and always asks for the password again; the
real machine has no NOPASSWD rule, so scripted runs that need sudo stop
at the prompt. Run those in a terminal the user can type into, or have the
user add a temporary sudoers rule and remove it afterwards.
- Check the bundle like CI: `.github/tools/bundle.sh`, then
`shellcheck -S warning -e SC2034,SC2154 dist/steamify.sh`.
@@ -0,0 +1,28 @@
---
name: steamify-branch-cleanup
description: Clean up git branches in steamify-cachyos (and sibling steamify repos) following the release-branch flow. Use when the user asks to clean up / prune branches.
---
# Steamify branch cleanup
Rules:
- ALWAYS keep `main` and every `release/X.Y.Z` branch (local and remote). Never delete them.
- Delete `feature/*` and `bugfix/*` branches (local and remote) only when merged into `origin/main` or the current `origin/release/*` branch.
- Delete local branches whose remote is gone (`[origin/...: gone]`) only if they are `feature/*` or `bugfix/*`; local stale `release/*` branches are kept unless the user says otherwise.
- Never touch unmerged branches; list them and report instead.
- Never commit or merge to main.
Merging a feature/bugfix PR yourself (into a release branch, or `feat/steamify` in the ISO repo): delete the
branch at once, remote and local (`gh pr merge N --merge --delete-branch`, then `git branch -D`, `git fetch
--prune`); the user asked for this on 2026-09-29. Release branches are never deleted (kept as history next to
their tags). Unmerged old branches (`feat/vram-booster`, `refactor/qml-screens`, `backup/*`) are reported, not
touched. The GitHub-side `release delete <tag> --cleanup-tag` is how test ISO releases go.
Steps:
1. `git fetch --all --tags --prune`
2. `git checkout main && git merge --ff-only origin/main`
3. List candidates: `git branch -vv`, `git branch -r --merged origin/main`, and same for the current release branch.
4. Delete local (`git branch -d`) and remote (`git push origin --delete <b>`) feature/bugfix branches per the rules above.
5. Report what was deleted and what was kept.
Note: remote deletions may be blocked by the auto-mode classifier; if so, stop and let the user approve a permission rule or run it via `!`.
@@ -0,0 +1,101 @@
---
name: steamify-iso-release
description: Use when releasing, debugging or changing how the Steamify ISO is built and published - the GitHub tag that names it, the Gitea mirror (git.upriser.nl) that builds and attaches the 3.2 GB ISO, the Steamify workflows that start it, and every trap met while setting it up (413, privileged runner, sudo dropping variables, mirror syncs deleting tags, upload-artifact@v3, GitHub 429). Also for the Gitea side of steamify-cachyos' own releases.
---
# Steamify ISO release pipeline (set up 2026-09-29, first ISO out the same night)
Tested end to end: release `v2.9.3-dev.2026.09.29-2044` -> `steamify-cachyos-2.9.3-dev.2026.09.29-2044-x86_64.iso`
on git.upriser.nl, GitHub's link to it answers 200.
## The flow (one tag names everything)
1. **GitHub, `iso-1-github-tag.yml`** (ISO repo `theupriser/steamify-cachyos-live-iso`, `workflow_dispatch` only):
takes Steamify's newest release (`X.Y.Z`), the time **now in UTC** and makes an **annotated tag**
`vX.Y.Z-[dev.]YYYY.MM.DD-HHMM` plus a GitHub release with the changelog notes and the *direct* download link
on Gitea. `dev.` and a pre-release on `feat/steamify` (test build), none on `master` (a release).
GitHub is the only place that reads a clock.
2. **The mirror syncs** (git.upriser.nl is a pull mirror of GitHub, every 10 minutes): the tag arrives.
3. **Gitea, `iso-2-gitea-build.yml`** (`on: push: tags: ['v*']`, skipped on GitHub by `github.server_url`): parses the
tag, builds the ISO on the runner with exactly that Steamify (`STEAMIFY_VERSION`), the tag's time (label) and
the tag without its `v` (`STEAMIFY_ISO_VERSION`: file name, boot menu, `/etc/steammachine-iso-build`), then
attaches ISO + `.sha256` + `.sha1` + `.pkgs.txt` to the mirror's release for that tag
(`akkuman/gitea-release-action`, the run's own token). Never overwrites a release that already has its ISO.
4. **Trigger:** by hand (*Actions -> ISO 1/2 · Tag and release (GitHub) -> Run workflow*, or
`gh workflow run iso-1-github-tag.yml -R theupriser/steamify-cachyos-live-iso --ref feat/steamify`), or by a
new Steamify release: `steamify-cachyos`' `bundle.yml` step "Start the Steamify ISO's release" (secret
`ISO_DISPATCH_TOKEN` there: fine-grained token, only the ISO repo, *Actions: read and write*; without it the
step is skipped). A push does **not** release (a build is 20 minutes and 3.2 GB).
5. Names, for tag `v2.9.3-dev.2026.09.29-2044`: file `steamify-cachyos-2.9.3-dev.2026.09.29-2044-x86_64.iso`,
label `STEAMIFY_2_9_3_20260929_2044` (ISO 9660: 32 chars, `A-Z 0-9 _`). By hand, without a tag: `local`
(`steamify-cachyos-local-x86_64.iso`, `STEAMIFY_<version>_LOCAL`). The build reads no clock.
Total from a Steamify release to the ISO on Gitea: about 30 minutes (release, tag, sync <=10 min, build 15-20).
## Why it is split like this (learned the hard way)
- **A release made only on the mirror disappears** at the next sync: the pull mirror removes tags GitHub doesn't
have, and the release goes with it (one that has files may linger, tagless, and sorts oddly: delete it). A
release on a tag that **comes from GitHub survives** every sync, files included (tested with a 0-byte asset).
Hence: GitHub makes the tag, Gitea only attaches the file. You can't delete a tag on a mirror by hand either.
- GitHub releases take **2 GB per file**; the ISO is 3.2 GB. So GitHub gets notes and a link, Gitea the file.
- **Annotated tags**: a lightweight tag has only its commit's date, so tags on the same commit sort randomly
on Gitea. UTC is fine (the release name says "UTC"; it is 2 h behind Dutch summer time, and nobody cared).
- The GitHub release's link is built from the tag alone (`.../releases/download/<tag>/steamify-cachyos-<tag
without v>-x86_64.iso`), so the Gitea file name must be that: check it with `curl -I -L` (the monitor in the
session did: HTTP 200 = the whole chain is right).
## Gitea side: what the runner and the server need
- **Runner** (act_runner in Docker Compose, network `gitea_gitea`): `config.yaml` -> `container: privileged: true`
(the workflow's `options: --privileged` is **ignored** otherwise; symptom: `mount: .../airootfs/proc: permission
denied`, `failed to setup chroot`; the job log shows `Privileged:false`). ~20 GB free disk.
- **Upload limit: `[repository.release] FILE_MAX_SIZE = 10240`** in `app.ini` (MB; default 2048), restart Gitea.
`[attachment] MAX_SIZE` is for issues, **not** releases: symptom `status: 413` on `.../releases/<id>/assets`.
The release is created *before* the upload, so a failed run leaves an empty release: delete it.
- Logs of a public repo's run are readable without login: `.../actions/runs/<run>/jobs/<job>/logs` (job numbers
count up; re-runs get a new job number). Good for a monitor; no `gh` for Gitea.
- Mirror sync is set to 10 min by the user with their own token; "Synchronize Now" or the API
(`POST /api/v1/repos/<repo>/mirror-sync`) for now. `GIT_UPRISER_TOKEN` (secret in the ISO repo) would make GitHub
trigger it; not needed.
- **Build traps in the container:** `sudo mkarchiso` in `util-iso.sh` **drops environment variables**:
it needs `sudo --preserve-env=STEAMIFY_ISO_VERSION,STEAMIFY_BUILD_STAMP` (the first Gitea ISO came out as
`-local-`). Step names are evaluated **before** earlier steps set env: don't put `${{ env.X }}` in a name.
Lots of `error: command failed to execute correctly` (pacman hooks wanting systemd) are harmless.
The bash test of the naming must go through `sudo` too, not only call `profiledef.sh` directly.
- Kernel: the ISO is built from the generic `x86_64` repo (must boot on any PC), which lags the `v3/v4/znver4`
repos by a day or so: 7.2.7 on the ISO while installed systems get 7.2.8. Normal, not a bug: the installer
switches to the CPU's optimised repo (znver4 on the 9800X3D) and the first update brings the newer kernel.
## Steamify's own workflow (`steamify-cachyos/.github/workflows/bundle.yml`), one file for both servers
- Build + check run everywhere. shellcheck through `ludeeus/action-shellcheck@2.0.0` (brings its own binary:
act_runner's image has none; GitHub's has). The artifact: `actions/upload-artifact@v4` on GitHub,
`christopherhx/gitea-upload-artifact@v4` elsewhere. **Never write `upload-artifact@v3`**: GitHub fails the
whole workflow for merely mentioning a deprecated version, even in a skipped step.
- Publishing: GitHub release (`gh`, tag + `latest`) only on GitHub; on Gitea the release is `akkuman/gitea-release-action`
with a Gitea-API version check (`RELEASE_TOKEN` secret optional, else the run's token). Steps are guarded by
`github.server_url == 'https://github.com'` (or `!=`). YAML check without PyYAML on the Mac:
`ruby -ryaml -e 'YAML.load_file(...)'`.
- Steamify releases still follow the AGENTS.md flow (`release/X.Y.Z`, `bugfix/`/`feature/` branch, PR into the
release branch, merged and **deleted** by the agent, the release PR into `main` is the user's). Even a CI-only
change is a release (2.9.2, 2.9.3).
## GitHub rate limit that started this (HTTP 429)
`raw.githubusercontent.com` refused Valve's CEC driver source after a day of test installs (each CEC turn-on
downloaded it again; the limit is per address, ~1 per 5 minutes seen). Fixed in Steamify 2.9.1: the driver is
cached in `/var/cache/steamify/cros-ec-cec-<sha256:12>.c` (named after its checksum: a new pin downloads once and
deletes the old file; a damaged file is fetched again). The test VMs share the host's `~/vms/pkg-cache/steamify`
(bound over `/var/cache/steamify` by `vmsuite.sh`, `vmbootloadertest.sh`, `vminstall-live.sh`); hw block 50 tests
the cache offline (`file://`). The source repo `evlaV/linux-integration` is an unofficial mirror without releases:
if it ever vanishes, ship the file with Steamify.
## Checklist for a release
1. Steamify released? (`gh release list -R theupriser/steamify-cachyos`). The ISO gets the newest.
2. `gh workflow run iso-1-github-tag.yml ... --ref feat/steamify` (test) or `--ref master` (release; master must have
the workflows: it doesn't yet, `feat/steamify` is the ISO repo's working branch).
3. Watch: tag on Gitea (`.../api/v1/repos/theupriser/steamify-cachyos-live-iso/tags`), the run's log (above),
then `curl -I -L` the GitHub link. Old test releases: delete on GitHub (`gh release delete <tag>
--cleanup-tag -y`); the sync removes the tag on Gitea; delete a leftover tagless Gitea release by hand.
+198
View File
@@ -0,0 +1,198 @@
---
name: vm-install
description: Use when there is no Steamify test VM yet, the VM should be reinstalled from scratch, or an ISO (CachyOS, later the Steamify CachyOS ISO) should be installed unattended into a VM - scripts/vminstall.sh installs it without any manual step and leaves the snapshots clean and ssh-ready.
---
# Installing the test VM unattended
`scripts/vminstall.sh` turns an ISO into a ready test VM, no clicking:
```bash
VM_DIR=~/vms/steamify-vm scripts/vminstall.sh --fremont # run.sh flags pass through
VM_DIR=~/vms/iso-vm scripts/vminstall.sh --iso ~/Downloads/steamify-cachyos-<date>-x86_64.iso
```
Result in `$VM_DIR`: `disk.qcow2` with the snapshots `clean` (just
installed) and `ssh-ready` (booted once, SSH/sudo/Plasma checked), their
`vars.*.fd`, `run.sh` (this repo's), `vm-user`, `ssh-key`, `repo-path`,
`known_hosts`, `install.log`. Every other script (`vmreset.sh`, `vmwatch.sh`,
...) reads the user, key and repo from there: pass the same `VM_DIR`.
## What gets installed
CachyOS with the ISO's own headless installer (`cachyos-installer`,
`"headless_mode": true`): KDE Plasma, plasma-login-manager, btrfs with the
default subvolumes, Limine, `linux-cachyos` + `linux-cachyos-lts`, fish.
Then `share/vminstall-post.sh` in the new system: hostname `steamify-vm`,
sshd with the chosen key, passwordless sudo, the test autologin into Plasma
(`/etc/plasmalogin.conf.d/00-test-autologin.conf`), `en_US.UTF-8`, the host's
timezone, tmux + shellcheck, Limine `default_entry: 2` (the installer's
config points at the folder, so the first boot would wait in the menu).
## Login (by hand)
- User: your host username (`id -un`), or `VM_USER=...`.
- Password: **`steamify`** (user and root), or `VM_PASSWORD=...`.
- SSH: `~/.ssh/steamify-vm_ed25519`. Made once, on the first install (the
script asks: that new key, or one of your own `~/.ssh/*.pub`), then always
used. An existing key is never overwritten. The VM's host key is kept in
`$VM_DIR/known_hosts`, never in `~/.ssh/known_hosts`.
## How it works
1. The ISO's kernel and initramfs are copied out (`udisksctl loop-setup`, no
root) and booted directly (`VM_KERNEL`/`VM_INITRD`/`VM_APPEND` in
`run.sh`) with `systemd.run=` (+ `systemd.wants=kernel-command-line.service`,
or the unit is only generated) running `curl http://10.0.2.2:<port>/vminstall-live.sh | bash` as root.
2. `scripts/vminstall-server.py` serves the live script, `settings.json`,
`vminstall.env` and the post script on 127.0.0.1; the guest reaches the
host at 10.0.2.2 (QEMU user networking, any host network;
`VM_HOST_IP` for a bridged setup). The live system PUTs its log
(`install-www/install.log`, every 20 s) and the status back.
3. The live desktop shows a Konsole following the install log; the serial
console is logged to `$VM_DIR/serial.log`, and root logs in there
without a password over `$VM_DIR/serial.log.sock` (for debugging).
4. After `== ok` the VM powers off, `clean` is taken, it boots once for the
checks, powers off, `ssh-ready` is taken.
Takes 15-30 minutes (mirrors, packages). Run it with `run_in_background`
and wait with an until-loop on the log, e.g.
`until grep -qaE '^== (ok|failed)' $VM_DIR/install-www/install.log; do sleep 20; done`.
## Gotchas
- Refuses an existing `disk.qcow2` without `--force` (asks YES), and a
running VM (port 2222).
- Never edit a script in place while it runs (bash reads it as it goes; Python `open(p,"w")` and editors that rewrite do this). `sed -i` or writing a copy and `mv` swaps the file: the running one keeps its old copy.
- Don't match your own wait loop with `pgrep -f`/`pkill -f` on a pattern that
is part of that loop's command line.
- Mirror 404s during the install are normal for an older ISO (pacman moves
on); `fatal library error, lookup self` in the chroot is harmless.
- QMP screenshots don't work (virgl, "no surface"): watch the window, the
serial log, or the install log.
- `pkill -f`/`pgrep -f` with a pattern that also appears in your own command
line kills or matches your own shell (exit 144). Use `[q]emu` bracket
patterns, or kill by PID from `ps -eo pid,args`, in a separate command.
- A wait loop that runs `pgrep -f "x"` over ssh matches its own ssh command:
write `"[x]"`. And `systemctl --wait start` on a unit that already
finished waits for ever; poll `systemctl is-active` instead.
- The live script must not wait for `systemctl is-system-running --wait`: it
is itself the running `kernel-command-line.service` job (deadlock). It waits
for `pacman-init.service` to be active instead (else "no secret key
available to sign with").
- Only one VM at a time (port 2222); a second `vminstall.sh` refuses while
qemu runs. Run several installs one after the other from a script, and
give `--force` runs a terminal or delete `disk.qcow2` first (the YES
prompt reads /dev/tty).
- The guest's login shell is fish: send bash through stdin
(`ssh ... 'bash -s' < script.sh`), not loops on the command line.
- `/boot` is root-only: use `sudo` for `ls`/`cat` there, or checks report
"no entries" for nothing.
- `ssh` "timed out during banner exchange" only means QEMU accepted the
forwarded port: the guest's sshd isn't answering (still booting, waiting in
a boot menu, or a firewall). It says nothing more.
- Never run `systemctl reboot --firmware-setup --dry-run` to "check": it
still sets the EFI flag and the next boot lands in the UEFI setup menu.
`bootctl status` shows `Boot into FW: supported` normally, `active` = set.
## The Steamify CachyOS ISO
`--iso <steamify iso>` (archiso with `cachyos-installer`). The headless
installer doesn't run Calamares, so `vminstall-live.sh` runs the ISO's
`steamify-install /mnt $VM_USER "$VM_STEAMIFY"` itself after the install
(empty `VM_STEAMIFY` = the Steamify page's default, everything on; ids to
narrow; `skip` = plain CachyOS) and fails the install if its log doesn't end
`exit: 0`. The log ends up in `/var/log/steamify-install.log`. The post script
keeps Steamify's gaming-mode login (SDDM,
`/etc/sddm.conf.d/10-gamescope-autologin.conf`) instead of forcing the Plasma
autologin, and the first-boot check then wants the display manager, not
plasmashell (gamescope doesn't render in the VM: a black window is normal).
## Other options
- `VM_BOOTLOADER=limine|systemd-boot|grub` (default limine) goes into the
installer's settings. Use one `VM_DIR` per loader (`~/vms/bl-grub`, ...).
cachyos-installer leaves systemd-boot with `#timeout 3` and no default (it
waits in the menu for ever): the post script writes `timeout 3` and
`default linux-cachyos.conf`, like it sets Limine's `default_entry`.
- The installer enables **ufw**, which drops the host's ssh
(`[UFW BLOCK] DPT=22` in the kernel log): the post script runs
`ufw allow 22/tcp`.
- `VM_CACHE=<dir>` (default `~/vms/pkg-cache`, empty = off) is a host package
cache shared as 9p tag `cache`, bound over pacman's cache in the live system
and in the new one (Steamify's packages): the first install fills it.
## The whole automated test: `scripts/vmtest.sh`
`scripts/vmtest.sh [--install] [--window] [--screen] [boot | <loader>... | <suite>...]`: no argument runs
everything: the boot loader test (below) and every suite in `share/vmtest/` (`cli`, `menu`, `hw`, `installer`,
`toggles`: the TESTPLAN.md rows that need no person, run by `scripts/vmsuite.sh` on the plain VM
`~/vms/steamify-vm`, each block from a fresh `ssh-ready`). `--screen` runs it in a detached `screen`
session (`screen -r vmtest`); output goes to `~/vms/test.log` (one shared Konsole follows it when headless
on a desktop), the summary to `~/vms/last-test.txt`, exit status = failed checks. Adding a check: a block file
in `share/vmtest/<suite>/NN-name.sh` (guest side, prelude helpers `pass/fail/skip/info/sf/is/expect_on`) or
`NN-name.host.sh` (host side, for reboots and host scripts); `# env: VAR=x` in a block sets it for the VM
start; `ONLY=<prefix> scripts/vmsuite.sh <suite>` runs one block. Rows that need a person (U1-U9, R2.2,
H-Real) are printed as SKIP at the end. Pitfalls met while building it: atomic file swaps (`os.replace`)
lose the executable bit (`chmod --reference` or call scripts with `bash`); a suite block that reboots must
be a `.host.sh` block; ssh has no Plasma session, the prelude exports one; a one-off network failure can fail
an install step (LED driver from the AUR), rerun the block before suspecting Steamify.
Gotchas found on 2026-09-29: never ssh to a VM with its `VM_DIR` settings while it installs (a leftover loop, also
one started from a laptop command that was stopped, saves the *live ISO's* host key in `$VM_DIR/known_hosts` and
the install then hangs at its first-boot ssh check: `ssh-keygen -R "[localhost]:<port>" -f $VM_DIR/known_hosts`);
`vminstall.sh` now never uses root as the guest user (WSL runs as root: `theupriser`), takes the ISO's loop
device under a lock (parallel installs collided), tolerates the poweroff dropping ssh, waits only for its own
qemu and stops at once when the live system reports `== failed: ...`. A failed install used to look like
"still installing" for 90 minutes: `vmprogress.sh` shows `INSTALL FAILED: <why>`.
Following a run for the user (a table per VM with a bar and a total, only on a change; their terminal and
the VM's screen): the `progress-report` skill, with `scripts/vmprogress.sh` as the monitor.
## Testing a boot loader: `scripts/vmtest.sh boot`
Run this instead of doing the steps below by hand: `scripts/vmtest.sh [--install] [--window] [loader...]`
(default: every loader the ISO advertises, read from `calamares-online.sh`; it fails when an advertised
loader has no test). One VM per loader in `~/vms/bl-<loader>` (`--install` builds them from the newest
Steamify ISO, ~8 min each, ~3 min per test), headless with a Konsole on the logs (`--window` shows the VMs),
one summary line per loader, `~/vms/bl-<loader>.test.log`, exit status = failed checks. Checks live in
`share/bootloader-test/*.sh` (one PASS/FAIL line each); the driver is `scripts/vmbootloadertest.sh`.
Last result (2026-09-29, ISO with Steamify 2.9.1): Limine 43, systemd-boot 42, GRUB 42 checks, 0 failed.
The boot entry check (B1, `boot-check.sh`, after every reboot): the system booted through the loader's **own**
EFI entry (`BootCurrent`, not `auto_created_boot_option`, a real partition GUID not `HD(0,GPT,0000...)`). It
found that the installer never registered systemd-boot (bootctl skips the EFI variables in a chroot): OVMF then
tried PXE/HTTP boot/EFI shell first, 4-5 minutes per boot, or "no bootable device". The ISO's `steamify-install`
now runs `efibootmgr` from the live system (a real partition, first in the order) and mounts the new system's
`/var/log` (`@log` subvolume) first so its logs (`steamify-install.log`, `steamify-bootentry.log`) survive.
To read a VM's boot entries without booting it: copy `vars.fd`, `virt-fw-vars -i <copy> --print` (package
`virt-firmware`). The power-off check accepts "loaded" or "tried at boot: No such device" (journal, not dmesg:
the VM has no AMD GPIO controller, the module refuses on purpose).
What only shows up per loader: Limine keeps its images under `/boot/<machine-id>/<kernel>/` and copies one
only when its content changed (same version = untouched), and `remember_last_entry: yes` overrides
`default_entry` (the test turns it off and restores it); GRUB's other kernel is picked with
`GRUB_DEFAULT="1>N"` + `grub-mkconfig` (key presses miss its 5 s menu); systemd-boot with
`bootctl set-oneshot <entry>.conf`. The "kernel update" reinstalls what the installed system's database lists,
which can be older than what the installer got (mirror skew: 7.2.8 installed, 7.2.7 in the database): a
downgrade, but still a version change through the same hooks.
Manual version of the same, for one loader:
Steamify's kernel-side parts (`steamify-fremont-poweroff`, `steamify-cros-ec-cec`,
`leds-valve-dkms`) are DKMS modules for every installed kernel, only installed
when the VM reports Fremont: start the VM with `./run.sh --fremont`, then run
`steamify.sh --defaults --options gaming,theme,glyphs,single,launcher,notify,vram,cec,machine,poweroff`
from the shared repo (`sudo mount -t 9p -o trans=virtio,version=9p2000.L repo /mnt`).
Then per loader: `dkms status` (3 modules x each kernel), reinstall both kernels
+ headers (`pacman -S linux-cachyos linux-cachyos-lts` + headers: rebuilds
DKMS, initramfs, and for GRUB the config), reboot into the default kernel and
into the other one (systemd-boot: `bootctl set-oneshot <entry>.conf`; GRUB or
Limine: send keys with `scripts/qmpkey.py <vm>/qmp.sock down ret ...`, or
change `default_entry`), and check `uname -r`, `lsmod`, and `dmesg | grep steamify`.
The legacy `drm.edid_firmware` removal (`hdmi_remove_boot_param`) is the code
that differs per loader: seed the parameter in `/etc/default/grub`,
`/etc/sdboot-manage.conf` or `/etc/default/limine`, source `lib/common.sh`,
`state.sh`, `hdmi-refresh.sh` from `/mnt`, and run it. To see why a VM won't
boot without a console, stop it, `qemu-img dd` the first 2 GB to a raw file,
`dd skip=1M` to cut the ESP, `udisksctl loop-setup` + `mount` it (no root),
and read `loader/loader.conf` and the entries; or boot the kernel directly with
`VM_KERNEL/VM_INITRD/VM_APPEND=... console=ttyS0` and `VM_SERIAL=` for a log.
+135
View File
@@ -0,0 +1,135 @@
---
name: wsl-build-host
description: Use when building or testing the Steam Machine ISO, or running the VM tests (vmtest.sh), on the user's Windows PC - Arch Linux in WSL2 (`ssh wsl`), with podman, QEMU + KVM and WSLg windows on the Windows desktop. Covers access, syncing working trees from the laptop, the podman ISO build as root, VM windows vs headless, showing output in the user's WSL terminal, freeing disk space, and the WSL pitfalls (closing the window kills it, no GPU passthrough).
---
# WSL build host (Arch on WSL2, the Windows PC)
The laptop is an arm64 Mac: x86_64 QEMU or containers there run emulated,
far too slow for Plasma/Calamares or an ISO build. The PC is the x86 host:
same workflows as `steam-machine-iso`, `vm-install` and `vmtest.sh`, with
the differences below. Found and tried on 2026-09-28 (ISO build, ISO VM with
a window, the loop-mount fallback). 2026-09-29: the whole `vmtest.sh` ran here, 328 checks pass in
18 minutes (3 VMs at a time).
## Access
- `ssh wsl` = `root@10.0.0.36`, port 22 (alias in the laptop's
`~/.ssh/config`). That's Arch's own sshd inside WSL, not Windows' OpenSSH.
- `%UserProfile%\.wslconfig`: `networkingMode=mirrored` (WSL shares the PC's
LAN IP), `nestedVirtualization=true` (`/dev/kvm`), `memory=` (WSL2 defaults
to half the RAM: 30 GB seen). `/etc/wsl.conf`: `[boot] systemd=true`.
- Installed: podman, qemu-full, edk2-ovmf (`/usr/share/edk2/x64/OVMF*.4m.fd`,
run.sh finds it), git, rsync, python, python-pillow (`qmpshot.py` writes a
.ppm without it). vmtest.sh also wants `screen`.
- **The Arch window must stay open.** Closing the last WSL terminal shuts
the distro down a little later, with every build, VM and sshd in it: SSH
times out while the PC still pings. Ask the user to keep it open during
runs and to reopen it after a Windows reboot. After such a restart, delete
half-written VM disks before rerunning (`vminstall.sh` refuses an
existing `disk.qcow2`, and `--force` asks).
## Differences from a normal Linux host
- User is **root**: no `sudo`, `systemd-run` without `--user`, paths under
`/root/projects/` and `/root/vms/`. Units have no `$HOME`: set `HOME=/root`
for scripts started with `systemd-run` (vminstall.sh needs it).
- udisks: `vminstall.sh` uses `udisksctl` when it's there (udisks2 is
installed on this box now, and works) and falls back to `losetup -P` +
`mount` as root without it. The ISO's loop device is set up under a lock
(parallel installs collided: "Device or resource busy").
- Root is the host user: `vminstall.sh` then makes `theupriser` the guest
user (never root).
- `pkill -x qemu-system-x86_64` never matches (names over 15 characters):
`pkill -f '^[q]emu-system'`.
## Getting the code there
The WSL clones have no GitHub key: sync the laptop's working trees
(uncommitted changes included) instead of pulling. `vmtest-remote.sh`
pulls the pushed branch on the remote, so it needs a GitHub key there first
(and `REMOTE_REPO=/root/projects/steamify-cachyos-dev`).
```bash
cd ~/Projects && rsync -az --exclude out --exclude build \
steamify-cachyos-live-iso steamify-cachyos steamify-cachyos-dev wsl:projects/
ssh wsl 'find ~/projects -name __pycache__ -prune -exec rm -rf {} +; chown -R root: ~/projects'
```
Both fixups are needed: rsync keeps the laptop's uid (git then refuses with
"dubious ownership"), and a synced `patches/__pycache__` breaks
`steamify-prepare.sh` ("Is a directory").
## Building the ISO (~8 minutes)
```bash
ssh wsl 'cd ~/projects/steamify-cachyos-live-iso && ./steamify-prepare.sh ~/projects/steamify-cachyos'
ssh wsl 'cd ~/projects/steamify-cachyos-live-iso && rm -rf build out && systemd-run --collect -q -u isobuild-$(date +%s) --working-directory=$PWD bash -c "podman run --rm -t --pids-limit=-1 --ulimit nofile=65536:65536 --privileged --network=host -v /root/projects/iso-cache:/var/cache/pacman/pkg -v $PWD:/iso -w /iso docker.io/cachyos/cachyos:latest bash -c '\''pacman-key --init && pacman-key --populate && pacman -Syu --noconfirm --needed archiso mkinitcpio-archiso git squashfs-tools grub sudo && ./build-live-modules.sh && ./build-calamares-modules.sh && { ./buildiso.sh -p desktop -w || ./buildiso.sh -p desktop -c -w; }'\'' > /root/projects/iso-build.log 2>&1"'
```
Wait in one background command, not a polling loop:
`ssh wsl 'until ! podman ps -q | grep -q .; do sleep 20; done; tail -c 1500 ~/projects/iso-build.log; ls -la ~/projects/steamify-cachyos-live-iso/out/desktop/'`.
Never start a build while `podman ps` shows one. Output:
`/root/projects/steamify-cachyos-live-iso/out/desktop/steamify-cachyos-local-x86_64.iso` (a build by hand
has no release tag, so it's named `local`, label `STEAMIFY_<version>_LOCAL`; releases are built on the Gitea
mirror from a GitHub tag, see the ISO repo's `iso-1-github-tag.yml`) (from
Windows Explorer: `\\wsl$\<distro>\root\projects\...`). The trailing
`chown: missing operand` / "unknown error" is harmless. mksquashfs shows no
progress in the log; the growing `build/iso/arch/x86_64/airootfs.sfs`
(~3.1 GB when done) is the measure.
## VM windows on the Windows desktop (WSLg)
There is a desktop after all: WSLg (`/tmp/.X11-unix/X0`,
`/mnt/wslg/runtime-dir/wayland-0`) puts QEMU's GTK window on the user's
Windows desktop. SSH sessions and units lack the variables, so set them:
`DISPLAY=:0 WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR=/mnt/wslg/runtime-dir`.
- A VM for the user to watch or drive: that env plus e.g.
`VM_SERIAL=/root/vms/iso-vm/serial ISO_VM_DIR=/root/vms/iso-vm scripts/vmisoboot.sh --fresh --iso <iso>`.
The user may click through it themselves: check the disk size / serial
log before assuming it still sits at the live session.
- Automated runs stay headless (`--headless` / `VM_HEADLESS=1`, vmtest.sh's
default); `qmpshot.py` works then.
- GPU: no passthrough of the NVIDIA card. WSL2 only has the paravirtual
`/dev/dxg`, no PCI device for VFIO, and Windows keeps driving the card.
run.sh's default virgl (`virtio-vga-gl`, `gl=on`) renders through WSLg's
d3d12 Mesa on it, but QMP screenshots then give "no surface".
## Freeing disk space
Everything here is re-creatable (VMs, `~/vms/pkg-cache`, `~/projects/iso-cache`, `build/`, the podman image):
`rm -rf` them when the VMs aren't running (`pgrep -f "^[q]emu"`), `podman system prune -a -f`,
`pacman -Scc --noconfirm`, `fstrim -av`. That freed 68 GB inside WSL (81 -> 13 GB), but Windows only gets it
back when the vhdx is sparse: in an admin PowerShell `wsl --shutdown`, `wsl --manage archlinux --set-sparse
true` (seconds), reopen the Arch window, `fstrim -av` again; else `compact vdisk` in diskpart. A rebuild costs
the caches: a full `vmtest.sh --install` (three loader VMs ~12 min in parallel, the suite VM ~12 min from a
CachyOS ISO the user keeps in `/root/`), and an ISO build ~6-8 min. Don't `wsl --shutdown` while a job runs.
Hand-built ISOs are called `steamify-cachyos-local-x86_64.iso`; releases are built on the Gitea mirror
(`steamify-iso-release` skill). Windows starting WSL at logon is the user's own business, not Steamify's.
## Running the tests here
`scripts/vmtest.sh --screen` from `~/projects/steamify-cachyos-dev` (as root,
`HOME=/root`), then read `~/vms/last-test.txt`; follow it as the
`progress-report` skill says. Needs `~/vms/steamify-vm` (`VM_STEAMIFY=skip
VM_DIR=/root/vms/steamify-vm scripts/vminstall.sh --iso <CachyOS ISO>`, the
user keeps one in `/root/`) and `~/vms/bl-<loader>` (`vmbootloadertest.sh
<loader> --install`, or `vmtest.sh --install`). Memory: WSL sees 30 GB, fine
for 3 VMs at 4 GB (and the 8 GB install VMs); raise `memory=` in
`.wslconfig` before `MAX_PARALLEL=4` or `VM_MEM=8G`.
## Showing output in the user's WSL terminal
Their visible terminal is the `-bash` whose parent is WSL's `/init`
(`ps -eo pid,ppid,tty,args --forest`), usually `/dev/pts/0`. Not the
`login -- root` one: a hidden console getty. Stream a log into it as a unit
so it can be stopped:
`systemd-run --collect -q -u showlog bash -c "tail -n 20 -F <log> > /dev/pts/0 2>&1"`;
stop with `systemctl stop showlog` (Ctrl+C there doesn't). Tell the user
not to type there while it runs. A header printed before a busy log scrolls
away at once.
Reporting progress (the table, only on a change), a headless VM's screen in a
window (`scripts/vmview.py`) and looking into a stuck job: the
`progress-report` skill.
+26
View File
@@ -0,0 +1,26 @@
name: Sync git.upriser.nl mirror
# Trigger the Gitea pull-mirror immediately instead of waiting for its interval,
# so Gitea builds/releases the ISO right after main or a tag lands on GitHub.
on:
push:
branches: [main]
tags: ['*']
workflow_dispatch:
jobs:
sync:
# Gitea also reads .github/workflows on the mirror; only run on GitHub
if: github.server_url == 'https://github.com'
runs-on: ubuntu-latest
steps:
- name: Trigger mirror-sync
env:
GITEA_URL: ${{ secrets.GIT_UPRISER_URL }}
GIT_UPRISER_TOKEN: ${{ secrets.GIT_UPRISER_TOKEN }}
# Gitea "owner/repo"; defaults to the same path as on GitHub
GITEA_REPO: ${{ vars.GITEA_REPO || github.repository }}
run: |
curl --fail-with-body -sS --retry 5 --retry-delay 10 --retry-all-errors -X POST \
-H "Authorization: token ${GIT_UPRISER_TOKEN}" \
"${GITEA_URL%/}/api/v1/repos/${GITEA_REPO}/mirror-sync"
+9 -3
View File
@@ -5,16 +5,21 @@
!/README.md
!/CHANGELOG.md
!/run.sh
!/run.ps1
!/get-iso.sh
!/get-iso.ps1
!/add-ssh-key.ps1
!/TESTPLAN.md
!/TODO.md
!/AGENTS.md
!/.github/
!/scripts/
!/share/
!/.claude/
share/*
!share/guest-ssh-setup.sh
!share/vminstall-live.sh
!share/vminstall-post.sh
!share/bootloader-test/
!share/vmtest/
# Belt and braces, anywhere in the tree
*.iso
@@ -26,3 +31,4 @@ share/*
*.log
*.tmp
host-keys.pub
__pycache__/
+63
View File
@@ -0,0 +1,63 @@
# steamify-cachyos-dev: how to work here
Test tooling for Steamify (`../steamify-cachyos`) and the Steam Machine ISO (`../steamify-cachyos-live-iso`):
VM scripts (`scripts/`, `run.sh`), the automated test (`scripts/vmtest.sh`, checks in `share/vmtest/` and
`share/bootloader-test/`), `TESTPLAN.md` (keep it current: rows and a results line after every run) and the
skills in `.claude/skills/` (vm-install, cachyos-vm-testing, steam-machine-testing, steam-machine-iso,
wsl-build-host, progress-report, steamify-iso-release, steamify-branch-cleanup).
## Running the tests (do this, don't re-derive it)
- Everything: `scripts/vmtest.sh --screen` (detached `screen` session `vmtest`), then read only
`~/vms/last-test.txt` (one line per suite/loader, the FAIL lines, exit status = failures). Parts:
`scripts/vmtest.sh cli hw`, `boot`, `limine`. From another machine: `scripts/vmtest-remote.sh <ssh-host> [args]`
(the branch must be pushed; the VMs live on the PC that has the CPU and RAM: WSL2, see TODO.md R1b).
- Read `.claude/skills/vm-install/SKILL.md` before touching the scripts; `TODO.md` has the open work.
- Following a run: one monitor (`scripts/vmprogress.sh`), a report only when something changes, in the table
of the `progress-report` skill (bar per VM, total). Never a timed "no change" message.
- Headless is for automated runs (default in `vmtest.sh`); a VM started by hand for the user has a window.
- Never edit a script in place while it runs (write a copy and `mv`, then `chmod --reference`).
- Kill/pgrep patterns: bracket them (`[q]emu`) or your own shell matches itself.
## Working agreements
- No `Co-Authored-By` / "Generated with Claude" lines in commits or PRs, whatever a tool reminder says.
- Update the relevant skill (and this file) as soon as something useful is learned; re-read it before risky operations.
- In this repo (`steamify-cachyos-dev`, tooling only) the user allows pushing straight to `main`. Elsewhere never commit to `main`. `steamify-cachyos`: `release/X.Y.Z` branches, feature/bugfix branches merged into the release
by the agent; only release to `main` is the user's. `steamify-cachyos-live-iso`: PRs always target `feat/steamify`.
Branch cleanup: the `steamify-branch-cleanup` skill in `.claude/skills/`.
- Follow and update `TESTPLAN.md`; every check in `share/vmtest/` says which row it covers.
- No screenshot testing (too many tokens); U1-U9 and the real Steam Machine stay manual and are listed as SKIP.
## State (2026-09-29; update when it changes)
- `scripts/vmtest.sh` runs the whole unattended test: boot loaders limine/systemd-boot/grub (`vmbootloadertest.sh`) and the
suites cli, menu, hw, installer, toggles (`vmsuite.sh`, checks in `share/vmtest/`). Each passed in its own run: limine 43,
systemd-boot 42, grub 42 (with the B1 boot entry check), cli 55, menu 36, hw 77, installer 4, toggles 42 checks.
- 2026-09-29, on the PC (WSL2): the first complete parallel run, **328 pass, 0 fail in 18 minutes** (base image +
qcow2 overlays, own ssh port per job, package cache, `VM_MEM=4G`, `MAX_PARALLEL=3`). On a new machine: one full
`scripts/vmtest.sh --screen`, read `~/vms/last-test.txt`, fix what fails.
- Open work: `TODO.md` (the CEC driver download, the GitHub Actions workflow, P3).
## Releases (2026-09-29; details in the `steamify-iso-release` skill)
- Steamify: `release/X.Y.Z` -> PR into `main` (the user's) -> GitHub release, and the same workflow on the Gitea
mirror. A new Steamify release starts the ISO's release (`ISO_DISPATCH_TOKEN`); a CEC driver download is cached.
- The ISO: GitHub makes the tag `vX.Y.Z-[dev.]YYYY.MM.DD-HHMM` (UTC) and a release with the notes; the Gitea
mirror (git.upriser.nl, pull mirror, 10 min) builds on that tag and attaches the 3.2 GB ISO, named after the tag.
A release that exists only on the mirror is deleted by its next sync: the tag must come from GitHub.
- Merged feature/bugfix branches are deleted at once (`gh pr merge --delete-branch`). Push every commit.
## Where the tests run
The tests run on the user's PC (Ryzen 9800X3D, 64 GB, **WSL2**), started from a laptop over ssh (the user's own skill, or
`scripts/vmtest-remote.sh <ssh-host> [args]`: pulls the pushed branch there, starts `vmtest.sh --screen`, waits for
`~/vms/last-test.txt`). WSL specifics (access `ssh wsl` = root@10.0.0.36, keep the Arch window open or WSL shuts
down, syncing from the laptop, the podman ISO build, VM windows through WSLg, output in the user's WSL terminal, no GPU
passthrough): `.claude/skills/wsl-build-host/SKILL.md`. `vminstall.sh` works there without udisks (losetup as root).
## Problems found in the Steamify ISO (fix in `steamify-cachyos-live-iso`, PRs into `feat/steamify`)
- Fixed (PR #2): cachyos-installer's `bootctl install` runs in a chroot and writes no EFI boot entry, so systemd-boot
only booted through the disk's fallback path (in OVMF after 4-5 minutes of network boot, every boot).
`steamify-install` now registers it with efibootmgr from the live system.
- Fixed (PR #3): `/var/log` (`@log`) wasn't mounted when `steamify-install` ran, so its logs were hidden after boot.
- cachyos-installer leaves systemd-boot with `#timeout 3` and no default entry: a real machine waits in the menu for ever
(the test setup papers over it in `share/vminstall-post.sh`).
- It enables ufw, which drops ssh from the host (the test setup runs `ufw allow 22/tcp`).
- An installed system's package database can be older than what the installer got (mirror skew): a kernel "update" may
downgrade. Not a Steamify bug; noted in the vm-install skill.
+32
View File
@@ -4,6 +4,38 @@ All notable changes, per version and per commit. Versions follow
[Semantic Versioning](https://semver.org/) and were numbered from the start
of the history.
## 0.4.0 - 2026-09-28
An unattended VM install, a test plan, and the lessons from testing
Steamify 2.7.0.
- **feat: scripts/vminstall.sh installs an ssh-ready test VM unattended; TESTPLAN.md; vm-install skill**
- `scripts/vminstall.sh [--force] [--iso <file>] [--fremont]`: CachyOS from
the ISO's headless `cachyos-installer` (kernel booted directly with
`systemd.run=` + `systemd.wants=`, files and log over
`scripts/vminstall-server.py`), then `share/vminstall-post.sh`: sshd,
passwordless sudo, test autologin, Limine `default_entry`. Snapshots
`clean` and `ssh-ready`. User: the host's username, password `steamify`;
key `~/.ssh/steamify-vm_ed25519` (made once, never overwritten).
- `scripts/common.sh`: `VM_DIR`, and the VM's user, key and known_hosts from
it; `run.sh`: `VM_ISO`, `VM_KERNEL`/`VM_INITRD`/`VM_APPEND`, `VM_SERIAL`,
`repo-path`.
- `scripts/steammachine-addkey.sh`: the VM key on the Steam Machine too.
- `TESTPLAN.md`: regression, the app, features per release, Steam Machine
hardware (faked and real), results log. `scripts/qmpkey.py` presses keys
in the VM (drives the app).
- Fixes: `vmreset.sh` doesn't restart plasmalogin when the snapshot already
logged in (HELPER_TTY_ERROR); `vmstate.sh` reads `~/.local/state/steamify`;
`vminstallsim.sh` passes `LANG=C.UTF-8` like the ISO.
- Skills: update `ssh-ready` at the start of every session; the ISO is built
in the test VM.
## 0.3.0 - 2026-09-27
The Steam Machine ISO and Steamify's install-time mode.
- `f8e2473` **feat: steam-machine-iso skill (ISO build in podman on the Steam Machine, installer simulation, first-login test) and scripts/vminstallsim.sh**
## 0.2.0 - 2026-09-24
Watchable test runs and the lessons from testing cachyos-gamescope-boot 0.7.0
+66 -66
View File
@@ -11,116 +11,116 @@ tracked file to it explicitly.
## Requirements
- Linux host with KVM, `qemu-system-x86_64` (GTK/OpenGL display, virtfs/9p) and `qemu-img`
- An x86_64 Linux host with KVM (not Windows or macOS: `run.sh` relies on KVM,
memfd and GTK/virgl, and CachyOS only exists for x86_64),
`qemu-system-x86_64` (GTK/OpenGL display, virtfs/9p) and `qemu-img`
(Ubuntu: `sudo apt install qemu-system-x86 qemu-utils ovmf`).
- OVMF firmware at `/usr/share/OVMF/OVMF_CODE_4M.fd` and `OVMF_VARS_4M.fd`.
- `curl` and `sha256sum` for `get-iso.sh`.
- Optional: an NVIDIA dGPU for `--nvidia` (PRIME offload of the virgl renderer).
### Windows
`run.ps1` and `get-iso.ps1` are Windows versions. They need
[QEMU for Windows](https://qemu.weilnetz.de/w64/) (or set `QEMU_DIR`) and the
"Windows Hypervisor Platform" feature (`-accel whpx`). Windows QEMU has no 9p
shares: copy the repo into the guest with `scp -P 2222 -r ...`, and authorize
your key with `.\add-ssh-key.ps1 -User <vm-user>`.
## Quick start
## Quick start: unattended install
```bash
./get-iso.sh # download and verify the latest CachyOS desktop ISO
./run.sh install # first run creates disk.qcow2 (60G) and vars.fd, boots the ISO
./get-iso.sh # latest CachyOS desktop ISO (verified)
VM_DIR=~/vms/steamify-vm scripts/vminstall.sh --fremont # 15-30 min, watch the VM's window
```
Install CachyOS with the **KDE Plasma** desktop and **plasma-login-manager**
as login manager. Power off and snapshot (only while the VM is off):
Installs CachyOS from the ISO without any manual step, with CachyOS's own
headless installer (KDE Plasma, plasma-login-manager, btrfs, Limine,
`linux-cachyos` + `-lts`), and leaves the snapshots `clean` and `ssh-ready`:
sshd with your key, passwordless sudo, autologin into Plasma, English, the
host's timezone. Details: [`.claude/skills/vm-install`](.claude/skills/vm-install/SKILL.md).
- **Login by hand**: your host username, password **`steamify`** (root too).
- **SSH**: `~/.ssh/steamify-vm_ed25519`, made on the first install (it asks:
that key, or one of your own `~/.ssh/*.pub`), then always used. Existing
keys are never overwritten; the VM's host key lives in `$VM_DIR/known_hosts`,
not in `~/.ssh/known_hosts`.
- `$VM_DIR` also records the user, key and Steamify checkout (`vm-user`,
`ssh-key`, `repo-path`); pass the same `VM_DIR` to the other scripts.
- `--iso <file>` installs another archiso-based ISO the same way (the Steam
Machine ISO, later); `--force` replaces an existing VM (asks first).
Snapshots (only while the VM is off; keep vars.fd with the disk):
```bash
qemu-img snapshot -c clean disk.qcow2 && cp vars.fd vars.clean.fd
qemu-img snapshot -a ssh-ready disk.qcow2 && cp vars.ssh-ready.fd vars.fd # restore
```
Boot the installed system (`./run.sh`) and, in the guest, enable SSH with key
auth and passwordless sudo through the read-only `vmtools` share:
### Manual install (fallback)
`./run.sh install` boots the ISO (a first run creates `disk.qcow2`, 60G, and
`vars.fd`). Install CachyOS with **KDE Plasma** and **plasma-login-manager**
through Calamares, power off, `qemu-img snapshot -c clean disk.qcow2 && cp
vars.fd vars.clean.fd`. Boot it (`./run.sh`) and, in the guest:
```bash
sudo mount -t 9p -o trans=virtio,version=9p2000.L vmtools /media && /media/guest-ssh-setup.sh
```
### Adding your SSH key
(sshd, the host's `~/.ssh/*.pub` from `share/host-keys.pub`, passwordless
sudo). Power off, `qemu-img snapshot -c ssh-ready disk.qcow2 && cp vars.fd
vars.ssh-ready.fd`.
`guest-ssh-setup.sh` authorizes the host's public keys. `run.sh` copies them
at every start from `~/.ssh/*.pub` into `share/host-keys.pub` (git-ignored),
which the guest sees as `/media/host-keys.pub`. So:
1. Make sure you have a key on the host; create one if `ls ~/.ssh/*.pub`
shows nothing:
```bash
ssh-keygen -t ed25519
```
2. Start (or restart) the VM with `./run.sh`, so the key is copied into the
share.
3. In the guest, run the setup script (above). It installs and starts
`sshd`, opens port 22 if a firewall is active, adds every host key to
`~/.ssh/authorized_keys` (without duplicates) and gives the guest user
passwordless sudo.
4. Test from the host: `ssh -p 2222 <vm-user>@localhost true` should return
without asking for a password.
If you ran the setup script before the key existed (it prints "No host keys
were shared"), or you want to add another key later, either restart the VM
and run the setup script again, or copy the key over SSH with the guest
user's password:
### The same key on the Steam Machine
```bash
ssh-copy-id -p 2222 <vm-user>@localhost
scripts/steammachine-addkey.sh [user@host] # default: steammachine (~/.ssh/config)
```
On Windows, use `.\add-ssh-key.ps1 -User <vm-user>` instead (it creates a key
if needed). If SSH hangs at "banner exchange", `sshd` isn't running in the
guest or a firewall blocks port 22; check with `systemctl is-active sshd` in
the guest.
Power off and snapshot again:
```bash
qemu-img snapshot -c ssh-ready disk.qcow2 && cp vars.fd vars.ssh-ready.fd
```
Connect with `ssh -p 2222 <user>@localhost`. The project repo is shared
read-write as 9p tag `repo`; in the guest:
`sudo mount -t 9p -o trans=virtio,version=9p2000.L repo /mnt`.
authorizes `~/.ssh/steamify-vm_ed25519` on the Steam Machine too (asks its
password once), so one key reaches the VM and the real machine.
### run.sh options
```
[REPO=/path/to/cachyos-gamescope-boot] ./run.sh [install] [--nvidia] [--vulkan] [--fremont]
[REPO=/path/to/steamify-cachyos] ./run.sh [install] [--nvidia] [--vulkan [--amd]] [--fremont]
```
- `install`: boot the installer ISO
- `install`: boot the installer ISO (`VM_ISO`, default `cachyos.iso`)
- `--nvidia`: render the guest's virtio-gpu (virgl) on the host NVIDIA dGPU
- `--vulkan`: expose Vulkan to the guest (venus; unstable)
- `--fremont`: fake the Valve Steam Machine (Fremont) DMI data via `-smbios`
- `REPO` defaults to `$HOME/projects/cachyos-gamescope-boot` (the local clone of
[steamify-cachyos](https://github.com/theupriser/steamify-cachyos))
- `REPO`: the Steamify checkout shared as 9p `repo` (in the guest: `sudo
mount -t 9p -o trans=virtio,version=9p2000.L repo /mnt`); default
`repo-path` next to the disk, else `$HOME/projects/cachyos-gamescope-boot`
- `VM_KERNEL`/`VM_INITRD`/`VM_APPEND` (boot a kernel directly) and
`VM_SERIAL` (serial console log + socket): used by `vminstall.sh`
## Helper scripts
- `scripts/vminstall.sh [--force] [--iso <file>] [--fremont]`: unattended install (above)
- `scripts/vmreset.sh [--fremont]`: restore `ssh-ready`, boot, mount the repo, autologin into Plasma
(and skip the broken krfoss mirror, install shellcheck)
- `scripts/vmrun.sh '<menu input>'`: run the wizard in the guest's Plasma session with scripted input
- `scripts/vmwatch.sh [--release] '<menu input>' [label]`: same, but in a visible Konsole window
in the VM; `--release` runs the newest GitHub release instead of the mounted repo
- `scripts/vminstallsim.sh [--fresh] [--defaults options]`: Steamify's install-time mode for a
user who never logged in (the Steam Machine ISO's installer step)
- `scripts/vmshot.sh [--clean] <out.png>`: screenshot the guest's desktop (`--clean` closes
Steam, CachyOS Hello and Konsole first)
- `scripts/vmstate.sh`: print the state of every wizard component
- `scripts/vmcec.sh [--no-sleep]`: HDMI-CEC against a fake TV (vivid)
- `scripts/qmptype.py <qmp.sock> "<text>"` / `scripts/qmpkey.py <qmp.sock> <keys>`: type text / press
keys in the VM (e.g. drive the Steamify app: `down right ctrl-ret ret`)
- `scripts/cmp.sh [save]`: save / diff the guest's KDE configs against a baseline
- `scripts/steammachine-addkey.sh [user@host]`: the VM key on the Steam Machine (above)
They use `VM_USER` (default `theupriser`), `VM_PORT` (`2222`) and `VM_HOST` (`localhost`).
They use `VM_DIR` (default: this repo if it has `disk.qcow2`, else
`~/vms/cachyos-test`), and from it `VM_USER` (`vm-user`, else `theupriser`)
and `VM_SSH_KEY` (`ssh-key`); `VM_PORT` (`2222`), `VM_HOST` (`localhost`).
## Claude Code skill
## Test plan
The full test workflow is documented in
[`.claude/skills/cachyos-vm-testing/SKILL.md`](.claude/skills/cachyos-vm-testing/SKILL.md);
Claude Code picks it up when run from this repo.
[`TESTPLAN.md`](TESTPLAN.md): what to test per release (regression, the app,
new features, Steam Machine hardware faked and real) and the results log.
## Claude Code skills
- [`cachyos-vm-testing`](.claude/skills/cachyos-vm-testing/SKILL.md): the test workflow in the VM
- [`vm-install`](.claude/skills/vm-install/SKILL.md): creating the VM unattended
- [`steam-machine-testing`](.claude/skills/steam-machine-testing/SKILL.md): on the real Steam Machine
- [`steam-machine-iso`](.claude/skills/steam-machine-iso/SKILL.md): the Steam Machine ISO
Claude Code picks them up when run from this repo.
+144
View File
@@ -0,0 +1,144 @@
# Steamify test plan
What to test before a Steamify release, in the VM (`--fremont`) and on the
real Steam Machine. How to run each step: `.claude/skills/cachyos-vm-testing`.
**Keep this file up to date**: add a section when a feature is added, and a
line to the results log after every test run.
Every VM run starts with the session start of the skill (`ssh-ready` brought
up to date), and each numbered block below starts from a fresh `ssh-ready`
(`scripts/vmreset.sh --fremont`, `REPO=` the branch checkout). Runs are
visible in a Konsole on the VM's desktop.
## R. Regression (every release)
### R1. Menu on an existing desktop (user `theupriser`)
| # | Step | Expect |
|---|---|---|
| R1.1 | `'q\n'` | first-run ticks: everything but Boot into desktop, BIOS |
| R1.2 | `'\ny\nm\nq\nn\n'` | every item OK, back in the menu with all on |
| R1.3 | `scripts/vmstate.sh` | SDDM + autologin, Vapor, 17 LEDs owned by the user, DKMS `installed` for every kernel, steamos-manager active |
| R1.4 | rerun `'\nq\n'` | "Everything is already the way you want it" |
| R1.5 | re-apply `'a\ny\nm\nq\nn\n'`, `vmstate.sh` | no errors, no `DUPLICATES` |
| R1.6 | `steamify.sh --backend status` | valid JSON, current `VERSION`, states match R1.3 |
| R1.7 | close the app first, then `sudo modprobe vivid; scripts/vmcec.sh --no-sleep` | all PASS (2 SKIP in the VM: wake from TV, sleep). The fake remote's Select arrives as Enter: an open app on "All done" restarts the PC |
| R1.8 | toggles (theme, single user, CEC, Steam Machine support off/on) | full matrix in the skill; `cmp.sh` clean after theme off |
| R1.10 | set an item older in `~/.local/state/steamify/features.state` (e.g. `cec=2.1.0`), `'q\n'`, then `'\ny\nm\nq\nn\n'` | menu shows "HDMI-CEC (update)" ticked; the plan lists it as "update"; after the run the file has the current version |
| R1.9 | reboot checks | single user: SDDM logs straight in; everything off: greeter |
### U. The app (`/mnt/ui/steamify-ui`, on the VM's desktop)
Drive it with `scripts/qmpkey.py <vm dir>/qmp.sock <keys>` (QEMU qcodes:
`up down left right ret esc ctrl-ret ctrl-q r`; wait ~1 s between keys and
~12 s after starting the app) and check every screen with a screenshot
(`spectacle -b -n -a` for the active window; the progress screen of a quick
change only shows right after Enter).
| # | Step | Expect |
|---|---|---|
| U1 | start after R1.5 | header `v<VERSION>`, Steam Machine chip, every row with "now on", Kernel / LED bar (17) / HDMI-CEC / Version panel |
| U2 | Up/Down over the rows | detail panel follows: body text and "What it changes" per row |
| U3 | an updated feature (bump `FEATURE_VERSION`, or `features.state` older) | `UPDATE` badge, "1 change" |
| U4 | Boot into: Right / Left | Gaming / Desktop, detail panel `DESKTOP`, change count +1 |
| U5 | Enter on a toggle, Enter again | off, back on, count back |
| U6 | Ctrl+Enter | review lists exactly the changes, summary counts right; Esc goes back |
| U7 | Enter (apply) | progress per step ("Boot into desktop", update badge), details log, "All done, N changes applied", Restart now / Back to the menu |
| U8 | after U7 | the changes are on the system (e.g. boot-desktop unit, `features.state`) |
| U9 | `r` (re-apply), Ctrl+Q | re-apply review; the app quits |
### R2. Installer mode (`scripts/vminstallsim.sh`, user `isotest`, no session)
| # | Step | Expect |
|---|---|---|
| R2.1 | plain `--defaults` | every item OK, `exit: 0`, first-login autostart present |
| R2.2 | first desktop login (steam-machine-iso skill) | Vapor layout, `primaryActions=3`, autostart gone, app opens |
### F2.8.0 `--defaults --list`
| # | Step | Expect |
|---|---|---|
| G1 | `steamify.sh --defaults --list` | exit 0, valid JSON array, no stderr, no header text mixed in |
| G2 | items | actions (bios), retired (kpin, hdmi) excluded; unavailable here (vram, steamgame) excluded; poweroff has parent machine, boot has parent gaming and kind choice |
| G3 | menu / plain `--defaults` still work | unaffected (regression) |
## F. Features per release
### F2.7.0 `--defaults --options` / `--boot`
| # | Step | Expect |
|---|---|---|
| F1 | `--options` (no value), `bogus`, `boot`, `bios`, `--boot` (no value), `--boot sideways`, `--skip x` | each `exit: 1` with a clear error, nothing changed |
| F2 | `--options theme --boot desktop` (either order) | `exit: 1`: desktop needs the conversion |
| F3 | `--options gaming,theme,glyphs,single,launcher,notify,vram,cec,machine,poweroff --boot desktop` | all on, boot-desktop unit enabled; VRAM left out with a warning in the VM |
| F4 | `--options theme,single,machine --boot desktop` | single brings the conversion; poweroff, CEC, glyphs, launcher, notify recorded `off` |
| F5 | `--options launcher,vram,poweroff` | poweroff brings Steam Machine support; conversion off, plasmalogin unchanged |
| F6 | `steamify.sh --boot desktop` without the conversion | `exit: 1` |
| F7 | on an install: `--boot desktop`, again, `--boot gamescope` | only Boot into changes; "Already starting in desktop"; menu ticks unchanged |
## H. Steam Machine hardware
### H-VM. With the faked Steam Machine (`--fremont`)
| # | Step | Expect |
|---|---|---|
| H1 | LED driver | `leds_valve` loaded, 17 `valve-leds*` nodes owned by the user (no light to see) |
| H2 | HDMI-CEC | R1.7 (vivid: fake TV and remote) |
| H3 | BIOS update | `BIOS_VERSION=F7F0107 scripts/vmreset.sh --fremont`, `WIZARD_BIOS_DRY_RUN=1`: whole flow, never flashes |
| H4 | Boot into desktop, reboot | Plasma straight after boot; Boot into gaming: SDDM autologin into gamescope (black in the VM, see the skill; check the session file) |
| H5 | power-off module | DKMS built for every kernel, module loads, "No such device" (no GPIO wake bit in the VM) |
### H-Real. Only on the real Steam Machine
- Power-off fix: stays off after shutdown, for every new major kernel.
- VRAM booster (the VM has no `dmem` VRAM region).
- Gamescope itself: gaming mode, Switch to Desktop / Return to Gaming Mode.
- LED bar light, real TV over HDMI-CEC (remote, TV on/off, sleep/wake).
## B. Boot loaders (`scripts/vmtest.sh`, unattended)
Every loader the ISO advertises (Limine, systemd-boot, GRUB), each on a VM installed from the Steamify ISO
and started with `--fremont`. Automated: `scripts/vmtest.sh` (see the vm-install skill).
| # | Step | Expect |
|---|---|---|
| B1 | first boot after the unattended install, and every reboot | boots by itself through the loader's own EFI boot entry (`BootCurrent`, a real partition; checked after every reboot), no network boot first (systemd-boot: the entry comes from the ISO's `steamify-install` since PR #2; `timeout`/`default` set by the test setup), SSH answers |
| B2 | Steamify's state, as its own `*_status` functions | gaming, theme, glyphs, single, launcher, notify, poweroff, cec all on |
| B3 | OS name | `os-release` NAME/PRETTY_NAME are CachyOS's, `lsb-release` has no "with Steamify"; Limine has `TARGET_OS_NAME` |
| B4 | loader identity, firmware setup | the expected loader; `Boot into FW: supported` (BIOS item) |
| B5 | DKMS modules (`leds-valve`, `cros-ec-cec`, `steamify-fremont-poweroff`) | installed for every kernel, files present, `modules-load.d` entry |
| B6 | legacy `drm.edid_firmware` removal (`hdmi_remove_boot_param`) | detected, removed, initramfs and entries rebuilt, no edid left |
| B7 | kernel update (both kernels + headers) | DKMS rebuilds 3 modules per kernel, initramfs rebuilt, loader config regenerated |
| B8 | reboot into the default and the other kernel | both boot, no failed units, power-off fix and LED modules loaded on each |
## Known gaps
- "Add as non-Steam game" can't be set up at install time (no Steam account
yet); not covered by any installer test.
- ISO: `steamify-install` and the packagechooser pages still pass `--skip`
(removed in 2.7.0); to be replaced by the Steamify page.
- `/etc/plasmalogin.conf` missing before Steamify (a VM from vminstall.sh,
not Calamares): turning the conversion off leaves an empty file instead of
none (harmless; Calamares installs have one, restored from the backup).
- R2.2: the first login's app is the newest *release* (`run-app`), so before
a release it shows the old version; start the branch's app by hand
(steam-machine-iso skill).
- CEC: the driver source comes from an unofficial mirror of Valve's kernel on GitHub; since 2.9.1 it is
downloaded once and cached (/var/cache/steamify; the VMs share the host's), so only a first download
can hit GitHub's rate limit (HTTP 429).
## Results log
| Date | Branch / version | Blocks | Result |
|---|---|---|---|
| 2026-09-28 | `feat/defaults-options` 2.7.0 (uncommitted `--options`, `--boot`) | R1.1-R1.6, R2.1, F1-F7 | all pass (VM `--fremont`) |
| 2026-09-28 | same + CEC socket fix, UI label fix | R1.7, R1.10, U1-U8, H1, H4 (desktop), H5 | R1.7 first failed: `cec-audio-control.socket` never enabled (old bug, fixed: `cec_enable` enables it, `FEATURE_VERSION[cec]=2.7.0`); after: 53 PASS / 0 FAIL / 2 SKIP. UI: progress row said "Boot into Boot into" (fixed). Rest pass. Not run: R1.8, R1.9, R2.2, U9, H3 |
| 2026-09-28 | `release/2.7.0` (VM from vminstall.sh, `~/vms/steamify-vm`) | R1.2 (fresh, CEC socket), R1.8, R1.9, R1.5 via U9, U9, R2.1, R2.2, H3 | all pass except one old bug: theme on (single on) → single off → theme off left single's launcher keys; fixed (`58e43d4`, PR #34) and retested. BIOS dry run F7F0107 → F7F0108 passed; greeter and SDDM reboot checks passed |
| 2026-09-28 | `release/2.7.0` | H-Real | counted as passed: power-off fix, VRAM booster, LEDs, gamescope and Steam Machine support are unchanged since 2.6.0, which passed on the real Steam Machine. HDMI-CEC's change (enabling `cec-audio-control.socket`) passed with the fake TV in the VM; check the TV remote's volume keys on the real TV after updating (HDMI-CEC shows as update) |
| 2026-09-29 | `feature/vminstall-bootloader` (Steamify ISO 2026.09.28, `--fremont`) | B1-B8 | Limine 41 / systemd-boot 40 / GRUB 40 checks pass. Found on the way (test setup, not Steamify): ufw blocks ssh, systemd-boot has no timeout/default after cachyos-installer |
| 2026-09-28 | `release/2.8.0` (`feature/defaults-list`) | G1-G3, R1.1, R1.2 | all pass (VM `--fremont`, fake Steam Machine). Bundle shellcheck clean |
| 2026-09-29 | Steamify 2.9.0 (`steamify-cachyos` main), ISO `feat/steamify` with PR #2 (2026.09.29), dev `main` 5b867f8; on the PC (WSL2) | everything automated: B1-B8 (3 loaders), cli, menu, hw, installer, toggles (`scripts/vmtest.sh`, parallel, `MAX_PARALLEL=3`) | **328 pass, 0 fail** (toggles 42 +1 expected skip, hw 70, cli 55, menu 36, installer 4, Limine 41, systemd-boot 40, GRUB 40) in 18 minutes: the first complete parallel run. Found on the way: systemd-boot had no EFI boot entry (installer's `bootctl` in a chroot), fixed in the ISO (PR #2); `vminstall.sh` fixes for parallel installs and a root host user |
| 2026-09-29 | ISO `feat/steamify` + PR #3 (`/var/log`), dev `main` with the B1 boot entry check | B1-B8 | Limine 43 / GRUB 42 pass (boot entry check passes: `Limine`, `cachyos`). systemd-boot (reinstalled): 36 pass, 8 fail, all CEC: the driver download got HTTP 429 from GitHub (rate limit); boot entry check passes (`Linux Boot Manager`); `/var/log/steamify-install.log` and `steamify-bootentry.log` readable after boot |
| 2026-09-29 | Steamify `release/2.9.1` (CEC driver cache), ISO `feat/steamify` (PRs #2, #3), dev `main` | everything automated incl. hw block 50 (CEC cache) and the B1 boot entry check | **341 pass, 0 fail** in 19 minutes (toggles 42 +1 expected skip, hw 77, cli 55, menu 36, installer 4, Limine 43, systemd-boot 42, GRUB 42) |
+95
View File
@@ -0,0 +1,95 @@
# TODO
## GitHub Actions: run the boot loader test in CI
Goal: every push/PR (and on demand) runs the whole automated test (`scripts/vmtest.sh`, see the next
section: boot loaders plus every suite) on runners, one job per suite or loader, and fails on any `FAIL` line.
Public repos, so the free Linux runners with KVM apply. Work on `feature/ci-bootloader-test`,
PR into the release branch. Tick items off as they're done.
- [x] 1. `run.sh`: headless mode (`VM_HEADLESS=1` -> `-display none`, plain virtio-vga); still to do: make `vmtest.sh` and `vmbootloadertest.sh` headless by default (`--window` opts out, a log Konsole opens on a desktop, CI opens none) and copy the repo's run.sh into `$VM_DIR` before starting
- [ ] 2. `vminstall.sh`: read the ISO's kernel/initramfs without `udisksctl` (bsdtar/7z when there is no session)
- [ ] 3. `vminstall.sh` / `vmbootloadertest.sh`: no `/dev/tty` prompts and no `pgrep` on the whole host in CI (`CI=1`)
- [ ] 4. Where the ISO comes from: the newest ISO release on git.upriser.nl (the Gitea mirror of
`theupriser/steamify-cachyos-live-iso` builds it and attaches it to the tag's release, see the
`steamify-iso-release` skill; GitHub's release links to it), or a `workflow_dispatch` input (release tag / URL)
- [ ] 5. `.github/workflows/bootloader-test.yml`: matrix over `limine, systemd-boot, grub`; steps: install
qemu + ovmf, enable /dev/kvm, fetch the ISO, run the script, upload `~/vms/bl-<loader>.test.log` as an artifact
- [ ] 6. Runner limits: disk (sparse 60 GB qcow2, free space check), RAM (8 GB VM on a 16 GB runner), timeout (60 min)
- [ ] 7. Cache the pacman packages between runs (`actions/cache` on `~/vms/pkg-cache`, `VM_CACHE`)
- [ ] 8. Trigger it once on the branch, fix what the runner shows; note the run time in the skill
- [ ] 8b. Second job, `--fremont` hardware tests from TESTPLAN.md section H (LED driver, CEC with vivid,
BIOS dry-run, boot into desktop/gaming and reboot, power-off module): guest-side, so they run on a runner too;
needs a way to check without screenshots (gaming mode doesn't render)
- [ ] 9. `vm-install` skill + `vmtest.sh` header: mention the workflow, and that the advertised-loader check fails CI when a loader has no test
- [x] 10. TESTPLAN.md: section B for the boot loader test (B1-B8), pointing at `scripts/vmtest.sh`
## Full automated test: everything in TESTPLAN.md that needs no person
One command, `scripts/vmtest.sh [suite...]` (suites: `boot`, `cli`, `menu`, `hw`, `installer`; default all),
each check prints `PASS`/`FAIL`, the summary counts them, exit status = failures. Each block starts from a
fresh `ssh-ready` (`scripts/vmreset.sh --fremont`), as TESTPLAN.md says. GitHub runs the same command.
Guest-side checks live in `share/vmtest/<suite>/`, one file per TESTPLAN block. Adding a row to
TESTPLAN.md means adding its check here (say which row each check covers in a comment: `# F4`).
Automatable (encode as checks): R1.1-R1.6, R1.7 (`vmcec.sh` already prints PASS/FAIL), R1.8 (toggle matrix),
R1.9, R1.10, R2.1, G1-G3, F1-F7, H1, H3, H4 (session file), H5, and the boot loader matrix (done).
Not automatable without a person (stay manual, listed in the summary as SKIP; no screenshot testing): U1-U9,
R2.2 (first desktop login look), H-Real (real Steam Machine).
- [x] S1. Framework (vmtest.sh runs boot + every suite, --screen, summary in ~/vms/last-test.txt): `scripts/vmtest.sh` takes suites, resets the VM per block, tallies PASS/FAIL/SKIP,
prints the skipped manual rows; move the loader matrix under suite `boot`
- [x] S2. Suite `cli` (55 checks pass): G1-G3, F1-F7 (exit codes, `features.state`, boot-desktop unit)
- [x] S3. Suite `menu` (36 checks pass): R1.1-R1.6, R1.10 with scripted menu input (`vmrun.sh`) and `vmstate.sh` asserts
- [x] S4. Suite `hw` (70 checks pass incl. H3 BIOS dry-run): H1, H3 (`WIZARD_BIOS_DRY_RUN=1`), H4, H5, R1.7 (`vmcec.sh --no-sleep`)
- [x] S5. Suite `installer` (4 pass): R2.1 (`vminstallsim.sh`)
- [x] S6. R1.8 toggle matrix as suite `toggles` (42 pass, 1 expected skip); R1.9 reboot checks still to add
- [ ] S7. TESTPLAN.md: mark which rows are automated (a column or a tag), keep the results log automatic:
the script appends a line to it (date, branch, suites, counts)
- [ ] S8. Update the `steam-machine-testing` and `cachyos-vm-testing` skills: "run `scripts/vmtest.sh`" first
## Speed: the full run takes about an hour (target 20-25 min)
Where the time goes: boot loader VMs ~3 min each, every suite block restores a snapshot and reboots
(~1 min), and every block that applies Steamify downloads Steam and the rest again from the mirrors.
- [x] P1 (proven 2026-09-29: full run in 18 min on the PC). Package cache for the suite VM: `VM_CACHE` shared as 9p and bound over `/var/cache/pacman/pkg`
in `vmreset.sh`/the prelude (as vminstall-live.sh does); downloads only once
- [x] P2 (base image + overlays, own port per suite, MAX_PARALLEL=3, VM_MEM=4G; first complete parallel run 2026-09-29: 328 pass, 18 min). Parallel: each suite (and each boot loader) on its own copy of the VM (`cp --reflink` of the disk and vars),
own `VM_PORT` and `VM_DIR`; `vmtest.sh` starts them together and waits; watch host CPU (16 cores, 6 vCPUs each)
- [ ] P3. Fewer resets: merge blocks that only read state (cli 10 + the menu R1.1)
## Run the tests on the PC (9800X3D, 64 GB) from a laptop
- [ ] R1. `scripts/vmtest-remote.sh <ssh-host>` written; try it once: the PC needs the three repos, qemu + OVMF + screen,
KVM (on Windows: WSL2 with nested virtualization), the VMs in `~/vms` (`vmtest.sh --install` builds the boot loader
VMs, `vminstall.sh --iso <CachyOS ISO>` with `VM_STEAMIFY=skip` the plain one), and the Steamify ISO
- [x] R1b (done 2026-09-29: full vmtest.sh run there, 328 pass in 18 min; setup in the wsl-build-host skill). The PC is WSL2 (Windows). To check there: `/dev/kvm` exists (Windows 11, virtualization on in the BIOS,
nested virtualization on); `%UserProfile%\.wslconfig` `memory=` is raised (WSL2 defaults to half the RAM = 32 GB;
3-4 VMs at 4-8 GB want ~48 GB) and `processors=16`; keep `~/vms` and the repos on WSL's own ext4, not under `/mnt/c`;
the distro is Arch (root): `pacman -S qemu-full edk2-ovmf screen`; automated runs are headless with no Konsole
(`view_start` returns early; watch with `screen -r vmtest` or `tail -f ~/vms/test.log`), a VM for the user gets a
window through WSLg (see the wsl-build-host skill). Still to do: a first full `vmtest.sh` run there
- [x] R1c. `vminstall.sh` without udisks (WSL): `losetup -P` + `mount` when `udisksctl` is missing and it runs as root
- [ ] R2. With 64 GB and 8c/16t: try `MAX_PARALLEL=4` and 8 GB VMs (`VM_MEM=8G`)
## Found 2026-09-29 (first full run on the PC)
- [x] B1 check (share/bootloader-test/boot-check.sh, after every reboot; passes on all three): the loader's own EFI boot entry exists, points to a real partition (not `HD(0,GPT,0000...)`)
and was used (`efibootmgr` `BootCurrent`); systemd-boot booted through the fallback path unnoticed
- [x] ISO: mount `@log` at `$ROOT/var/log` before `steamify-install` writes its logs (PR #3 merged)
- [x] Steamify 2.9.1 (PR #53 into release/2.9.1; hw block 50 tests it): the CEC driver comes from one download (raw.githubusercontent.com, no retry, no fallback);
a GitHub rate limit (HTTP 429, seen 2026-09-29 after a day of test installs) leaves CEC off. Ship the
pinned source with Steamify instead (steamify-cachyos, via a release branch). Done as a cache in
/var/cache/steamify (named after its checksum); the VMs share the host's (~/vms/pkg-cache/steamify).
Later maybe: ship the file itself (the source repo is an unofficial mirror)
- [x] ISO: systemd-boot's EFI boot entry (efibootmgr from the live system), PR #2 merged
- [x] vminstall.sh: a poweroff that drops ssh is no failure, wait only for its own VM, ISO loop device under
a lock, never root as the guest user
## Boot loader test (local)
- [x] `scripts/vmtest.sh [--install] [loader...]` runs all advertised loaders, `vmbootloadertest.sh` one
- [x] guest checks in `share/bootloader-test/` (state, modules, legacy param, kernel update, boot check)
- [x] first full run of all three through `vmtest.sh`: Limine 41 / systemd-boot 40 / GRUB 40, 0 failed
- [ ] test the `VM_CACHE` speed-up with a second install
-9
View File
@@ -1,9 +0,0 @@
# Windows: authorize your SSH key in the VM (asks for the VM password once).
# Run after guest-ssh-setup.sh inside the VM, or at least after sshd is enabled there.
# .\add-ssh-key.ps1 -User <vm-user>
param([Parameter(Mandatory)][string]$User)
$ErrorActionPreference = 'Stop'
$key = Join-Path $env:USERPROFILE '.ssh\id_ed25519.pub'
if (-not (Test-Path $key)) { ssh-keygen -t ed25519 -f ($key -replace '\.pub$', '') -N '""' }
Get-Content $key | ssh -p 2222 "$User@localhost" 'install -d -m 700 ~/.ssh && k=$(cat) && (grep -qxF "$k" ~/.ssh/authorized_keys 2>/dev/null || echo "$k" >> ~/.ssh/authorized_keys) && chmod 600 ~/.ssh/authorized_keys'
Write-Host "Done. Connect with: ssh -p 2222 $User@localhost"
-28
View File
@@ -1,28 +0,0 @@
# Download the latest CachyOS desktop ISO to cachyos.iso and verify its checksum.
# Windows version of get-iso.sh; re-running resumes a partial download.
$ErrorActionPreference = 'Stop'
Set-Location $PSScriptRoot
$mirror = if ($env:MIRROR) { $env:MIRROR } else { 'https://nl.mirror.cx/cachyos' }
$page = (Invoke-WebRequest -UseBasicParsing https://cachyos.org/download/).Content
$m = [regex]::Match($page, 'ISO/desktop/[0-9]+/cachyos-desktop-linux-[0-9]+\.iso')
if (-not $m.Success) { throw 'Could not find the current ISO on cachyos.org/download' }
$url = "$mirror/$($m.Value)"
$name = Split-Path $m.Value -Leaf
Write-Host "Latest release: $name"
if ((Test-Path cachyos.iso) -and ((Get-Content cachyos.iso.version -ErrorAction SilentlyContinue) -ne $name)) {
Write-Host 'Existing cachyos.iso is an older release, replacing it.'
Remove-Item cachyos.iso
}
# curl.exe ships with Windows 10+ and supports resuming (-C -).
curl.exe -fL -C - -o cachyos.iso $url
if ($LASTEXITCODE -ne 0) { throw 'Download failed' }
Set-Content cachyos.iso.version $name
Write-Host 'Verifying checksum...'
$expected = ((curl.exe -fsL "$url.sha256") -split '\s+')[0]
$actual = (Get-FileHash cachyos.iso -Algorithm SHA256).Hash
if ($actual -ieq $expected) { Write-Host 'cachyos.iso: OK' } else { throw "Checksum mismatch: $actual" }
-29
View File
@@ -1,29 +0,0 @@
# CachyOS test VM for steamify.sh -- Windows version.
# .\run.ps1 [-Install]
# Needs QEMU for Windows (https://qemu.weilnetz.de/w64/) and the Windows
# feature "Windows Hypervisor Platform" (for -accel whpx).
# Windows QEMU has no 9p shared folders: copy the repo in over SSH instead:
# scp -P 2222 -r ..\..\projects\cachyos-gamescope-boot <user>@localhost:
param([switch]$Install)
$ErrorActionPreference = 'Stop'
Set-Location $PSScriptRoot
$qemuDir = if ($env:QEMU_DIR) { $env:QEMU_DIR } else { 'C:\Program Files\qemu' }
$qemu = Join-Path $qemuDir 'qemu-system-x86_64.exe'
$code = Join-Path $qemuDir 'share\edk2-x86_64-code.fd'
if (-not (Test-Path vars.fd)) { Copy-Item (Join-Path $qemuDir 'share\edk2-i386-vars.fd') vars.fd }
if (-not (Test-Path disk.qcow2)) { & (Join-Path $qemuDir 'qemu-img.exe') create -f qcow2 disk.qcow2 60G | Out-Null }
$qargs = @(
'-accel', 'whpx,kernel-irqchip=off', '-machine', 'q35', '-cpu', 'max', '-smp', '6', '-m', '8G',
'-drive', "if=pflash,format=raw,readonly=on,file=$code",
'-drive', 'if=pflash,format=raw,file=vars.fd',
'-drive', 'file=disk.qcow2,if=virtio',
'-device', 'virtio-vga,xres=1920,yres=1080', '-display', 'sdl',
'-device', 'qemu-xhci', '-device', 'usb-tablet',
'-nic', 'user,model=virtio-net-pci,hostfwd=tcp::2222-:22'
)
if ($Install) { $qargs += @('-cdrom', 'cachyos.iso', '-boot', 'd') }
& $qemu @qargs
+45 -10
View File
@@ -1,11 +1,19 @@
#!/bin/bash
# CachyOS test VM for steamify.sh.
# [REPO=/path/to/cachyos-gamescope-boot] ./run.sh [install] [--nvidia] [--vulkan] [--fremont]
# [REPO=/path/to/cachyos-gamescope-boot] ./run.sh [install] [--nvidia] [--vulkan] [--amd] [--fremont] [--headless]
# install boot the installer ISO
# --nvidia render the guest's virtio-gpu (virgl) on the host NVIDIA dGPU
# --vulkan expose Vulkan to the guest (venus; needed by gamescope, can be unstable)
# --amd with --vulkan: venus on the host AMD iGPU (RADV) instead of NVIDIA
# --fremont report the Valve Steam Machine's DMI data (for testing the wizard)
# --headless no window (unattended tests, CI); a VM you start by hand has one. VM_HEADLESS=1 does the same
# REPO defaults to $HOME/projects/cachyos-gamescope-boot.
# VM_ISO=<path> boots that ISO for `install` instead of cachyos.iso; with
# VM_KERNEL/VM_INITRD/VM_APPEND its kernel is booted directly with those
# parameters (scripts/vminstall.sh, the unattended install).
# VM_MEM=<size>: guest RAM (8G; the automated tests use 4G so three VMs fit next to a desktop).
# VM_CPUS=<n>: guest CPUs (6); ISO builds use ~75% of host cores (nproc * 3 / 4), so the host stays usable.
# VM_PORT=<n>: the host port for the guest's SSH (2222), e.g. a second VM.
# BIOS_VERSION=F7F0107 makes the guest report that BIOS version (DMI), e.g.
# to test the wizard's BIOS update item; the firmware itself doesn't change.
# The repo is shared into the guest; mount it there with:
@@ -14,39 +22,66 @@
# sudo mount -t 9p -o trans=virtio,version=9p2000.L vmtools /media && /media/guest-ssh-setup.sh
# SSH from the host: ssh -p 2222 <user>@localhost
cd "$(dirname "$0")"
# REPO: from the environment, else what scripts/vminstall.sh recorded next
# to the disk (repo-path), else the old default.
[[ -z "${REPO:-}" && -f repo-path ]] && REPO="$(cat repo-path)"
repo="${REPO:-$HOME/projects/cachyos-gamescope-boot}"
cdrom=()
direct=()
[[ -n "${VM_KERNEL:-}" ]] && direct=(-kernel "$VM_KERNEL" -initrd "$VM_INITRD" -append "$VM_APPEND")
# VM_CACHE=<host dir>: shared read-write as 9p tag `cache` (the ISO build's
# package cache, scripts/vmisobuild.sh; one per VM, e.g. $VM_DIR/iso-cache),
# so snapshot resets don't lose it.
[[ -n "${VM_CACHE:-}" ]] && { mkdir -p "$VM_CACHE"; direct+=(-virtfs "local,path=$VM_CACHE,mount_tag=cache,security_model=mapped-xattr"); }
# VM_SERIAL=<file>: the guest's serial console (console=ttyS0) logged into
# that file, and reachable as a socket next to it (<file>.sock) to type into.
[[ -n "${VM_SERIAL:-}" ]] && direct+=(-chardev "socket,id=ser0,path=$VM_SERIAL.sock,server=on,wait=off,logfile=$VM_SERIAL" -serial chardev:ser0)
smbios=()
gpu=virtio-vga-gl,xres=1920,yres=1080
gpu=virtio-vga-gl,xres=1920,yres=1080 gl=on
# VM_NOGL=1: no virgl (software rendering in the guest), so QMP screendump
# works (scripts/qmpshot.py); for the live ISO and Calamares.
[[ -n "${VM_NOGL:-}" ]] && { gpu=virtio-vga,xres=1920,yres=1080; gl=off; }
[[ -n "${BIOS_VERSION:-}" ]] && smbios+=(-smbios "type=0,version=$BIOS_VERSION")
for arg in "$@"; do
case "$arg" in
install) cdrom=(-cdrom cachyos.iso -boot d) ;;
install) cdrom=(-cdrom "${VM_ISO:-cachyos.iso}" -boot d) ;;
--nvidia) export __NV_PRIME_RENDER_OFFLOAD=1 __GLX_VENDOR_LIBRARY_NAME=nvidia \
__EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/10_nvidia.json ;;
--vulkan) gpu+=,hostmem=4G,blob=true,venus=true ;;
--amd) export VK_DRIVER_FILES=/usr/share/vulkan/icd.d/radeon_icd.json ;;
--headless) VM_HEADLESS=1 ;;
--fremont) smbios+=(-smbios type=1,manufacturer=Valve,product=Fremont -smbios type=2,manufacturer=Valve,product=Fremont) ;;
*) echo "unknown argument: $arg" >&2; exit 1 ;;
esac
done
# VM_HEADLESS=1: no window (unattended tests, CI): plain virtio-vga and -display none;
# QMP screendump works then too. The guest and ssh don't notice.
display=(-display "gtk,gl=$gl,zoom-to-fit=off")
[[ -n "${VM_HEADLESS:-}" ]] && { gpu=virtio-vga,xres=1920,yres=1080; display=(-display none); }
# UEFI firmware: Ubuntu's path, or Arch's (edk2-ovmf).
ovmf=/usr/share/OVMF; ovmf_code=OVMF_CODE_4M.fd; ovmf_vars=OVMF_VARS_4M.fd
[[ -f "$ovmf/$ovmf_code" ]] || { ovmf=/usr/share/edk2/x64; ovmf_code=OVMF_CODE.4m.fd; ovmf_vars=OVMF_VARS.4m.fd; }
# First run: create the disk and the writable UEFI variable store.
[[ -f disk.qcow2 ]] || qemu-img create -f qcow2 disk.qcow2 60G
[[ -f vars.fd ]] || cp /usr/share/OVMF/OVMF_VARS_4M.fd vars.fd
[[ -f vars.fd ]] || cp "$ovmf/$ovmf_vars" vars.fd
# Hand the host's public keys to the guest setup script.
cat ~/.ssh/*.pub > share/host-keys.pub
exec qemu-system-x86_64 \
-enable-kvm -machine q35,memory-backend=mem -cpu host -smp 6 -m 8G \
-object memory-backend-memfd,id=mem,size=8G,share=on \
-drive if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE_4M.fd \
-enable-kvm -machine q35,memory-backend=mem -cpu host -smp "${VM_CPUS:-6}" -m "${VM_MEM:-8G}" \
-object memory-backend-memfd,id=mem,size=${VM_MEM:-8G},share=on \
-drive if=pflash,format=raw,readonly=on,file="$ovmf/$ovmf_code" \
-drive if=pflash,format=raw,file=vars.fd \
-drive file=disk.qcow2,if=virtio \
-device "$gpu" -display gtk,gl=on,zoom-to-fit=off \
-device "$gpu" "${display[@]}" \
-device qemu-xhci -device usb-tablet \
-nic user,model=virtio-net-pci,hostfwd=tcp::2222-:22 \
-nic user,model=virtio-net-pci,hostfwd=tcp::${VM_PORT:-2222}-:22 \
-virtfs local,path="$repo",mount_tag=repo,security_model=mapped-xattr \
-virtfs local,path="$PWD/share",mount_tag=vmtools,security_model=mapped-xattr,readonly=on \
"${smbios[@]}" "${cdrom[@]}"
-qmp unix:"$PWD/qmp.sock",server=on,wait=off \
"${smbios[@]}" "${cdrom[@]}" "${direct[@]}"
+28 -2
View File
@@ -1,5 +1,31 @@
# Sourced by the other scripts: SSH target of the test VM.
VM_USER="${VM_USER:-theupriser}"
VM_PORT="${VM_PORT:-2222}"
VM_HOST="${VM_HOST:-localhost}"
vm_ssh() { ssh -p "$VM_PORT" -o BatchMode=yes "$VM_USER@$VM_HOST" "$@"; }
# Where disk.qcow2, vars*.fd and run.sh live: this repo if it has a disk,
# else ~/vms/cachyos-test.
_repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
if [[ -z "${VM_DIR:-}" ]]; then
if [[ -f "$_repo/disk.qcow2" ]]; then VM_DIR="$_repo"; else VM_DIR="$HOME/vms/cachyos-test"; fi
fi
# The guest user: what scripts/vminstall.sh created ($VM_DIR/vm-user), else
# theupriser (VMs installed by hand).
if [[ -z "${VM_USER:-}" && -f "$VM_DIR/vm-user" ]]; then VM_USER="$(cat "$VM_DIR/vm-user")"; fi
VM_USER="${VM_USER:-theupriser}"
# scripts/vminstall.sh records the key it authorized in $VM_DIR/ssh-key.
# The VM's host key lives next to its disk, never in ~/.ssh/known_hosts: a
# reinstalled VM gets a new one on the same localhost port.
VM_SSH_OPTS=(-o BatchMode=yes -o "UserKnownHostsFile=$VM_DIR/known_hosts" -o StrictHostKeyChecking=accept-new)
if [[ -z "${VM_SSH_KEY:-}" && -f "$VM_DIR/ssh-key" ]]; then VM_SSH_KEY="$(cat "$VM_DIR/ssh-key")"; fi
[[ -n "${VM_SSH_KEY:-}" ]] && VM_SSH_OPTS+=(-i "$VM_SSH_KEY" -o IdentitiesOnly=yes)
vm_ssh() { ssh -p "$VM_PORT" "${VM_SSH_OPTS[@]}" "$VM_USER@$VM_HOST" "$@"; }
vm_scp() { scp -q -P "$VM_PORT" "${VM_SSH_OPTS[@]}" "$@"; }
# One Konsole for every test run: it follows $TEST_LOG, which the test scripts append to.
# Started only when there is a desktop, the run is headless and no such window exists.
TEST_LOG="${TEST_LOG:-$HOME/vms/test.log}"
view_start() {
[[ -n "${VM_HEADLESS:-}" && -z "${CI:-}" && -n "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ]] && command -v konsole >/dev/null || return 0
systemctl --user is-active --quiet vmtest-view 2>/dev/null && return 0
mkdir -p "$(dirname "$TEST_LOG")"; touch "$TEST_LOG"
systemd-run --user -q --collect --unit=vmtest-view konsole --hold -e tail -n 60 -F "$TEST_LOG" >/dev/null 2>&1 || true
}
+16
View File
@@ -0,0 +1,16 @@
# qmpclick.py <qmp socket> <screen w> <screen h> <x,y>...: left-clicks at
# those guest-screen pixels (the usb-tablet from run.sh, absolute 0..32767).
import json, socket, sys, time
sock, w, h = sys.argv[1], int(sys.argv[2]), int(sys.argv[3])
s = socket.socket(socket.AF_UNIX); s.connect(sock); f = s.makefile("rw"); f.readline()
def cmd(c):
f.write(json.dumps(c) + "\n"); f.flush(); return f.readline()
cmd({"execute": "qmp_capabilities"})
def ev(*events): cmd({"execute": "input-send-event", "arguments": {"events": list(events)}})
for pos in sys.argv[4:]:
x, y = (int(v) for v in pos.split(","))
ev({"type": "abs", "data": {"axis": "x", "value": x * 32767 // (w - 1)}},
{"type": "abs", "data": {"axis": "y", "value": y * 32767 // (h - 1)}})
time.sleep(0.2)
ev({"type": "btn", "data": {"down": True, "button": "left"}}); time.sleep(0.08)
ev({"type": "btn", "data": {"down": False, "button": "left"}}); time.sleep(0.8)
+8
View File
@@ -0,0 +1,8 @@
# qmpkey.py <qmp socket> <combo>...: presses keys in the VM (QEMU qcodes), e.g. down right ctrl-ret ret esc
import socket, json, sys, time
s = socket.socket(socket.AF_UNIX); s.connect(sys.argv[1]); f = s.makefile("rw"); f.readline()
def cmd(c): f.write(json.dumps(c) + "\n"); f.flush(); return f.readline()
cmd({"execute": "qmp_capabilities"})
for combo in sys.argv[2:]:
cmd({"execute": "send-key", "arguments": {"keys": [{"type": "qcode", "data": k} for k in combo.split("-")]}})
time.sleep(0.4)
+27
View File
@@ -0,0 +1,27 @@
# qmpshot.py <qmp socket> <out.png>: screenshot of the VM's screen over QMP
# (screendump). Needs a VM without virgl (run.sh with VM_NOGL=1): with
# virgl QEMU answers "no surface". Before the guest's desktop is up it shows
# whatever is on screen (boot menu, Calamares, ...).
import json, os, socket, sys, tempfile
sock, out = sys.argv[1], sys.argv[2]
s = socket.socket(socket.AF_UNIX); s.connect(sock); f = s.makefile("rw"); f.readline()
def cmd(c):
f.write(json.dumps(c) + "\n"); f.flush()
while True:
r = json.loads(f.readline())
if "event" not in r:
return r
cmd({"execute": "qmp_capabilities"})
ppm = tempfile.mktemp(suffix=".ppm")
r = cmd({"execute": "screendump", "arguments": {"filename": ppm, "format": "ppm"}})
if "error" in r:
raise SystemExit("screendump: %s" % r["error"]["desc"])
try:
from PIL import Image
Image.open(ppm).save(out)
os.remove(ppm)
except ImportError:
os.replace(ppm, os.path.splitext(out)[0] + ".ppm")
print("PIL missing: wrote a .ppm instead", file=sys.stderr)
print(out)
+23
View File
@@ -0,0 +1,23 @@
# qmptype.py <qmp socket> <text>: types <text> (US layout) into the VM, then Enter.
import socket, json, sys, time
sock, text = sys.argv[1], sys.argv[2]
s = socket.socket(socket.AF_UNIX); s.connect(sock); f = s.makefile("rw"); f.readline()
def cmd(c):
f.write(json.dumps(c) + "\n"); f.flush(); return f.readline()
cmd({"execute": "qmp_capabilities"})
plain = {" ": "spc", "/": "slash", ".": "dot", "-": "minus", "'": "apostrophe", "\n": "ret",
"=": "equal", ",": "comma", ";": "semicolon", "\\": "backslash", "[": "bracket_left", "]": "bracket_right"}
shifted = {"&": "7", "*": "8", "_": "minus", ":": "semicolon", "|": "backslash", "~": "grave_accent",
"!": "1", "@": "2", "#": "3", "$": "4", "%": "5", "^": "6", "(": "9", ")": "0", "+": "equal", '"': "apostrophe",
">": "dot", "<": "comma", "?": "slash", "{": "bracket_left", "}": "bracket_right"}
for ch in text + "\n":
keys = []
if ch.isalpha():
if ch.isupper(): keys.append("shift")
keys.append(ch.lower())
elif ch.isdigit(): keys.append(ch)
elif ch in plain: keys.append(plain[ch])
elif ch in shifted: keys += ["shift", shifted[ch]]
else: raise SystemExit("no key for %r" % ch)
cmd({"execute": "send-key", "arguments": {"keys": [{"type": "qcode", "data": k} for k in keys]}})
time.sleep(0.03)
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
# Authorize the test VM's SSH key (made by vminstall.sh) on the Steam Machine
# too, so the same key reaches both. Asks the Steam Machine user's password
# once (ssh-copy-id); adds the key only if it isn't there yet.
# scripts/steammachine-addkey.sh [user@host] default: steammachine (~/.ssh/config)
# Env: VM_SSH_KEY (default ~/.ssh/steamify-vm_ed25519).
set -euo pipefail
key="${VM_SSH_KEY:-$HOME/.ssh/steamify-vm_ed25519}"
target="${1:-steammachine}"
[[ -f "$key.pub" ]] || { echo "No $key.pub: run scripts/vminstall.sh first (it makes the key)." >&2; exit 1; }
ssh-copy-id -i "$key.pub" "$target"
ssh -i "$key" -o IdentitiesOnly=yes -o BatchMode=yes "$target" true &&
echo "OK: ssh -i $key $target works without a password."
+121
View File
@@ -0,0 +1,121 @@
#!/bin/bash
# Does a boot loader work with Steamify's kernel side (the Steam Machine's DKMS
# modules)? On a VM that reports Fremont hardware: applies Steamify's options,
# checks the modules for every kernel, Steamify's boot-loader code (removal of
# the old drm.edid_firmware parameter), a kernel update, and a reboot into
# each of the two kernels. Guest checks: share/bootloader-test/*.sh.
# All loaders in one go: scripts/vmtest.sh.
# scripts/vmbootloadertest.sh <limine|systemd-boot|grub> [--install] [--window]
# --install first install the VM (scripts/vminstall.sh, from the Steamify ISO)
# --window show the VM's window (default: headless, with a Konsole on the log when there is a desktop)
# Env: VM_DIR (default ~/vms/bl-<loader>), VM_ISO (default: the newest ISO in
# ../steamify-cachyos-live-iso/out/desktop), REPO (steamify-cachyos, 9p `repo`).
# Exit status: the number of failed checks. Only one VM runs at a time (port 2222).
set -uo pipefail
here="$(cd "$(dirname "$0")" && pwd)"
repo="$(cd "$here/.." && pwd)"
loader="${1:?usage: $0 <limine|systemd-boot|grub> [--install]}"
install=false; window=false
for a in "${@:2}"; do
case "$a" in --install) install=true ;; --window) window=true ;; *) echo "Unknown argument: $a" >&2; exit 2 ;; esac
done
# No human is needed: headless (no VM window) unless --window. On a desktop a Konsole follows the log.
$window || export VM_HEADLESS=1
case "$loader" in limine|systemd-boot|grub) ;; *) echo "Unknown boot loader: $loader" >&2; exit 2 ;; esac
export VM_DIR="${VM_DIR:-$HOME/vms/bl-$loader}"
# Own ssh port per loader, so the loaders can run side by side (vmtest.sh).
case "$loader" in limine) export VM_PORT="${VM_PORT:-2401}" ;; systemd-boot) export VM_PORT="${VM_PORT:-2402}" ;; grub) export VM_PORT="${VM_PORT:-2403}" ;; esac
. "$here/common.sh"
guest="$repo/share/bootloader-test"
log="$VM_DIR.test.log"
step() { printf '\n##### %s %s\n' "$(date +%T)" "$*"; }
gssh() { vm_ssh -o ConnectTimeout=6 -o LogLevel=ERROR "$@"; }
gscript() { gssh env "LOADER=$loader" bash -s < "$guest/$1"; } # the guest's login shell is fish
waitssh() { local _; for _ in $(seq 60); do gssh 'uptime -p' 2>/dev/null | grep -q '^up' && return 0; sleep 5; done; echo "FAIL no SSH"; return 1; }
running() { pgrep -f "[h]ostfwd=tcp::$VM_PORT-" >/dev/null; }
stop_vm() {
running || return 0
# Whichever VM runs: its key and host key are not this VM's.
ssh -p "$VM_PORT" -i "$VM_SSH_KEY" -o BatchMode=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o LogLevel=ERROR \
"$VM_USER@$VM_HOST" 'sudo systemctl poweroff' >/dev/null 2>&1
local _; for _ in $(seq 30); do running || return 0; sleep 2; done
pkill -f "[h]ostfwd=tcp::$VM_PORT-"; sleep 2
}
export VM_CACHE="${VM_CACHE-$HOME/vms/pkg-cache}" # 9p tag `cache`: Steamify's downloads once for every VM
start_vm() { stop_vm; cp "$repo/run.sh" "$VM_DIR/run.sh"; (cd "$VM_DIR" && VM_MEM="${VM_MEM:-4G}" REPO="${REPO:-$repo/../steamify-cachyos}" setsid nohup ./run.sh --fremont > vm.log 2>&1 &); }
reboot_vm() { gssh 'sudo systemctl reboot' >/dev/null 2>&1; sleep 25; waitssh; }
view_start # one shared Konsole (common.sh)
# The guest checks print PASS/FAIL lines; the summary at the end counts them in the log.
check() { cat; }
{
printf '\n===== vmbootloadertest.sh %s (%s) =====\n' "$loader" "$(date +%T)"
if $install; then
step "install $loader VM in $VM_DIR"
stop_vm
iso="${VM_ISO:-$(ls -t "$repo"/../steamify-cachyos-live-iso/out/desktop/*.iso 2>/dev/null | head -n 1)}"
[[ -f "$iso" ]] || { echo "No ISO: set VM_ISO"; exit 2; }
if [[ -f "$VM_DIR/disk.qcow2" ]]; then
[[ -t 0 ]] || { echo "$VM_DIR has a disk; run this from a terminal to replace it"; exit 2; }
read -r -p "Replace $VM_DIR/disk.qcow2? Type YES: " a
[[ "$a" == YES ]] || exit 1
rm -f "$VM_DIR/disk.qcow2"
fi
VM_BOOTLOADER="$loader" "$here/vminstall.sh" --iso "$iso" < /dev/null || exit 1
fi
step "start $loader as a Steam Machine"
start_vm; waitssh || exit 1
gssh 'sudo cat /sys/class/dmi/id/product_name' 2>/dev/null
step "Steamify's Steam Machine options"
gssh 'sudo mkdir -p /var/cache/steamify-pkg /var/cache/steamify; sudo mountpoint -q /var/cache/steamify-pkg || sudo mount -t 9p -o trans=virtio,version=9p2000.L cache /var/cache/steamify-pkg && { sudo mkdir -p /var/cache/steamify-pkg/steamify; sudo mount --bind /var/cache/steamify-pkg/steamify /var/cache/steamify; }' 2>/dev/null
gssh 'sudo mountpoint -q /mnt || sudo mount -t 9p -o trans=virtio,version=9p2000.L repo /mnt; cd /mnt && nohup bash steamify.sh --defaults --options gaming,theme,glyphs,single,launcher,notify,vram,cec,machine,poweroff --boot gamescope > /tmp/steamify.log 2>&1 < /dev/null &'
sleep 20
# [s]: the pattern must not match this ssh command itself.
for _ in $(seq 90); do gssh 'pgrep -f "[s]teamify.sh --defaults" >/dev/null && echo R || echo D' 2>/dev/null | grep -q D && break; sleep 10; done
gssh 'sed "s/\x1b\[[0-9;]*m//g" /tmp/steamify.log | grep -E "^\[(WARN|ERROR)\]" | cut -c1-140'
gssh 'tail -n 1 /tmp/steamify.log | sed "s/\x1b\[[0-9;]*m//g"' | grep -q '^\[OK\] Done' && echo "PASS Steamify finished" || { echo "FAIL Steamify did not finish"; }
step "Steamify's state, OS name, boot loader"; gscript steamify-state.sh | check
step "Steamify's modules"; gscript modules.sh | check
step "boot loader code (legacy kernel parameter removal)"; gscript legacy-param.sh | check
step "kernel update"; gscript kernel-update.sh | check
step "reboot into the default kernel"
if [[ "$loader" == limine ]]; then
# remember_last_entry: yes boots whatever ran last, not default_entry: pin the default for this test, restore it at the end.
cur="$(gssh "sudo sed -n 's/^default_entry: *//p' /boot/limine.conf")"
rem="$(gssh "sudo sed -n 's/^remember_last_entry: *//p' /boot/limine.conf")"
gssh "sudo sed -i -e 's/^default_entry: .*/default_entry: $cur/' -e 's/^remember_last_entry: .*/remember_last_entry: no/' /boot/limine.conf"
fi
reboot_vm && gscript boot-check.sh | check
a="$(gssh uname -r)"
step "reboot into the other kernel"
case "$loader" in
systemd-boot) # the default entry is linux-cachyos.conf (vminstall-post.sh)
gssh 'sudo bootctl set-oneshot linux-cachyos-lts.conf'; gssh 'sudo systemctl reboot' >/dev/null 2>&1; sleep 25; waitssh ;;
grub) # GRUB_DEFAULT = the other kernel in the "Advanced options" submenu (0 = lts, 1 = linux-cachyos), no key presses
sub=$([[ "$a" == *lts* ]] && echo 1 || echo 0)
gg="$(gssh "sudo sed -n 's/^GRUB_DEFAULT=//p' /etc/default/grub")"
gssh "sudo sed -i 's/^GRUB_DEFAULT=.*/GRUB_DEFAULT=\"1>$sub\"/' /etc/default/grub && sudo grub-mkconfig -o /boot/grub/grub.cfg >/dev/null 2>&1"
reboot_vm ;;
limine) # the next entry in limine.conf; remember_last_entry would override default_entry
gssh "sudo sed -i -e 's/^default_entry: .*/default_entry: $((cur + 1))/' /boot/limine.conf"
reboot_vm ;;
esac
gscript boot-check.sh | check
b="$(gssh uname -r)"
[[ -n "$a" && -n "$b" && "$a" != "$b" ]] && echo "PASS booted both kernels ($a, $b)" || echo "FAIL both boots ran the same kernel ($a, $b)"
[[ "$loader" == grub ]] && gssh "sudo sed -i 's/^GRUB_DEFAULT=.*/GRUB_DEFAULT=$gg/' /etc/default/grub && sudo grub-mkconfig -o /boot/grub/grub.cfg >/dev/null 2>&1"
[[ "$loader" == limine ]] && gssh "sudo sed -i -e 's/^default_entry: .*/default_entry: $cur/' -e 's/^remember_last_entry: .*/remember_last_entry: ${rem:-yes}/' /boot/limine.conf"
step "DONE"
stop_vm
} 2>&1 | tee "$log" | tee >(sed -u "s/^/[$loader] /" >> "$TEST_LOG") | sed 's/\x1b\[[0-9;]*m//g'
fails="$(grep -c '^FAIL' "$log")"
echo; echo "== $loader: $(grep -c '^PASS' "$log") passed, $fails failed (log: $log)"
grep '^FAIL' "$log"
exit "$fails"
+230
View File
@@ -0,0 +1,230 @@
#!/bin/bash
# Test HDMI-CEC in the guest against a fake TV: the kernel's vivid driver
# gives a virtual HDMI input (/dev/cec0, plays the TV, driven with cec-ctl)
# and output (/dev/cec1, the PC, where cecd runs). Needs the wizard's HDMI-CEC
# item on (cecd, cec-ctl from v4l-utils); steamos-manager (Steam Machine
# support) for the Steam settings tests. Prints PASS/FAIL/SKIP per check.
#
# scripts/vmcec.sh everything, including a real suspend/resume
# (needs the QMP socket from run.sh to wake up)
# scripts/vmcec.sh --no-sleep everything except suspending the VM
# scripts/vmcec.sh --sleep-only only the real sleep/wake check (after a full run)
#
# Afterwards the normal cecd service runs again; vivid stays loaded.
# Env: VM_USER (theupriser), VM_PORT (2222), VM_HOST (localhost).
set -euo pipefail
. "$(dirname "$0")/common.sh"
SLEEP_TEST=true SLEEP_ONLY=false
[[ "${1:-}" == --no-sleep ]] && SLEEP_TEST=false
[[ "${1:-}" == --sleep-only ]] && SLEEP_ONLY=true
# --- in the guest: helpers, setup and all checks except the real sleep ---
$SLEEP_ONLY || vm_ssh "SLEEP_TEST=$SLEEP_TEST bash -s" << 'REMOTE'
export XDG_RUNTIME_DIR=/run/user/1000 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
W=~/.cache/vmcec; mkdir -p "$W"
pass=0 fail=0 skip=0
PASS() { echo " PASS $*"; pass=$((pass + 1)); }
FAIL() { echo " FAIL $*"; fail=$((fail + 1)); }
SKIP() { echo " SKIP $*"; skip=$((skip + 1)); }
check() { local d="$1"; shift; if "$@" >/dev/null 2>&1; then PASS "$d"; else FAIL "$d"; fi; }
TV() { cec-ctl -d /dev/cec0 "$@"; }
BUS=com.steampowered.CecDaemon1
has_smgr() { steamosctl get-hdmi-cec-state >/dev/null 2>&1; }
# Key reader for the cecd input device: prints key names while it runs.
cat > "$W/keys.py" << 'PY'
import re, select, struct, sys, time
names = {}
for line in open('/usr/include/linux/input-event-codes.h'):
m = re.match(r'#define\s+(KEY_\w+|BTN_\w+)\s+(0x[0-9a-fA-F]+|\d+)\b', line)
if m: names.setdefault(int(m.group(2), 0), m.group(1))
f = open(sys.argv[1], 'rb'); end = time.time() + float(sys.argv[2])
while time.time() < end:
if select.select([f], [], [], 0.1)[0]:
_, _, t, c, v = struct.unpack('llHHi', f.read(24))
if t == 1 and v == 1: print(names.get(c, c), flush=True)
PY
# Monitor the TV side's CEC bus into $W/mon.txt (needs root).
mon_start() { sudo pkill -f 'cec-ctl -d /dev/cec0 -M' 2>/dev/null; : > "$W/mon.txt"
sudo sh -c "exec cec-ctl -d /dev/cec0 -M > $W/mon.txt 2>&1" & sleep 1; }
mon_stop() { sleep 1; sudo pkill -f 'cec-ctl -d /dev/cec0 -M' 2>/dev/null; wait 2>/dev/null; }
seen() { grep -q "Received from.*): $1" "$W/mon.txt"; }
echo "== Setup: fake TV (vivid)"
lsmod | grep -q '^vivid' || sudo modprobe vivid n_devs=1 num_inputs=1 input_types=0x3 num_outputs=1 output_types=0x1
sleep 2
TV --tv --osd-name "Fake TV" >/dev/null 2>&1
v4l2-ctl -d /dev/video0 --set-ctrl hdmi_000_0_is_connected_to=2
# The cecd service (-e) takes every CEC device, the TV too: a temporary
# drop-in limits it to the PC side, so steamos-manager still controls it.
mkdir -p ~/.config/systemd/user/cecd.service.d
printf '[Service]\nExecStart=\nExecStart=/usr/bin/cecd -d /dev/cec1\n' > ~/.config/systemd/user/cecd.service.d/vmcec.conf
systemctl --user daemon-reload; systemctl --user restart cecd.service
sleep 4
systemctl --user is-active -q steamos-manager.service && { systemctl --user restart steamos-manager.service; sleep 3; }
LA=$(cec-ctl -d /dev/cec1 | awk '/Logical Address /{print $4; exit}')
PA=$(cec-ctl -d /dev/cec1 | awk '/Physical Address/{print $4; exit}')
EV=$(grep -A4 'N: Name="cecd' /proc/bus/input/devices | grep -oE 'event[0-9]+' | head -1)
echo " PC side: physical $PA, logical $LA, input /dev/input/${EV:-none}"
echo "== 1. Identity (TV asks the PC)"
[[ "$PA" == 1.0.0.0 ]] && PASS "physical address 1.0.0.0" || FAIL "physical address is $PA"
[[ "$LA" =~ ^(4|8|9|11)$ ]] && PASS "logical address $LA (playback device)" || FAIL "logical address is '$LA'"
name=$(TV -t "$LA" --give-osd-name 2>&1 | awk -F': ' '/name:/{print $2; exit}')
[[ "$name" == "Steam Machine" ]] && PASS "OSD name 'Steam Machine'" || FAIL "OSD name is '$name' (steamos-manager's config not applied?)"
vendor=$(TV -t "$LA" --give-device-vendor-id 2>&1 | awk '/vendor-id:/{print $2; exit}')
[[ "$vendor" == 0xe0319e* ]] && PASS "vendor ID Valve (0xe0319e)" || FAIL "vendor ID is '$vendor'"
check "answers a poll" TV -t "$LA" --poll
echo "== 2. TV remote -> PC (key presses)"
press() { TV -t "$LA" --user-control-pressed ui-cmd="$1" >/dev/null; sleep 0.15; TV -t "$LA" --user-control-released >/dev/null; sleep 0.25; }
keys_for() { # keys_for <seconds> <ui-cmd>...: keys arriving while pressing
EV=$(grep -A4 'N: Name="cecd' /proc/bus/input/devices | grep -oE 'event[0-9]+' | head -1)
: > "$W/keys.txt"; local r=
[[ -n "$EV" ]] && { sudo timeout "$1" python3 "$W/keys.py" "/dev/input/$EV" "$1" > "$W/keys.txt" & r=$!; }
sleep 1; shift
local k; for k in "$@"; do press "$k"; done; [[ -n "$r" ]] && wait "$r" 2>/dev/null; cat "$W/keys.txt"; }
if [[ -z "$EV" ]]; then FAIL "no cecd input device"
else
for k in up down left right select exit back root-menu setup-menu contents-menu \
play pause stop rewind fast-forward forward backward \
volume-up volume-down mute number-entry-mode number-0-or-number-10 number-5 \
channel-up channel-down display-information electronic-program-guide \
f1-blue f2-red f3-green f4-yellow; do
got=$(keys_for 2 "$k" | tr '\n' ' ')
if [[ -n "$got" ]]; then PASS "$(printf '%-28s -> %s' "$k" "$got")"
else SKIP "$(printf '%-28s -> nothing (not mapped by cecd)' "$k")"; fi
done
# The TV's power button arrives as KEY_POWER, and Steam Machine support
# makes the power button sleep: with sleep blocked, check the key only.
sudo systemd-inhibit --what=sleep:handle-power-key --mode=block --who=vmcec --why=test sleep 6 & inh=$!; sleep 1
got=$(keys_for 2 power-toggle-function | tr '\n' ' ')
wait $inh 2>/dev/null
[[ "$got" == *KEY_POWER* ]] && PASS "$(printf '%-28s -> %s' power-toggle-function "$got") (sleeps the PC)" ||
FAIL "power-toggle-function -> '$got'"
fi
echo "== 3. Steam settings (steamos-manager, over D-Bus like Steam)"
# steamosctl's suspend-tv/suspend-device getters and setters don't work
# (steamos-manager 26.4.1); Steam uses these D-Bus properties.
SM=com.steampowered.SteamOSManager1; SMP=/com/steampowered/SteamOSManager1; CEC2=$SM.HdmiCec2
prop() { busctl --user get-property $SM $SMP $CEC2 "$1" 2>/dev/null | awk '{print $2}'; }
setp() { busctl --user set-property $SM $SMP $CEC2 "$1" b "$2" 2>/dev/null; sleep 2; }
cfg() { grep -h "^$1 *=" ~/.config/cecd/config.d/*.toml 2>/dev/null | tail -1 | awk -F'= *' '{print $2}'; }
if ! has_smgr; then SKIP "steamos-manager has no HDMI-CEC interface (no Steam Machine support?)"
else
echo " now: $(steamosctl get-hdmi-cec-state 2>&1); EnableControl=$(prop EnableControl) WakeTv=$(prop WakeTv) SuspendTv=$(prop SuspendTv) SuspendDevice=$(prop SuspendDevice) WakeDeviceSupported=$(prop WakeDeviceSupported)"
setp EnableControl false
got=$(keys_for 2 down | tr '\n' ' ')
[[ -z "$got" && "$(cfg uinput)" == false ]] && PASS "remote control off: keys are ignored (uinput = false)" || FAIL "remote control off: keys '$got', uinput = $(cfg uinput)"
setp EnableControl true
got=$(keys_for 2 down | tr '\n' ' ')
[[ -n "$got" ]] && PASS "remote control on again: $got" || FAIL "remote control on, but no keys"
for pr in WakeTv:wake_tv SuspendTv:suspend_tv SuspendDevice:allow_standby; do
for v in false true; do
setp "${pr%%:*}" $v
[[ "$(prop "${pr%%:*}")" == "$v" && "$(cfg "${pr#*:}")" == "$v" ]] &&
PASS "${pr%%:*} $v (cecd config ${pr#*:} = $v)" || FAIL "${pr%%:*} $v: property $(prop "${pr%%:*}"), config ${pr#*:} = $(cfg "${pr#*:}")"
done
done
[[ "$(prop WakeDeviceSupported)" == true ]] && PASS "waking the PC with the TV is supported" ||
SKIP "waking the PC with the TV: not supported here (WakeDeviceSupported = false)"
steamosctl set-hdmi-cec-state control-and-wake >/dev/null 2>&1; sleep 3
fi
systemctl --user is-active -q cecd.service && PASS "cecd still running" || FAIL "cecd stopped"
echo "== 4. PC -> TV"
# A plain TV (no soundbar): volume goes to the TV itself (address 0).
TV --tv --osd-name "Fake TV" >/dev/null 2>&1; sleep 2
echo " fake TV: $(TV | awk '/Logical Address /{printf "%s%s ", $4, $5}')"
DEV=$(busctl --user tree $BUS 2>/dev/null | grep -oE '/com/steampowered/CecDaemon1/Devices/Cec[0-9]+' | head -1)
mon_start
if has_smgr; then steamosctl hdmi-cec-make-active >/dev/null 2>&1; sleep 1; else busctl --user call $BUS "$DEV" $BUS.CecDevice1 SetActiveSource >/dev/null 2>&1; sleep 1; fi
busctl --user call $BUS /com/steampowered/CecDaemon1/Daemon $BUS.Daemon1 Wake >/dev/null 2>&1; sleep 1
mon_stop
{ seen ACTIVE_SOURCE || seen IMAGE_VIEW_ON || seen TEXT_VIEW_ON; } && PASS "make active: TV switches to the PC ($(grep -oE '\): [A-Z_]+' "$W/mon.txt" | sort -u | tr -d '):' | tr '\n' ' '))" || FAIL "make active: TV got nothing"
seen IMAGE_VIEW_ON || seen TEXT_VIEW_ON && PASS "wake: TV is turned on (IMAGE_VIEW_ON)" || FAIL "wake: no IMAGE_VIEW_ON"
mon_start
for m in VolumeUp VolumeDown Mute; do busctl --user call $BUS "$DEV" $BUS.CecDevice1 $m y 0 >/dev/null 2>&1; sleep 0.5; done
echo " audio status: $(busctl --user --timeout=3 call $BUS "$DEV" $BUS.CecDevice1 GetAudioStatus y 0 2>&1)"; sleep 1
mon_stop
n=$(grep -cE 'USER_CONTROL_PRESSED' "$W/mon.txt"); ops=$(grep -A1 USER_CONTROL_PRESSED "$W/mon.txt" | grep -oiE 'volume-up|volume-down|mute' | sort -u | tr '\n' ' ')
[[ $n -ge 3 ]] && PASS "volume up/down/mute sent to the TV ($ops)" || FAIL "volume: $n key presses reached the TV ($ops)"
seen GIVE_AUDIO_STATUS && PASS "asks the TV's audio status" || FAIL "no GIVE_AUDIO_STATUS"
mon_start
busctl --user call $BUS /com/steampowered/CecDaemon1/Daemon $BUS.Daemon1 Standby b true >/dev/null 2>&1; sleep 1
mon_stop
seen STANDBY && PASS "standby: TV is turned off (STANDBY)" || FAIL "standby: no STANDBY"
systemctl --user is-active -q cec-audio-control.socket && PASS "cec-audio-control socket listening" || FAIL "cec-audio-control socket not active"
echo "== 5. TV off -> PC to sleep (sleep blocked, so the VM stays up)"
if ! has_smgr; then SKIP "needs steamos-manager"
else
TV --tv >/dev/null 2>&1; sleep 2
for want in true false; do
setp SuspendDevice $want; sleep 1
since=$(date '+%Y-%m-%d %H:%M:%S')
sudo systemd-inhibit --what=sleep --mode=block --who=vmcec --why=test sleep 7 & inh=$!; sleep 1
TV -t 15 --standby >/dev/null 2>&1; sleep 4
wait $inh 2>/dev/null
tried=$(journalctl --user -u cecd --since "$since" --no-pager | grep -c 'Failed to standby')
if [[ $want == true ]]; then
[[ $tried -gt 0 ]] && PASS "SuspendDevice on: TV standby makes the PC go to sleep (blocked here)" || FAIL "SuspendDevice on, but no sleep attempt"
else
[[ $tried -eq 0 ]] && PASS "SuspendDevice off: TV standby doesn't touch the PC" || FAIL "SuspendDevice off, but the PC tried to sleep"
fi
done
setp SuspendDevice true
fi
if ! $SLEEP_TEST; then rm -f ~/.config/systemd/user/cecd.service.d/vmcec.conf; systemctl --user daemon-reload; systemctl --user restart cecd.service; fi
echo "== Result: $pass passed, $fail failed, $skip skipped (before the sleep test)"
echo "$LA" > "$W/la"
REMOTE
$SLEEP_TEST || { echo "(sleep test skipped)"; exit 0; }
# --- real suspend/resume: the TV should go off, and on again at wake ---
# Through logind (systemctl suspend), like Steam and the power button: cecd
# acts on logind's PrepareForSleep, which rtcwake skips. cec-follower plays a
# TV that answers power status; QMP (vmwake.sh) wakes the VM.
echo "== 6. Real sleep and wake (systemctl suspend, woken over QMP)"
vm_ssh bash -s << 'REMOTE'
export XDG_RUNTIME_DIR=/run/user/1000 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
W=~/.cache/vmcec
# cecd on the PC side only, with debug logging (it logs what it does at sleep).
mkdir -p ~/.config/systemd/user/cecd.service.d
printf '[Service]\nExecStart=\nExecStart=/usr/bin/cecd -d /dev/cec1\nEnvironment=RUST_LOG=debug\n' > ~/.config/systemd/user/cecd.service.d/vmcec.conf
systemctl --user daemon-reload; systemctl --user restart cecd.service; sleep 4
SM=com.steampowered.SteamOSManager1; SMP=/com/steampowered/SteamOSManager1
busctl --user set-property $SM $SMP $SM.HdmiCec2 SuspendTv b true 2>/dev/null
busctl --user set-property $SM $SMP $SM.HdmiCec2 WakeTv b true 2>/dev/null
sudo systemctl stop vmcec-tv vmcec-mon 2>/dev/null; sudo systemctl reset-failed vmcec-tv vmcec-mon vmcec-sleep 2>/dev/null
cec-ctl -d /dev/cec0 --tv >/dev/null; sleep 1
sudo systemd-run -q --unit=vmcec-tv sh -c 'exec cec-follower -d /dev/cec0 > /dev/null 2>&1'; sleep 2
steamosctl hdmi-cec-make-active >/dev/null 2>&1; sleep 3
sudo systemd-run -q --unit=vmcec-mon sh -c "exec cec-ctl -d /dev/cec0 -M > $W/sleep-mon.txt 2>&1"; sleep 1
sudo systemd-run -q --unit=vmcec-sleep sh -c 'sleep 2; systemctl suspend'
echo " suspending..."
REMOTE
sleep 15
"$(dirname "$0")/vmwake.sh" || true
until vm_ssh -o ConnectTimeout=4 true 2>/dev/null; do sleep 3; done
sleep 8
vm_ssh bash -s << 'REMOTE'
export XDG_RUNTIME_DIR=/run/user/1000 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
W=~/.cache/vmcec
sudo systemctl stop vmcec-mon vmcec-tv 2>/dev/null
t=$(journalctl -b --no-pager -o short-iso | grep "PM: suspend entry" | tail -1 | awk '{print $1}')
[[ -n "$t" ]] && echo " PASS the VM really slept and woke (suspend at $t)" || echo " FAIL no suspend in the kernel log"
log=$(journalctl --user -u cecd --since "$(date -d "${t:-now} -10 sec" '+%F %T')" --no-pager)
grep -q 'Putting TV in standby' <<< "$log" && echo " PASS cecd: 'Putting TV in standby'" || echo " FAIL cecd didn't act on going to sleep"
grep -q 'Waking TV' <<< "$log" && echo " PASS cecd: 'Woke from standby. Waking TV.'" || echo " FAIL cecd didn't act on waking up"
grep -q 'Received from.*: STANDBY' "$W/sleep-mon.txt" && echo " PASS going to sleep: TV turned off (STANDBY)" || echo " FAIL going to sleep: no STANDBY to the TV"
grep -qE 'Received from.*: (IMAGE_VIEW_ON|TEXT_VIEW_ON)' "$W/sleep-mon.txt" && echo " PASS waking up: TV turned on (IMAGE_VIEW_ON)" || echo " FAIL waking up: no IMAGE_VIEW_ON"
grep -oE '^(Received|Transmitted)[^:]*: [A-Z_]+' "$W/sleep-mon.txt" | sed 's/^/ /'
# Back to the normal service.
rm -f ~/.config/systemd/user/cecd.service.d/vmcec.conf; systemctl --user daemon-reload; systemctl --user restart cecd.service
REMOTE
+28
View File
@@ -0,0 +1,28 @@
# vminstall-server.py <dir> <port>: serves <dir> to the installing VM (the
# guest reaches the host as 10.0.2.2) and stores what it PUTs there (the
# install log, the final status). Only on 127.0.0.1. Used by vminstall.sh.
import http.server, os, sys
root, port = sys.argv[1], int(sys.argv[2])
os.chdir(root)
class Handler(http.server.SimpleHTTPRequestHandler):
def do_PUT(self):
name = os.path.basename(self.path)
if not name:
self.send_error(400)
return
data = self.rfile.read(int(self.headers.get("Content-Length", 0)))
with open(name, "wb") as f:
f.write(data)
self.send_response(204)
self.end_headers()
def log_message(self, fmt, *args):
# requests.log: shows the live system reached the host, and when.
with open("requests.log", "a") as f:
f.write("%s %s\n" % (self.log_date_time_string(), fmt % args))
http.server.ThreadingHTTPServer(("127.0.0.1", port), Handler).serve_forever()
+208
View File
@@ -0,0 +1,208 @@
#!/bin/bash
# Install a fresh, ssh-ready test VM unattended: CachyOS (KDE Plasma,
# plasma-login-manager, btrfs, Limine, linux-cachyos + -lts) from the ISO's
# headless installer, then sshd with your key, passwordless sudo and the test
# autologin into Plasma. Ends with the snapshots `clean` and `ssh-ready`.
# scripts/vminstall.sh [--force] [--iso <file>] [run.sh flags...] e.g. --fremont
# --force replace an existing disk.qcow2 in $VM_DIR (asks first)
# --iso <file> the live ISO (default $VM_DIR/cachyos.iso, e.g. a
# Steamify CachyOS build later; it must be archiso-based and
# have cachyos-installer)
# Env: VM_DIR (see common.sh), VM_USER (default: your host username; theupriser when that is root),
# VM_PASSWORD (steamify), VM_SSH_KEY (private key to authorize; default
# ~/.ssh/steamify-vm_ed25519, asked once when that doesn't exist), REPO (shared as 9p `repo`, default ../steamify-cachyos),
# VM_CACHE (host dir for the packages, default ~/vms/pkg-cache; empty = none),
# VM_STEAMIFY (a Steamify ISO also runs steamify-install: the Steamify page's ids,
# empty = defaults, skip = plain CachyOS), VM_BOOTLOADER (limine; or systemd-boot, grub), VM_TIMEZONE (the host's), VM_HOST_IP (the host as the guest sees it,
# 10.0.2.2 with QEMU's user networking).
# The VM's user and key are recorded in $VM_DIR (vm-user, ssh-key) for the
# other scripts. Never touches your existing SSH keys or ~/.ssh/known_hosts.
set -euo pipefail
here="$(cd "$(dirname "$0")" && pwd)"
repo="$(cd "$here/.." && pwd)"
[[ -n "${VM_USER:-}" ]] || VM_USER="$(id -un)"
[[ "$VM_USER" != root ]] || VM_USER=theupriser # root can't be the guest user (WSL runs as root)
export VM_USER
. "$here/common.sh"
# The pacman package cache, on the host and shared by every VM you install
# (9p tag `cache`, see run.sh): a second install downloads nothing. VM_CACHE= (empty) turns it off.
export VM_CACHE="${VM_CACHE-$HOME/vms/pkg-cache}"
force=false iso="" run_flags=()
while [[ $# -gt 0 ]]; do
case "$1" in
--force) force=true; shift ;;
--iso) iso="${2:?--iso needs a file}"; shift 2 ;;
*) run_flags+=("$1"); shift ;;
esac
done
password="${VM_PASSWORD:-steamify}"
timezone="${VM_TIMEZONE:-$(timedatectl show -p Timezone --value 2>/dev/null || cat /etc/timezone 2>/dev/null || echo UTC)}"
hostname=steamify-vm
REPO="${REPO:-$(dirname "$repo")/steamify-cachyos}"
[[ -d "$REPO" ]] || { echo "REPO $REPO doesn't exist (the Steamify checkout shared into the VM); set REPO=." >&2; exit 1; }
export REPO
mkdir -p "$VM_DIR"
iso="${iso:-$VM_DIR/cachyos.iso}"
[[ -f "$iso" ]] || { echo "No ISO at $iso: run get-iso.sh (in $VM_DIR) or pass --iso <file>." >&2; exit 1; }
iso="$(cd "$(dirname "$iso")" && pwd)/$(basename "$iso")"
pgrep -f "[h]ostfwd=tcp::$VM_PORT-" >/dev/null && { echo "A VM is running on port $VM_PORT; power it off first." >&2; exit 1; }
if [[ -f "$VM_DIR/disk.qcow2" ]]; then
[[ "$force" == true ]] || { echo "$VM_DIR already has a disk.qcow2; use --force to replace it, or another VM_DIR." >&2; exit 1; }
read -r -p "Replace $VM_DIR/disk.qcow2 and all its snapshots? Type YES: " answer < /dev/tty
[[ "$answer" == YES ]] || { echo "Nothing changed."; exit 1; }
fi
# --- The SSH key to authorize: a new one just for the VM, or one of yours.
# The VM's own key is made once and then always used: asked only when it
# doesn't exist yet.
dedicated="$HOME/.ssh/steamify-vm_ed25519"
[[ -z "${VM_SSH_KEY:-}" && -f "$dedicated" ]] && VM_SSH_KEY="$dedicated"
if [[ -z "${VM_SSH_KEY:-}" ]]; then
mapfile -t pubs < <(ls "$HOME"/.ssh/*.pub 2>/dev/null | grep -vxF "$dedicated.pub")
echo "Which SSH key should log in to the VM?"
echo " 1) a new key just for the VM, used from now on: $dedicated (no passphrase)"
for i in "${!pubs[@]}"; do echo " $((i + 2))) your key ${pubs[$i]%.pub}"; done
read -r -p "Choice [1]: " choice < /dev/tty
choice="${choice:-1}"
if [[ "$choice" == 1 ]]; then
VM_SSH_KEY="$dedicated"
# Never overwrite a key: it doesn't exist here.
ssh-keygen -q -t ed25519 -N "" -C "steamify-vm" -f "$dedicated"
elif [[ "$choice" =~ ^[0-9]+$ && -n "${pubs[$((choice - 2))]:-}" ]]; then
VM_SSH_KEY="${pubs[$((choice - 2))]%.pub}"
else
echo "Unknown choice: $choice" >&2; exit 1
fi
fi
[[ -f "$VM_SSH_KEY.pub" ]] || { echo "No public key $VM_SSH_KEY.pub." >&2; exit 1; }
pubkey="$(cat "$VM_SSH_KEY.pub")"
echo "VM: $VM_DIR user: $VM_USER password: $password hostname: $hostname timezone: $timezone"
echo "ISO: $iso key: $VM_SSH_KEY"
# --- The ISO's kernel and initramfs, to boot it with our parameters.
boot="$VM_DIR/iso-boot"
mkdir -p "$boot"
# One loop setup at a time: parallel installs (vmtest.sh --install) otherwise get "Device or resource busy".
exec 9> /tmp/vminstall-iso.lock; flock 9
if ! command -v udisksctl >/dev/null && [[ $EUID -eq 0 ]]; then
# No udisks (the WSL box, as root): a plain loop device and mount.
loop="$(losetup -f -r -P --show "$iso")"
mnt="$(mktemp -d)"
cleanup_loop() { umount "$mnt" 2>/dev/null || true; rmdir "$mnt" 2>/dev/null || true; losetup -d "$loop" 2>/dev/null || true; }
trap cleanup_loop EXIT
mount -r "${loop}p1" "$mnt" 2>/dev/null || mnt=""
else
loop="$(udisksctl loop-setup --no-user-interaction -r -f "$iso" | grep -o '/dev/loop[0-9]*')"
cleanup_loop() { udisksctl unmount --no-user-interaction -b "${loop}p1" >/dev/null 2>&1 || true; udisksctl loop-delete --no-user-interaction -b "$loop" >/dev/null 2>&1 || true; }
trap cleanup_loop EXIT
mnt=""
for _ in $(seq 10); do
mnt="$(findmnt -nro TARGET "${loop}p1" 2>/dev/null || true)"
[[ -n "$mnt" ]] && break
udisksctl mount --no-user-interaction -b "${loop}p1" >/dev/null 2>&1 || true; sleep 1
done
fi
[[ -n "$mnt" ]] || { echo "Couldn't mount the ISO." >&2; exit 1; }
rm -f "$boot"/* # copied read-only from the ISO
install -m 644 "$mnt/arch/boot/x86_64/vmlinuz-linux-cachyos" "$mnt/arch/boot/x86_64/initramfs-linux-cachyos.img" "$boot/"
search="$(grep -rhoE 'archisosearchuuid=[^ ]+' "$mnt/boot" "$mnt/EFI" 2>/dev/null | head -1)"
[[ -n "$search" ]] || { echo "No archisosearchuuid in the ISO's boot menu." >&2; exit 1; }
cleanup_loop; trap - EXIT
flock -u 9; exec 9>&-
# --- What the live system fetches from the host.
www="$VM_DIR/install-www"
rm -rf "$www"; mkdir -p "$www"
# QEMU's user networking (-nic user in run.sh) always shows the host to the
# guest at 10.0.2.2, whatever network the host is on; only a bridged/tap
# setup would need another address.
host_ip="${VM_HOST_IP:-10.0.2.2}"
port="$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1])')"
sed -e "s/@PORT@/$port/" -e "s/@HOST_IP@/$host_ip/" "$repo/share/vminstall-live.sh" > "$www/vminstall-live.sh"
cp "$repo/share/vminstall-post.sh" "$www/"
{
printf 'VM_USER=%q\nVM_HOSTNAME=%q\nVM_PUBKEY=%q\nDEVICE=/dev/vda\nVM_STEAMIFY=%q\n' "$VM_USER" "$hostname" "$pubkey" "${VM_STEAMIFY:-}"
} > "$www/vminstall.env"
python3 - "$www/settings.json" "$VM_USER" "$password" "$timezone" "$hostname" "${VM_BOOTLOADER:-limine}" << 'PY'
import json, sys
out, user, pw, tz, host, bootloader = sys.argv[1:]
json.dump({
"install_type": "simple", "headless_mode": True,
"device": "/dev/vda", "fs_name": "btrfs", "subvolumes": "default",
"partitions": [
{"name": "/dev/vda1", "mountpoint": "/boot", "size": "2G", "fs_name": "vfat", "type": "boot"},
{"name": "/dev/vda2", "mountpoint": "/", "size": "100%", "type": "root"},
],
"hostname": host, "locale": "en_US.UTF-8", "xkbmap": "us", "timezone": tz,
"user_name": user, "user_pass": pw, "user_shell": "/bin/fish", "root_pass": pw,
"kernel": "linux-cachyos linux-cachyos-lts", "desktop": "kde", "bootloader": bootloader,
}, open(out, "w"), indent=2)
PY
python3 "$here/vminstall-server.py" "$www" "$port" &
server=$!
trap 'kill $server 2>/dev/null || true' EXIT
# --- Install.
cd "$VM_DIR"
cp "$repo/run.sh" run.sh # always this repo's: the install needs its kernel/serial options
[[ -e share ]] || ln -s "$repo/share" share
rm -f disk.qcow2 vars.fd vars.clean.fd vars.ssh-ready.fd known_hosts
qemu-img create -q -f qcow2 disk.qcow2 60G
echo "$VM_USER" > vm-user
echo "$REPO" > repo-path
echo "$VM_SSH_KEY" > ssh-key
# systemd.run= only generates kernel-command-line.service; systemd.wants=
# starts it next to the normal boot (desktop, network). It fetches and runs
# the live script. (Root logs in on the serial console without a password:
# VM_SERIAL.sock, for debugging.)
append="$search archisobasedir=arch cow_spacesize=10G module_blacklist=pcspkr console=ttyS0,115200 console=tty0"
append+=" systemd.run=\"/usr/bin/bash -c 'curl -fsS --retry 150 --retry-all-errors --retry-delay 2 http://$host_ip:$port/vminstall-live.sh | bash'\""
append+=" systemd.run_success_action=none systemd.run_failure_action=none systemd.wants=kernel-command-line.service"
echo "Installing: watch the VM's window, or follow the console with tail -f $VM_DIR/serial.log (install log: $www/install.log)..."
VM_SERIAL="$VM_DIR/serial.log" VM_ISO="$iso" VM_KERNEL="$boot/vmlinuz-linux-cachyos" VM_INITRD="$boot/initramfs-linux-cachyos.img" VM_APPEND="$append" \
./run.sh install "${run_flags[@]}" > vm-install.log 2>&1 &
qemu=$!
# 90 minutes; a failure reported by the live system ends it at once (it stays up after one)
for _ in $(seq 1080); do
kill -0 $qemu 2>/dev/null || break
[[ -s "$www/status" && "$(cat "$www/status")" != ok ]] && { sleep 5; kill $qemu 2>/dev/null; wait $qemu 2>/dev/null; break; }
sleep 5
done
kill -0 $qemu 2>/dev/null && { echo "The install didn't finish in 90 minutes; see $www/install.log." >&2; kill $qemu; exit 1; }
status="$(cat "$www/status" 2>/dev/null || echo "no status (the live system never reported back)")"
cp "$www/install.log" install.log 2>/dev/null || true
[[ "$status" == ok ]] || { echo "Install failed: $status. Log: $VM_DIR/install.log" >&2; exit 1; }
qemu-img snapshot -c clean disk.qcow2 && cp vars.fd vars.clean.fd
echo "Installed; snapshot 'clean' taken."
# --- First boot: check it's ssh-ready, then snapshot.
nohup ./run.sh "${run_flags[@]}" > vm.log 2>&1 &
vm=$!
for _ in $(seq 100); do vm_ssh -o ConnectTimeout=3 true 2>/dev/null && break; sleep 3; done
vm_ssh true || { echo "No SSH after the first boot; see $VM_DIR/vm.log." >&2; exit 1; }
# The poweroff at the end may drop the connection (ssh exits 255): judge by the ssh-ready line instead.
out="$(vm_ssh bash -s 2>&1 << 'REMOTE'
set -e
if [[ -f /etc/sddm.conf.d/10-gamescope-autologin.conf ]]; then
# Steamify's gaming mode (gamescope, no Plasma, and it doesn't render in the VM): the login manager has to be up.
for _ in $(seq 60); do systemctl is-active --quiet display-manager && break; sleep 3; done
systemctl is-active --quiet display-manager || { echo "The login manager didn't start." >&2; exit 1; }
else
for _ in $(seq 60); do pgrep -u "$USER" -x plasmashell >/dev/null && break; sleep 3; done
pgrep -u "$USER" -x plasmashell >/dev/null || { echo "Plasma didn't start." >&2; exit 1; }
fi
sudo -n true
echo "ssh-ready: $(id -un)@$(hostname), $(uname -r), $(localectl status | sed -n 's/.*LANG=//p'), $(timedatectl show -p Timezone --value)"
sudo systemctl poweroff
REMOTE
)" || true
printf '%s\n' "$out" | grep -v '^Connection to .* closed'
grep -q '^ssh-ready: ' <<< "$out" || { echo "The first boot check failed; see above and $VM_DIR/vm.log." >&2; kill $vm 2>/dev/null; exit 1; }
while kill -0 $vm 2>/dev/null; do sleep 2; done # this VM only: others may run in parallel
qemu-img snapshot -c ssh-ready disk.qcow2 && cp vars.fd vars.ssh-ready.fd
rm -rf "$www"
echo "Done: snapshots 'clean' and 'ssh-ready' in $VM_DIR."
echo "Log in by hand as $VM_USER / $password (root: $password)."
+32
View File
@@ -0,0 +1,32 @@
#!/bin/bash
# Simulate the Steam Machine ISO's installer step in the test VM: a new user
# who never logged in (no session bus, user systemd or plasmashell) gets
# `steamify.sh --defaults` from /mnt, visibly in a Konsole on the VM's
# desktop. Prints the per-component result lines.
# scripts/vminstallsim.sh [--fresh] [--defaults options...]
# --fresh: delete and recreate the user first; the rest goes to
# --defaults (e.g. --options gaming,theme --boot desktop)
# Env: VM_USER / VM_PORT / VM_HOST, SIM_USER (isotest), GUEST_UID (1000).
set -euo pipefail
. "$(dirname "$0")/common.sh"
fresh=""; [[ "${1:-}" == --fresh ]] && { fresh=1; shift; }
{ printf 'u=%q fresh=%q uid=%q opts=%q\n' "${SIM_USER:-isotest}" "$fresh" "${GUEST_UID:-1000}" "$*"; cat << 'REMOTE'
mountpoint -q /mnt || sudo mount -t 9p -o trans=virtio,version=9p2000.L repo /mnt
[ -n "$fresh" ] && sudo userdel -r "$u" 2>/dev/null
cat > /tmp/install-sim.sh << SIM
#!/bin/bash
id $u >/dev/null 2>&1 || { sudo useradd -m -G wheel -s /bin/bash $u; echo "$u:$u" | sudo chpasswd; }
echo "$u ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/10-steamify-install-sim >/dev/null
sudo -u $u env -i HOME=/home/$u USER=$u LOGNAME=$u PATH=/usr/local/bin:/usr/bin TERM=xterm LANG=C.UTF-8 \
bash -c 'cd ~ && /mnt/steamify.sh --defaults $opts < /dev/null' 2>&1 | tee /tmp/install-sim.log
echo "exit: \${PIPESTATUS[0]}" | tee -a /tmp/install-sim.log
sudo rm -f /etc/sudoers.d/10-steamify-install-sim
touch /tmp/install-sim.done
SIM
chmod +x /tmp/install-sim.sh; rm -f /tmp/install-sim.done
XDG_RUNTIME_DIR=/run/user/$uid DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$uid/bus \
systemd-run --user -q konsole --hold -e /tmp/install-sim.sh
while [ ! -f /tmp/install-sim.done ]; do sleep 3; done
sed 's/\x1b\[[0-9;]*m//g' /tmp/install-sim.log | grep -E '^Turning|^\[(WARN|ERROR|OK)\]|^exit'
REMOTE
} | vm_ssh bash -s 2>&1
+44
View File
@@ -0,0 +1,44 @@
#!/bin/bash
# Boot a live ISO (the Steam Machine ISO) in its own VM, the normal way, to go
# through Calamares by hand or driven with scripts/qmpkey.py / qmptype.py,
# with screenshots of every page (scripts/qmpshot.py; no virgl, so QMP
# screendump works). Unlike vminstall.sh nothing is automated: the wizard,
# its Steamify pages and the installer's Steamify step run as on real
# hardware. Starts in the background; the VM's window is where you watch.
# scripts/vmisoboot.sh [--fresh] [--iso <file>] [run.sh flags...] e.g. --fremont
# --fresh a new, empty disk (else the existing one: e.g. boot the
# installed system with `./run.sh` in $VM_DIR afterwards)
# --iso <file> default: the newest out/desktop/*.iso of the host's
# steamify-cachyos-live-iso checkout
# Env: ISO_VM_DIR (~/vms/iso-vm), ISO_VM_PORT (2223: runs next to the test
# VM on 2222).
set -euo pipefail
here="$(cd "$(dirname "$0")" && pwd)"
repo="$(cd "$here/.." && pwd)"
dir="${ISO_VM_DIR:-$HOME/vms/iso-vm}"
port="${ISO_VM_PORT:-2223}"
fresh=false iso="" flags=()
while [[ $# -gt 0 ]]; do
case "$1" in
--fresh) fresh=true; shift ;;
--iso) iso="${2:?--iso needs a file}"; shift 2 ;;
*) flags+=("$1"); shift ;;
esac
done
if [[ -z "$iso" ]]; then
iso="$(ls -t "$(dirname "$repo")"/steamify-cachyos-live-iso/out/desktop/*.iso 2>/dev/null | head -1)"
[[ -n "$iso" ]] || { echo "No ISO in steamify-cachyos-live-iso/out/desktop: build one (vmisobuild.sh) or pass --iso." >&2; exit 1; }
fi
iso="$(cd "$(dirname "$iso")" && pwd)/$(basename "$iso")"
mkdir -p "$dir"
cd "$dir"
[[ -S qmp.sock ]] && python3 -c "import socket,sys; s=socket.socket(socket.AF_UNIX); s.connect(sys.argv[1])" qmp.sock 2>/dev/null &&
{ echo "The ISO VM in $dir is running already." >&2; exit 1; }
cp "$repo/run.sh" run.sh
[[ -e share ]] || ln -s "$repo/share" share
if [[ "$fresh" == true ]]; then rm -f disk.qcow2 vars.fd; fi
REPO="${REPO:-$(dirname "$repo")/steamify-cachyos}" VM_NOGL=1 VM_PORT="$port" VM_ISO="$iso" \
nohup ./run.sh install "${flags[@]}" > vm.log 2>&1 &
echo "ISO VM: $iso in $dir (SSH port $port once installed)."
echo "Screenshot: python3 $here/qmpshot.py $dir/qmp.sock shot.png"
echo "Keys: python3 $here/qmpkey.py $dir/qmp.sock down ret / qmptype.py $dir/qmp.sock \"text\""
+67
View File
@@ -0,0 +1,67 @@
#!/bin/bash
# Build the Steam Machine ISO (steamify-cachyos-live-iso) in the test VM
# (directly, no container: build-live-modules.sh and build-calamares-modules.sh with sudo, buildiso.sh as
# the user, it sudos itself),
# visibly (a Konsole on the VM's desktop follows the log), with the package
# downloads cached on the host, and copy the ISO back.
# scripts/vmisobuild.sh [--steamify <guest path>]
# --steamify a Steamify checkout in the guest (e.g. /mnt, the mounted
# REPO) instead of the newest release
# The VM must run with its cache shared (one per VM, next to its disk), and
# gets all host cores but 2 for the build: start it with
# VM_CACHE=$VM_DIR/iso-cache VM_CPUS=$(( $(nproc) * 3 / 4 )) scripts/vmreset.sh --fremont
# Env: VM_DIR (see common.sh), ISO_BRANCH (feat/steamify), ISO_OUT (the host's
# steamify-cachyos-live-iso/out/desktop).
set -euo pipefail
here="$(cd "$(dirname "$0")" && pwd)"
. "$here/common.sh"
steamify=""
[[ "${1:-}" == --steamify ]] && steamify="${2:?--steamify needs a guest path}"
branch="${ISO_BRANCH:-feat/steamify}"
# Where the ISO lands: out/desktop of the host's live-ISO checkout (next to
# this repo), where a local build would put it.
out="${ISO_OUT:-$(dirname "$(cd "$here/.." && pwd)")/steamify-cachyos-live-iso/out/desktop}"
mkdir -p "$out"
{ printf 'branch=%q steamify=%q\n' "$branch" "$steamify"; cat << 'REMOTE'
set -e
export XDG_RUNTIME_DIR=/run/user/$(id -u) DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus
# The package cache on the host (9p tag `cache`), kept across snapshot resets,
# is the VM's pacman cache: the build tools and mkarchiso (which installs the
# ISO's packages with the build system's cache) both use it. No container:
# the VM is CachyOS already, and a snapshot reset removes the build tools.
sudo mkdir -p /var/cache/steamify-iso
mountpoint -q /var/cache/steamify-iso || sudo mount -t 9p -o trans=virtio,version=9p2000.L cache /var/cache/steamify-iso ||
{ echo "No 9p share 'cache': start the VM with VM_CACHE=<host dir> (run.sh)." >&2; exit 1; }
sudo mkdir -p /var/cache/steamify-iso/pkg
mountpoint -q /var/cache/pacman/pkg || sudo mount --bind /var/cache/steamify-iso/pkg /var/cache/pacman/pkg
[ -z "$steamify" ] || mountpoint -q /mnt || sudo mount -t 9p -o trans=virtio,version=9p2000.L repo /mnt
# archlinux.cachyos.org serves packages without their .sig (404), and pacman
# doesn't try the next mirror for the signature of a cached package: the
# build fails. mkarchiso uses this mirrorlist, so skip it here.
sudo sed -i '/archlinux\.cachyos\.org/s/^Server/#Server/' /etc/pacman.d/mirrorlist
sudo pacman -Sy --needed --noconfirm archiso mkinitcpio-archiso git squashfs-tools grub >/dev/null
iso=~/projects/steamify-cachyos-live-iso
if [ -d "$iso/.git" ]; then git -C "$iso" fetch -q && git -C "$iso" checkout -q "$branch" && git -C "$iso" reset -q --hard "origin/$branch"
else mkdir -p ~/projects && git clone -q -b "$branch" https://github.com/theupriser/steamify-cachyos-live-iso "$iso"; fi
cd "$iso" && echo "ISO repo: $(git log --oneline -1)"
./steamify-prepare.sh $steamify | tail -1
sudo rm -rf build out
cat > ~/projects/iso-build.sh << S
#!/bin/bash
cd $iso
sudo ./build-live-modules.sh && sudo ./build-calamares-modules.sh && { ./buildiso.sh -p desktop -w || ./buildiso.sh -p desktop -c -w; }
echo "== BUILD EXIT \$?"
ls -la out/desktop/*.iso 2>/dev/null
S
chmod +x ~/projects/iso-build.sh; : > ~/projects/iso-build.log
systemd-run --user --collect -q -u "isobuild-$(date +%s)" bash -c "~/projects/iso-build.sh > ~/projects/iso-build.log 2>&1"
systemd-run --user -q --setenv=XDG_CURRENT_DESKTOP=KDE konsole --separate --hold -e bash -c "tail -n 50 -F ~/projects/iso-build.log"
REMOTE
} | vm_ssh bash -s
echo "Building in the VM (its Konsole shows the log)..."
until vm_ssh 'grep -q "^== BUILD EXIT" ~/projects/iso-build.log'; do sleep 30; done
iso="$(vm_ssh "bash -c 'ls -t ~/projects/steamify-cachyos-live-iso/out/desktop/*.iso 2>/dev/null | head -1'")"
[[ -n "$iso" ]] || { echo "No ISO was built; see ~/projects/iso-build.log in the VM." >&2; exit 1; }
vm_scp "$VM_USER@$VM_HOST:$iso" "$out/"
echo "ISO: $out/$(basename "$iso") (cache: $VM_DIR/iso-cache)"
+50
View File
@@ -0,0 +1,50 @@
#!/bin/bash
# Progress of test jobs, one line per change (for a monitor; see the progress-report skill).
# scripts/vmprogress.sh [--once] [job...] jobs: limine systemd-boot grub cli menu hw installer toggles (default all)
# Per job (only a new step, a finished job or a FAIL counts as a change): state (install NN% <stage> / <test step> / done / -), checks passed/expected, failed.
# Line: "HH:MM total P/E F failed ;; job | state | P/E | F failed ;; ...". Prints nothing while nothing
# changes (checked every 30 s), "ALLDONE" and exits once every started job has its summary line.
# Expected counts: the last complete run of each (AGENTS.md "State"); update them when checks are added.
set -uo pipefail
declare -A expect=([limine]=43 [systemd-boot]=42 [grub]=42 [cli]=55 [menu]=36 [hw]=77 [installer]=4 [toggles]=42)
once=false; jobs=()
for a in "$@"; do [[ "$a" == --once ]] && once=true || jobs+=("$a"); done
[[ ${#jobs[@]} -gt 0 ]] || jobs=(limine systemd-boot grub cli menu hw installer toggles)
vms="$HOME/vms"
status() { # status <job>: "state|pass|fail|finished(0/1)|started(0/1)"
local j="$1" log dir
case "$j" in limine|systemd-boot|grub) log="$vms/bl-$j.test.log" dir="$vms/bl-$j" ;; *) log="$vms/run-$j.log" dir="$vms/run-$j" ;; esac
[[ -f "$log" ]] || { echo "-|0|0|0|0"; return; }
local p f st run
# The logs are appended to: only the latest run counts (from its "===== vm..." header on).
run="$(tac "$log" | sed '/^===== vm/q' | tac)"
p=$(grep -ac '^PASS' <<< "$run"); f=$(grep -ac '^FAIL' <<< "$run")
if grep -aq "^== $j: [0-9]* passed" <<< "$run"; then echo "done|$p|$f|1|1"; return; fi
st=$(grep -aE '^##### ' <<< "$run" | tail -1 | sed 's/^##### [0-9:]* //' | cut -c1-40)
if [[ "$st" == install* && -f "$dir/install-www/install.log" ]]; then
st="install $(grep -aoE '^\[ *[0-9.]+%\] [^.(]*' "$dir/install-www/install.log" | tail -1 | sed 's/^\[ *\([0-9]*\)[0-9.]*%\] /\1% /; s/ *$//')"
# The live installer's own failure (vminstall-live.sh: "== failed: ..."), or the host side giving up.
local why; why=$(grep -ahE '^== failed: ' "$dir/install-www/install.log" "$log" 2>/dev/null | tail -1 | cut -c12-70)
[[ -n "$why" ]] && { st="INSTALL FAILED: $why"; f=$((f + 1)); }
fi
echo "${st:-starting}|$p|$f|0|1"
}
prev=""
while true; do
line="" tp=0 te=0 tf=0 running=0
for j in "${jobs[@]}"; do
IFS='|' read -r st p f fin started <<< "$(status "$j")"
[[ "$started" == 1 && "$fin" == 0 ]] && running=$((running + 1))
tp=$((tp + p)) tf=$((tf + f)) te=$((te + ${expect[$j]:-0}))
line+=" ;; $j | $st | $p/${expect[$j]:-?} | $f failed"
done
cur="total $tp/$te $tf failed$line"
# A change is a new stage/step, a job finishing or a FAIL: not a new install percentage or PASS count.
key="$(sed -E 's/install [0-9]+% /install /g; s/\| [0-9]+\/[0-9?]+ \|/|/g; s/^total [0-9]+\/[0-9]+ //' <<< "$cur")"
[[ "$key" != "$prev" ]] && { echo "$(date +%H:%M) $cur"; prev="$key"; }
$once && exit 0
[[ $running -eq 0 ]] && { echo ALLDONE; exit 0; }
sleep 30
done
+7 -5
View File
@@ -9,10 +9,7 @@
# VM_LOG (QEMU output, default $VM_DIR/vm.log).
set -euo pipefail
. "$(dirname "$0")/common.sh"
repo="$(cd "$(dirname "$0")/.." && pwd)"
if [[ -z "${VM_DIR:-}" ]]; then
if [[ -f "$repo/disk.qcow2" ]]; then VM_DIR="$repo"; else VM_DIR="$HOME/vms/cachyos-test"; fi
fi
repo_dir="$(cd "$(dirname "$0")/.." && pwd)"
SNAPSHOT="${SNAPSHOT:-ssh-ready}"
VM_LOG="${VM_LOG:-$VM_DIR/vm.log}"
@@ -25,6 +22,8 @@ fi
echo "Restoring snapshot '$SNAPSHOT' in $VM_DIR..."
cd "$VM_DIR"
# Always this repo's run.sh: a copy in $VM_DIR falls behind (VM_CACHE, ...).
[[ "$repo_dir/run.sh" -ef run.sh ]] || cp "$repo_dir/run.sh" run.sh
qemu-img snapshot -a "$SNAPSHOT" disk.qcow2
cp "vars.$SNAPSHOT.fd" vars.fd
@@ -37,7 +36,10 @@ vm_ssh bash -s << 'REMOTE'
sudo mount -t 9p -o trans=virtio,version=9p2000.L repo /mnt
sudo mkdir -p /etc/plasmalogin.conf.d
printf '[Autologin]\nUser=%s\nSession=plasma.desktop\n' "$USER" | sudo tee /etc/plasmalogin.conf.d/00-test-autologin.conf >/dev/null
sudo systemctl restart plasmalogin
# Snapshots with the autologin file log in at boot; restarting plasmalogin
# then fails (HELPER_TTY_ERROR on tty1) and leaves the greeter.
for _ in $(seq 10); do pgrep -u "$USER" -x plasmashell >/dev/null && break; sleep 3; done
pgrep -u "$USER" -x plasmashell >/dev/null || { sudo systemctl reset-failed plasmalogin; sudo systemctl restart plasmalogin; }
for _ in $(seq 40); do pgrep -u "$USER" -x plasmashell >/dev/null && break; sleep 3; done
sleep 10
# Windows the snapshot opens at login would cover the screenshots.
+1 -1
View File
@@ -25,5 +25,5 @@ sleep 2
systemd-run --user --wait -q spectacle -b -n -f -o /tmp/vmshot.png
REMOTE
} | vm_ssh bash -s
scp -q -P "$VM_PORT" "$VM_USER@$VM_HOST:/tmp/vmshot.png" "$out"
vm_scp "$VM_USER@$VM_HOST:/tmp/vmshot.png" "$out"
echo "$out"
+2 -2
View File
@@ -9,7 +9,7 @@ echo "DM=$(systemctl show -p Id --value display-manager) sddm-autologin=$(yn /et
echo "plasmalogin [Autologin]: $(sed -n '/^\[Autologin\]/,/^\[/p' /etc/plasmalogin.conf | grep -v '^\[' | tr '\n' ' ')"
echo "shortcut=$(grep -h Exec= ~/Desktop/*Gaming*.desktop 2>/dev/null || echo none)"
echo "steam-unit=$([ -f ~/.config/systemd/user/steam-desktop-autostart.service ] && systemctl --user is-enabled steam-desktop-autostart.service || echo none) glyphs=$(yn ~/.config/environment.d/99-gamescope-steam-glyphs.conf)"
echo "lnf=$(kreadconfig6 --file kdeglobals --group KDE --key LookAndFeelPackage) dark-lnf=$(kreadconfig6 --file kdeglobals --group KDE --key DefaultDarkLookAndFeel) scheme=$(kreadconfig6 --file kdeglobals --group General --key ColorScheme) cursor=$(kreadconfig6 --file kcminputrc --group Mouse --key cursorTheme) vapor-pkg=$(pacman -Q cachyos-vapor 2>/dev/null || echo none) layout-backup=$(yn ~/.local/state/cachyos-gamescope-boot/theme-layout)"
echo "lnf=$(kreadconfig6 --file kdeglobals --group KDE --key LookAndFeelPackage) dark-lnf=$(kreadconfig6 --file kdeglobals --group KDE --key DefaultDarkLookAndFeel) scheme=$(kreadconfig6 --file kdeglobals --group General --key ColorScheme) cursor=$(kreadconfig6 --file kcminputrc --group Mouse --key cursorTheme) vapor-pkg=$(pacman -Q cachyos-vapor 2>/dev/null || echo none) layout-backup=$(yn ~/.local/state/steamify/theme-layout)"
echo "wallpaper: $(grep -h '^Image=' ~/.config/plasma-org.kde.plasma.desktop-appletsrc | sort -u | tr '\n' ' ')"
echo "lock_screen=$(kreadconfig6 --file kdeglobals --group 'KDE Action Restrictions' --key action/lock_screen) autolock=$(kreadconfig6 --file kscreenlockerrc --group Daemon --key Autolock) metaL=$(kreadconfig6 --file kglobalshortcutsrc --group ksmserver --key 'Lock Session' | cut -c1-12)"
echo "panel: $(grep -A1 '^\[PlasmaViews\]\[Panel [0-9]*\]$' ~/.config/plasmashellrc | grep floating | tr '\n' ' ') $(grep -h 'thickness' ~/.config/plasmashellrc | tr '\n' ' ')"
@@ -18,7 +18,7 @@ echo "leds-pkg=$(pacman -Q leds-valve-dkms-git 2>/dev/null || echo none) steamos
echo "dkms: $(dkms status leds-valve-dkms 2>/dev/null | cut -d, -f2- | sed 's/, x86_64//' | tr '\n' ';') kernels: $(ls /usr/lib/modules | tr '\n' ' ')"
echo "dkms-override=$(yn /etc/dkms/leds-valve-dkms.conf) headers-check=$(systemctl is-enabled ensure-kernel-headers.service 2>/dev/null || echo none)"
echo "leds-owner=$(stat -c %U /sys/class/leds/valve-leds*/brightness 2>/dev/null | sort -u | tr '\n' ' ')"
j=~/.local/state/cachyos-gamescope-boot
j=~/.local/state/steamify
echo "journals: $(ls "$j" 2>/dev/null | tr '\n' ' ')"
for f in "$j"/*; do [ -f "$f" ] || continue; d=$(cut -f1-3 "$f" | sort | uniq -d); [ -n "$d" ] && echo "DUPLICATES in $(basename "$f"): $d"; done
true
+102
View File
@@ -0,0 +1,102 @@
#!/bin/bash
# One suite of the automated test (TESTPLAN.md rows that need no person) on the plain CachyOS VM
# (Plasma, no Steamify: ~/vms/steamify-vm, from vminstall.sh --iso <CachyOS ISO> with VM_STEAMIFY=skip).
# scripts/vmsuite.sh <suite> [--window] suites: the folders in share/vmtest/ (cli, ...)
# scripts/vmsuite.sh --prepare only make the base image (below)
# Every block share/vmtest/<suite>/NN-name.sh starts from a fresh copy of the VM's `ssh-ready` state,
# runs in the guest as the VM user (repo on /mnt; NN-name.host.sh blocks run on the host instead: reboots,
# host scripts), and prints PASS/FAIL/SKIP lines; the first comment lines say which TESTPLAN rows it
# covers; `# env: VAR=value` there is set for the VM start (e.g. BIOS_VERSION=F7F0107).
# Fast and parallel: the `ssh-ready` snapshot is converted once into $SRC/base-ssh-ready.qcow2; each suite
# has its own directory ~/vms/run-<suite> and ssh port, where every block gets a new qcow2 overlay of the
# base (instant, no snapshot restore), and the host's package cache (VM_CACHE, ~/vms/pkg-cache) is bound
# over pacman's cache, so packages are downloaded once for all. Log: $VM_DIR.log. Exit status = FAILs.
# Env: ONLY=<block name prefix> runs just that block; SRC (default ~/vms/steamify-vm); REPO (default ../steamify-cachyos).
set -uo pipefail
here="$(cd "$(dirname "$0")" && pwd)"
repo="$(cd "$here/.." && pwd)"
src="${SRC:-$HOME/vms/steamify-vm}"
base="$src/base-ssh-ready.qcow2"
prepare_base() { # once, under a lock (parallel suites): the ssh-ready snapshot as a stand-alone image
( flock 9
if [[ ! -f "$base" || "$src/vars.ssh-ready.fd" -nt "$base" ]]; then
echo "Making $base from the ssh-ready snapshot (once, a few minutes)..." >&2
qemu-img convert -f qcow2 -O qcow2 -l snapshot.name=ssh-ready "$src/disk.qcow2" "$base.tmp" && mv "$base.tmp" "$base"
fi ) 9>"$src/.base.lock"
[[ -f "$base" ]]
}
if [[ "${1:-}" == --prepare ]]; then prepare_base; exit $?; fi
suite="${1:?usage: $0 <suite> [--window] | --prepare}"
[[ "${2:-}" == --window ]] || export VM_HEADLESS=1
dir="$repo/share/vmtest/$suite"
[[ -d "$dir" ]] || { echo "No suite $suite (folders in share/vmtest/)" >&2; exit 2; }
case "$suite" in cli) port=2301 ;; menu) port=2302 ;; hw) port=2303 ;; installer) port=2304 ;; toggles) port=2305 ;; *) port=2390 ;; esac
export VM_PORT="${VM_PORT:-$port}"
export VM_DIR="$HOME/vms/run-$suite"
export REPO="${REPO:-$repo/../steamify-cachyos}"
export VM_CACHE="${VM_CACHE-$HOME/vms/pkg-cache}"
export VM_MEM="${VM_MEM:-4G}" # three VMs at once next to a desktop: 8G each pushed the host into swap
prepare_base || { echo "FAIL no base image" >&2; exit 1; }
mkdir -p "$VM_DIR"
for f in vm-user ssh-key; do cp "$src/$f" "$VM_DIR/$f"; done
echo "$REPO" > "$VM_DIR/repo-path"; [[ -e "$VM_DIR/share" ]] || ln -s "$repo/share" "$VM_DIR/share"
. "$here/common.sh"
log="$VM_DIR.log"
view_start # one shared Konsole (common.sh), headless on a desktop
vm_running() { pgrep -f "[h]ostfwd=tcp::$VM_PORT-" >/dev/null; }
vm_stop() {
vm_running || return 0
vm_ssh -o ConnectTimeout=4 'sudo systemctl poweroff' >/dev/null 2>&1
local _; for _ in $(seq 20); do vm_running || return 0; sleep 1; done
pkill -f "[h]ostfwd=tcp::$VM_PORT-"; sleep 2
}
# A fresh VM for a block: new overlay of the base, the snapshot's UEFI vars, Plasma up, repo mounted, package cache bound.
vm_fresh() { # vm_fresh [VAR=value...]: extra environment for the VM start (BIOS_VERSION=...)
vm_stop
cp "$repo/run.sh" "$VM_DIR/run.sh"; cp "$src/vars.ssh-ready.fd" "$VM_DIR/vars.fd"
rm -f "$VM_DIR/disk.qcow2" "$VM_DIR/known_hosts"
qemu-img create -q -f qcow2 -b "$base" -F qcow2 "$VM_DIR/disk.qcow2" || return 1
(cd "$VM_DIR" && env "$@" nohup ./run.sh --fremont > vm.log 2>&1 &)
local _; for _ in $(seq 100); do vm_ssh -o ConnectTimeout=3 true 2>/dev/null && break; sleep 3; done
vm_ssh -o LogLevel=ERROR bash -s <<'REMOTE'
sudo mount -t 9p -o trans=virtio,version=9p2000.L repo /mnt
sudo mkdir -p /etc/plasmalogin.conf.d
printf '[Autologin]\nUser=%s\nSession=plasma.desktop\n' "$USER" | sudo tee /etc/plasmalogin.conf.d/00-test-autologin.conf >/dev/null
for _ in $(seq 10); do pgrep -u "$USER" -x plasmashell >/dev/null && break; sleep 3; done
pgrep -u "$USER" -x plasmashell >/dev/null || { sudo systemctl reset-failed plasmalogin; sudo systemctl restart plasmalogin; }
for _ in $(seq 40); do pgrep -u "$USER" -x plasmashell >/dev/null && break; sleep 3; done
pkill -u "$USER" systemsettings; pkill -u "$USER" cachyos-hello
for f in /etc/pacman.d/*mirrorlist*; do sudo sed -i '/krfoss/s/^Server/#Server/' "$f"; done
# the host's package cache over pacman's
sudo mkdir -p /var/cache/steamify-pkg
if sudo mount -t 9p -o trans=virtio,version=9p2000.L cache /var/cache/steamify-pkg 2>/dev/null; then
sudo mkdir -p /var/cache/steamify-pkg/pkg; sudo mount --bind /var/cache/steamify-pkg/pkg /var/cache/pacman/pkg
# Steamify's own downloads (the CEC driver: GitHub rate-limits it) once for every VM
sudo mkdir -p /var/cache/steamify-pkg/steamify /var/cache/steamify; sudo mount --bind /var/cache/steamify-pkg/steamify /var/cache/steamify
fi
pgrep -u "$USER" -x plasmashell >/dev/null && echo "Logged in to Plasma." || { echo "Plasma did not start." >&2; exit 1; }
REMOTE
}
{
printf '\n===== vmsuite.sh %s (%s) =====\n' "$suite" "$(date +%T)"
for block in "$dir"/*.sh; do
[[ -z "${ONLY:-}" || "$(basename "$block")" == "$ONLY"* ]] || continue # ONLY=50: just that block
printf '\n##### %s %s\n' "$(date +%T)" "$(basename "$block" .sh)"
vm_fresh $(sed -n 's/^# env: //p' "$block" | head -n 1) > /tmp/vmsuite-$suite-reset.out 2>&1 \
|| { echo "FAIL the VM did not come up: $(tail -n 1 /tmp/vmsuite-$suite-reset.out)"; continue; }
if [[ "$block" == *.host.sh ]]; then # runs on the host (reboots, host scripts); prelude-host.sh has the helpers
( . "$repo/share/vmtest/prelude-host.sh"; . "$block" ) 2>&1 | sed -u 's/\x1b\[[0-9;]*m//g'
continue
fi
cat "$repo/share/vmtest/prelude.sh" "$block" | vm_ssh -o ConnectTimeout=6 -o LogLevel=ERROR bash -s 2>&1 | sed -u 's/\x1b\[[0-9;]*m//g'
done
vm_stop
} 2>&1 | tee "$log" | sed -u "s/^/[$suite] /" >> "$TEST_LOG"
fails="$(grep -c '^FAIL' "$log")"
echo "== $suite: $(grep -c '^PASS' "$log") passed, $fails failed, $(grep -c '^SKIP' "$log") skipped (log: $log)" | tee -a "$log"
grep '^FAIL' "$log"
exit "$fails"
+21
View File
@@ -0,0 +1,21 @@
#!/bin/bash
# Run the whole automated test on another machine (the PC with the VMs) from this one (e.g. a laptop):
# update the repo there, start scripts/vmtest.sh in a detached `screen`, wait for the summary, print it.
# scripts/vmtest-remote.sh <ssh-host> [--no-wait] [vmtest.sh args...] e.g. scripts/vmtest-remote.sh pc cli hw
# The branch you are on must be pushed (the remote pulls it). The remote needs this repo, ../steamify-cachyos,
# ../steamify-cachyos-live-iso (for the loader list and the ISO), qemu + OVMF + screen, KVM, and the VMs in
# ~/vms (see the vm-install skill). Env: REMOTE_REPO (default ~/projects/steamify/steamify-cachyos-dev).
set -uo pipefail
host="${1:?usage: $0 <ssh-host> [--no-wait] [vmtest.sh args...]}"; shift
wait=true; args=()
for a in "$@"; do [[ "$a" == --no-wait ]] && wait=false || args+=("$a"); done
repo="$(cd "$(dirname "$0")/.." && pwd)"
branch="$(git -C "$repo" branch --show-current)"
rr="${REMOTE_REPO:-~/projects/steamify/steamify-cachyos-dev}"
git -C "$repo" ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1 || { echo "Push $branch first: the remote pulls it." >&2; exit 1; }
[[ -z "$(git -C "$repo" status --porcelain)" ]] || echo "Note: uncommitted changes here are NOT tested; the remote runs what is pushed." >&2
ssh "$host" "rm -f ~/vms/last-test.txt; cd $rr && git fetch -q origin && git checkout -q $branch && git pull -q --ff-only && scripts/vmtest.sh --screen ${args[*]:-}" || exit 1
$wait || { echo "Started on $host; the summary lands in ~/vms/last-test.txt there (screen -r vmtest to watch)."; exit 0; }
until ssh "$host" 'grep -q "^== total" ~/vms/last-test.txt 2>/dev/null'; do sleep 30; done
ssh "$host" 'cat ~/vms/last-test.txt'
ssh "$host" 'test "$(grep -c "^FAIL" ~/vms/last-test.txt)" -eq 0'
+84
View File
@@ -0,0 +1,84 @@
#!/bin/bash
# The whole automated test, one command: the boot loader test for every loader the Steamify ISO
# advertises, plus every suite in share/vmtest/ (TESTPLAN.md rows that need no person).
# scripts/vmtest.sh [--install] [--window] [--screen] [boot | <loader>... | <suite>...]
# no argument everything: boot (all advertised loaders) and all suites
# boot the boot loader test (limine, systemd-boot, grub), or name loaders
# <suite> cli, menu, hw, installer, toggles (folders in share/vmtest/)
# --install install the boot loader VMs first from the newest Steamify ISO (asks before replacing a disk)
# --window show the VMs' windows (default: headless; one shared Konsole follows ~/vms/test.log)
# MAX_PARALLEL=3 (env) VMs at once; the slowest suites start first
# --screen run in a detached `screen` session named vmtest: `screen -r vmtest` to watch, Ctrl-a d to leave
# The suites run on the plain CachyOS VM ~/vms/steamify-vm (Plasma, from vminstall.sh --iso <CachyOS ISO>
# with VM_STEAMIFY=skip); the boot loader test on ~/vms/bl-<loader> (Steamify ISO).
# Output: one line per loader/suite and the failed checks; the same in ~/vms/last-test.txt. Exit status = failures.
# Rows that need a person (screenshots, the real Steam Machine) are listed at the end, never silently dropped.
set -uo pipefail
here="$(cd "$(dirname "$0")" && pwd)"
repo="$(cd "$here/.." && pwd)"
iso_repo="$repo/../steamify-cachyos-live-iso"
args=("$@")
install=""; window=""; screen=false; loaders=(); suites=(); want_boot=false
for a in "$@"; do
case "$a" in
--install) install=--install ;;
--window) window=--window ;;
--screen) screen=true ;;
boot) want_boot=true ;;
limine|systemd-boot|grub) loaders+=("$a"); want_boot=true ;;
*) [[ -d "$repo/share/vmtest/$a" ]] && suites+=("$a") || { echo "Unknown argument: $a" >&2; exit 2; } ;;
esac
done
if $screen; then
command -v screen >/dev/null || { echo "screen is not installed" >&2; exit 2; }
screen -ls | grep -q '\.vmtest\s' && { echo "A vmtest screen session is already running: screen -r vmtest" >&2; exit 1; }
rest=(); for a in "${args[@]}"; do [[ "$a" == --screen ]] || rest+=("$a"); done
screen -dmS vmtest bash -c "$(printf '%q ' "$0" "${rest[@]}"); echo; echo 'Done (summary: ~/vms/last-test.txt). Press Enter to close.'; read -r _"
echo "Running in the screen session vmtest: screen -r vmtest (Ctrl-a d to leave it running); the summary lands in ~/vms/last-test.txt"
exit 0
fi
if [[ ${#loaders[@]} -eq 0 && ${#suites[@]} -eq 0 ]] && ! $want_boot; then
want_boot=true
for d in cli menu hw installer toggles; do [[ -d "$repo/share/vmtest/$d" ]] && suites+=("$d"); done
fi
# What the ISO advertises: the boot loaders calamares-online.sh keeps.
advertised="$(sed -n 's/.*"bootloader:\([^"]*\)".*/\1/p' "$iso_repo/archiso/airootfs/usr/local/bin/calamares-online.sh" 2>/dev/null | head -n 1)"
[[ -n "$advertised" ]] || { echo "Can't read the advertised boot loaders from $iso_repo (calamares-online.sh)" >&2; exit 2; }
$want_boot && [[ ${#loaders[@]} -eq 0 ]] && read -r -a loaders <<< "$advertised"
tested="limine systemd-boot grub" # the loaders vmbootloadertest.sh has a way to boot the other kernel for
summary="$HOME/vms/last-test.txt"
maxp="${MAX_PARALLEL:-3}" # VMs at once: 8 GB RAM each, 6 vCPUs each
# Jobs, the slowest first so the last ones are short: suites, then the boot loaders.
jobs_list=()
for s in toggles hw cli menu installer; do [[ " ${suites[*]} " == *" $s "* ]] && jobs_list+=("suite:$s"); done
for s in "${suites[@]}"; do [[ " toggles hw cli menu installer " == *" $s "* ]] || jobs_list+=("suite:$s"); done
for l in "${loaders[@]}"; do jobs_list+=("loader:$l"); done
run_job() { # run_job <kind:name>: output in /tmp/vmtest-<name>.out, exit status in /tmp/vmtest-<name>.rc
local kind="${1%%:*}" name="${1#*:}"
if [[ "$kind" == loader ]]; then bash "$here/vmbootloadertest.sh" "$name" $install $window > "/tmp/vmtest-$name.out" 2>&1
else bash "$here/vmsuite.sh" "$name" $window > "/tmp/vmtest-$name.out" 2>&1; fi
echo $? > "/tmp/vmtest-$name.rc"
}
{
echo "vmtest.sh $(date '+%F %T') branch $(git -C "$repo" branch --show-current) $(git -C "$repo" rev-parse --short HEAD)"
total=0
for l in $advertised; do
[[ " $tested " == *" $l "* ]] || { echo "FAIL the ISO advertises $l, but there is no test for it (scripts/vmbootloadertest.sh)"; total=$((total + 1)); }
done
[[ ${#suites[@]} -gt 0 ]] && { bash "$here/vmsuite.sh" --prepare || { echo "FAIL no base image for the suites"; total=$((total + 1)); }; }
rm -f /tmp/vmtest-*.rc
for job in "${jobs_list[@]}"; do
run_job "$job" &
while [[ $(jobs -rp | wc -l) -ge $maxp ]]; do wait -n; done
done
wait
for job in "${jobs_list[@]}"; do
name="${job#*:}"
sed -n '/^== /,$p' "/tmp/vmtest-$name.out"
total=$((total + $(cat "/tmp/vmtest-$name.rc" 2>/dev/null || echo 1)))
done
echo
echo "SKIP (needs a person, TESTPLAN.md): U1-U9 the app's screens, R2.2 the first desktop login, H-Real the real Steam Machine"
echo "== total: $total failed check(s); boot loaders: ${loaders[*]:-none}; suites: ${suites[*]:-none}"
} 2>&1 | tee "$summary"
exit "$(grep -c '^FAIL' "$summary")"
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env python3
# vmview.py <VM dir or qmp socket> [seconds]: a window that shows a headless VM's screen, refreshed
# every 2 seconds (QMP screendump; read-only, no input). For watching an automated run without
# giving the VM a window. Needs python-pillow and tk; on WSL set the WSLg env (wsl-build-host skill).
import io, json, os, socket, sys, tempfile, tkinter as tk
from PIL import Image, ImageTk
target = sys.argv[1]
sock_path = os.path.join(target, "qmp.sock") if os.path.isdir(target) else target
every = int(float(sys.argv[2]) * 1000) if len(sys.argv) > 2 else 2000
def shot():
s = socket.socket(socket.AF_UNIX); s.settimeout(5); s.connect(sock_path); f = s.makefile()
f.readline(); s.sendall(b'{"execute":"qmp_capabilities"}'); f.readline()
out = os.path.join(tempfile.gettempdir(), f"vmview-{os.getpid()}.ppm")
s.sendall(json.dumps({"execute": "screendump", "arguments": {"filename": out}}).encode())
reply = json.loads(f.readline()); s.close()
if "error" in reply: raise RuntimeError(reply["error"].get("desc"))
with open(out, "rb") as fh: img = Image.open(io.BytesIO(fh.read()))
os.unlink(out); return img
root = tk.Tk(); root.title(f"vmview: {target}")
label = tk.Label(root, bg="black", fg="white"); label.pack(fill="both", expand=True)
def tick():
try:
img = shot(); w = min(img.width, 1280); img = img.resize((w, img.height * w // img.width))
label.photo = ImageTk.PhotoImage(img); label.config(image=label.photo, text="")
except Exception as e:
label.config(image="", text=f"no screen: {e}", width=60, height=10)
root.after(every, tick)
tick(); root.mainloop()
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
# Wake the VM from sleep (suspend to RAM), e.g. after the TV's power button
# in the HDMI-CEC test. Uses the QMP socket run.sh opens in the VM directory.
# Env: VM_DIR (the repo if it holds disk.qcow2, else ~/vms/cachyos-test).
set -euo pipefail
here="$(cd "$(dirname "$0")/.." && pwd)"
VM_DIR="${VM_DIR:-$([[ -f "$here/disk.qcow2" ]] && echo "$here" || echo "$HOME/vms/cachyos-test")}"
sock="$VM_DIR/qmp.sock"
[[ -S "$sock" ]] || { echo "No QMP socket at $sock (VM not running, or started before run.sh had -qmp)." >&2; exit 1; }
printf '%s\n' '{"execute":"qmp_capabilities"}' '{"execute":"system_wakeup"}' |
socat -t 2 - UNIX-CONNECT:"$sock" 2>/dev/null | grep -c '"return"' >/dev/null && echo "Wake-up sent."
+25
View File
@@ -0,0 +1,25 @@
#!/bin/bash
# Guest side: after a reboot, the running kernel has Steamify's modules.
pass() { echo "PASS $*"; }; fail() { echo "FAIL $*"; }
k=$(uname -r)
echo "kernel: $k"; echo "cmdline: $(cut -c1-140 /proc/cmdline)"
compgen -G "/usr/lib/modules/$k/updates/dkms/steamify-fremont-poweroff.ko*" >/dev/null && pass "power-off fix built for the running kernel" || fail "no power-off fix for $k"
# Loaded (a real Steam Machine), or tried at boot and refused with "No such device" (the VM has no AMD GPIO
# controller, so the module returns -ENODEV silently). The refusal is in the journal; it only sometimes
# reaches dmesg, which is what this used to grep.
if lsmod | grep -q '^steamify_fremont_poweroff'; then pass "power-off fix loaded at boot"
elif sudo journalctl -b -q --no-pager -u systemd-modules-load | grep -q "steamify_fremont_poweroff.*No such device"; then
pass "power-off fix tried at boot (No such device: no AMD GPIO controller in the VM)"
else fail "power-off fix not loaded, and not tried at boot"; fi
lsmod | grep -q '^leds_valve' && pass "leds_valve loaded" || fail "leds_valve not loaded"
[[ -z "$(systemctl --failed --no-legend)" ]] && pass "no failed units" || { fail "failed units:"; systemctl --failed --no-legend; }
# B1: booted through the loader's own EFI boot entry, not the firmware's automatic disk entry (the
# fallback path \EFI\BOOT\BOOTX64.EFI) and not an entry without a partition (HD(0,GPT,0000...)).
cur=$(sudo efibootmgr | sed -n 's/^BootCurrent: //p')
entry=$(sudo efibootmgr -v | grep -a "^Boot$cur")
if [[ -n "$cur" && "$entry" != *auto_created_boot_option* && "$entry" =~ HD\(([0-9]+),GPT,([0-9a-fA-F-]+) \
&& "${BASH_REMATCH[1]}" != 0 && "${BASH_REMATCH[2]}" != 00000000-0000-* ]]; then
pass "booted through its own EFI entry: $(cut -f1 <<< "$entry" | cut -c1-60)"
else
fail "not booted through its own EFI entry (BootCurrent $cur: $(cut -c1-120 <<< "$entry"))"
fi
+28
View File
@@ -0,0 +1,28 @@
#!/bin/bash
# Guest side: reinstall both kernels + headers (what an update does: the
# DKMS hook rebuilds the modules, mkinitcpio and the boot loader's hook run).
pass() { echo "PASS $*"; }; fail() { echo "FAIL $*"; }
start=$(date +%s)
sudo pacman -S --noconfirm linux-cachyos linux-cachyos-lts linux-cachyos-headers linux-cachyos-lts-headers > /tmp/kernel-update.log 2>&1
rc=$?
sed 's/\x1b\[[0-9;]*m//g' /tmp/kernel-update.log | grep -E "^==> dkms|Building image|Generating grub|sdboot|limine|error" | cut -c1-140
[[ $rc -eq 0 ]] && pass "pacman kernel reinstall exit 0" || fail "pacman exit $rc"
for k in $(ls /usr/lib/modules); do
for m in cros-ec-cec leds-valve steamify-fremont-poweroff; do
compgen -G "/usr/lib/modules/$k/updates/dkms/$m.ko*" >/dev/null || fail "$m not rebuilt for $k"
done
done
[[ $(dkms status | grep -c ': installed') -eq $((3 * $(ls /usr/lib/modules | wc -w))) ]] && pass "DKMS rebuilt 3 modules for each kernel" || fail "DKMS status after the update"
# The images: /boot/initramfs-*.img (GRUB, systemd-boot); Limine keeps them per kernel
# under /boot/<machine-id>/<kernel>/initramfs (limine-mkinitcpio) and rewrites limine.conf.
if [[ "$LOADER" == limine ]]; then
# limine-entry-tool copies an image only when its content changed (same version = untouched):
# the hook ran when both images were built and limine.conf was regenerated.
n=$(sed 's/\x1b\[[0-9;]*m//g' /tmp/kernel-update.log | grep -c 'Creating zstd-compressed initcpio image')
[[ $n -ge 2 ]] && pass "Limine's hook built the initramfs for both kernels" || fail "Limine's hook built $n initramfs image(s)"
[[ $(sudo stat -c %Y /boot/limine.conf) -ge $start ]] && pass "limine.conf regenerated" || fail "limine.conf not regenerated"
else
for img in /boot/initramfs-linux-cachyos.img /boot/initramfs-linux-cachyos-lts.img; do
[[ $(sudo stat -c %Y "$img") -ge $start ]] && pass "$(basename "$img") rebuilt" || fail "$(basename "$img") not rebuilt"
done
fi
+28
View File
@@ -0,0 +1,28 @@
#!/bin/bash
# Guest side: Steamify's boot-loader-specific code, hdmi_remove_boot_param
# (older versions put drm.edid_firmware= on the kernel command line): seed
# the parameter where this loader keeps it, run the removal, and check that
# it's gone and the boot entries are rebuilt.
pass() { echo "PASS $*"; }; fail() { echo "FAIL $*"; }
sudo mountpoint -q /mnt || sudo mount -t 9p -o trans=virtio,version=9p2000.L repo /mnt
cd /mnt || exit 1
. lib/common.sh; . lib/state.sh; . lib/hdmi-refresh.sh
f="$(hdmi_boot_file)"
echo "boot file: ${f:-none}"
[[ -n "$f" ]] && pass "hdmi_boot_file finds the loader's file" || { fail "hdmi_boot_file finds nothing"; exit 0; }
p='drm.edid_firmware=DP-1:edid/steamify-test.bin'
case "$f" in
/etc/default/grub) sudo sed -i -E "s|^(GRUB_CMDLINE_LINUX_DEFAULT=\")|\1$p |" "$f" ;;
/etc/sdboot-manage.conf) if grep -q '^LINUX_OPTIONS=' "$f"; then sudo sed -i -E "s|^(LINUX_OPTIONS=\")|\1$p |" "$f"; else echo "LINUX_OPTIONS=\"$p\"" | sudo tee -a "$f" >/dev/null; fi ;;
/etc/default/limine) if grep -q '^KERNEL_CMDLINE\[default\]' "$f"; then sudo sed -i -E "s|^(KERNEL_CMDLINE\[default\]\+?=\")|\1$p |" "$f"; else echo "KERNEL_CMDLINE[default]+=\"$p\"" | sudo tee -a "$f" >/dev/null; fi ;;
esac
[[ -n "$(hdmi_cmdline_param)" ]] && pass "the seeded parameter is detected" || fail "seeded parameter not detected in $f"
hdmi_remove_boot_param > /tmp/legacy-param.log 2>&1; rc=$?
tail -3 /tmp/legacy-param.log | sed 's/\x1b\[[0-9;]*m//g' | cut -c1-120
[[ $rc -eq 0 ]] && pass "hdmi_remove_boot_param exit 0" || fail "hdmi_remove_boot_param exit $rc"
[[ -z "$(hdmi_cmdline_param)" ]] && pass "parameter removed from $f" || fail "parameter still in $f"
case "$f" in
/etc/default/grub) sudo grep -q edid /boot/grub/grub.cfg && fail "edid still in grub.cfg" || pass "no edid in grub.cfg"; [[ $(sudo grep -c '^\s*linux\s' /boot/grub/grub.cfg) -ge 1 ]] && pass "grub.cfg has linux entries" || fail "grub.cfg has no linux entries" ;;
/etc/sdboot-manage.conf) sudo sh -c 'grep -l edid /boot/loader/entries/* 2>/dev/null' | grep -q . && fail "edid still in the entries" || pass "no edid in the entries"; [[ $(sudo ls /boot/loader/entries | wc -l) -ge 1 ]] && pass "boot entries exist" || fail "no boot entries" ;;
/etc/default/limine) sudo grep -q edid /boot/limine.conf && fail "edid still in limine.conf" || pass "no edid in limine.conf"; sudo grep -q 'linux-cachyos' /boot/limine.conf && pass "limine.conf lists the kernels" || fail "limine.conf lists no kernel" ;;
esac
+15
View File
@@ -0,0 +1,15 @@
#!/bin/bash
# Guest side: Steamify's DKMS modules exist for every installed kernel.
# Prints PASS/FAIL lines (scripts/vmbootloadertest.sh counts the FAILs).
pass() { echo "PASS $*"; }; fail() { echo "FAIL $*"; }
kernels=$(ls /usr/lib/modules)
n=$(wc -w <<< "$kernels")
echo "kernels: $(tr '\n' ' ' <<< "$kernels")"
dkms status
[[ $(dkms status | grep -c ': installed') -eq $((3 * n)) ]] && pass "3 DKMS modules x $n kernels installed" || fail "dkms status is not 3 modules x $n kernels"
for k in $kernels; do
for m in cros-ec-cec leds-valve steamify-fremont-poweroff; do
compgen -G "/usr/lib/modules/$k/updates/dkms/$m.ko*" >/dev/null && pass "$m for $k" || fail "$m missing for $k"
done
done
[[ -f /etc/modules-load.d/steamify-fremont-poweroff.conf ]] && pass "modules-load.d has the power-off fix" || fail "no modules-load.d entry"
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
# Guest side ($LOADER = limine|systemd-boot|grub): Steamify's state as its own
# menu detects it (the same *_status functions), the OS name untouched, and
# the boot loader as expected.
pass() { echo "PASS $*"; }; fail() { echo "FAIL $*"; }
sudo mountpoint -q /mnt || sudo mount -t 9p -o trans=virtio,version=9p2000.L repo /mnt
cd /mnt || exit 1
for lib in common state packages login-manager single-user steam-desktop steam-machine fremont-poweroff vram-booster hdmi-refresh cec boot-session vapor-theme steamos-extras bios desktop-shortcut wizard-shortcut steam-game update-notifier; do
# shellcheck source=/dev/null
. "lib/$lib.sh" 2>/dev/null
done
# Items this Steam Machine VM has on after `--options gaming,theme,glyphs,single,launcher,notify,cec,machine,poweroff`.
for item in gaming theme glyphs single launcher notify poweroff cec; do
if "${item}_status" 2>/dev/null; then pass "$item is on (the menu shows it ticked)"; else fail "$item is off"; fi
done
# Steamify never renames the OS (limine-snapper-sync / grub-btrfs read it).
grep -qx 'NAME="CachyOS Linux"' /etc/os-release && grep -qx 'PRETTY_NAME="CachyOS"' /etc/os-release && pass "os-release NAME and PRETTY_NAME are CachyOS's" || fail "os-release NAME/PRETTY_NAME changed"
grep -q 'with Steamify' /etc/lsb-release && fail "lsb-release still says 'with Steamify'" || pass "lsb-release name is CachyOS's"
# The boot loader is the one this VM was installed with.
case "$LOADER" in
limine) command -v limine-update >/dev/null && [[ -f /etc/default/limine ]] && pass "Limine is installed" || fail "no Limine"
grep -q '^TARGET_OS_NAME="CachyOS"' /etc/default/limine && pass "limine TARGET_OS_NAME is set" || fail "TARGET_OS_NAME missing in /etc/default/limine" ;;
systemd-boot) sudo bootctl status --no-pager 2>/dev/null | grep -q 'Product: systemd-boot' && pass "systemd-boot is the boot loader" || fail "not systemd-boot" ;;
grub) sudo bootctl status --no-pager 2>/dev/null | grep -q 'Product: GRUB' && pass "GRUB is the boot loader" || fail "not GRUB" ;;
esac
# "BIOS" menu item: rebooting into the firmware setup must be possible (never trigger it here).
sudo bootctl status --no-pager 2>/dev/null | grep -q 'Boot into FW: supported' && pass "reboot into firmware setup is supported" || fail "firmware setup reboot not supported"
+93
View File
@@ -0,0 +1,93 @@
#!/bin/bash
# Runs in the live ISO as root (started by the systemd.run= boot parameter
# that scripts/vminstall.sh passes): installs CachyOS unattended with its
# headless installer (settings.json from the host), then makes the new
# system a test VM (SSH key, passwordless sudo, test autologin), and powers
# off. Everything it needs comes from the host's vminstall-server.py; the log
# and the result go back there (PUT log / status).
set -uo pipefail
H="http://@HOST_IP@:@PORT@"
LOG=/tmp/vminstall.log
exec > >(tee -a "$LOG") 2>&1
up() { curl -fsS -T "$LOG" "$H/install.log" >/dev/null 2>&1; }
finish() { echo "== $1"; up; curl -fsS -X PUT --data "$1" "$H/status" >/dev/null 2>&1; }
fail() { finish "failed: $*"; exit 1; }
echo "== Steamify test VM: unattended install ($(date))"
curl -fsS "$H/vminstall.env" -o /tmp/vminstall.env || fail "no vminstall.env from the host"
# shellcheck disable=SC1091
. /tmp/vminstall.env
curl -fsS "$H/settings.json" -o /tmp/settings.json || fail "no settings.json from the host"
curl -fsS "$H/vminstall-post.sh" -o /tmp/vminstall-post.sh || fail "no vminstall-post.sh from the host"
# Let whoever watches the VM's window follow along: a Konsole on the live
# desktop that shows this log.
(
for _ in $(seq 100); do pgrep -u liveuser -x plasmashell >/dev/null && break; sleep 3; done
sleep 5
runuser -u liveuser -- env XDG_RUNTIME_DIR=/run/user/1000 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus \
systemd-run --user -q konsole --hold -e tail -n +1 -f "$LOG"
) &
while sleep 20; do up; done &
# No `systemctl is-system-running --wait`: this script is itself the running
# kernel-command-line.service job, so the boot never finishes while it waits.
# The live keyring must exist before pacman-key can sign (the install failed with
# "no secret key available to sign with" when this ran first).
for _ in $(seq 120); do systemctl is-active --quiet pacman-init.service && break; sleep 2; done
nm-online -t 180 >/dev/null || fail "no network"
# The live ISO's mirror list and keys can be old; the installer's pacstrap needs current ones.
pacman -Sy --noconfirm archlinux-keyring cachyos-keyring || echo "(keyring update failed, going on)"
# The host's package cache (VM_CACHE, 9p tag `cache`) as pacman's cache: the
# installer's pacstrap -c uses the live system's, so a second install downloads nothing.
CACHE=/var/cache/steamify-pkg
if mkdir -p $CACHE && mount -t 9p -o trans=virtio,version=9p2000.L cache $CACHE 2>/dev/null; then
mkdir -p $CACHE/pkg && mount --bind $CACHE/pkg /var/cache/pacman/pkg &&
echo "== package cache: $(ls $CACHE/pkg | wc -l) files from the host"
else
CACHE=""; echo "== no package cache (VM_CACHE off)"
fi
echo "== cachyos-installer"
cachyos-installer --config /tmp/settings.json || fail "cachyos-installer exited with $?"
# The installer leaves the new system mounted on /mnt, or not: mount it.
if ! mountpoint -q /mnt; then
mount -o subvol=@ "${DEVICE}2" /mnt || fail "can't mount the new system"
mount -o subvol=@home "${DEVICE}2" /mnt/home 2>/dev/null
mount "${DEVICE}1" /mnt/boot 2>/dev/null
fi
[[ -d "/mnt/home/$VM_USER" ]] || fail "the installer created no /home/$VM_USER"
# Steamify's packages (Steam, ...) install inside the new system: same cache.
if [[ -n "$CACHE" ]]; then
mkdir -p /mnt/var/cache/pacman/pkg && mount --bind $CACHE/pkg /mnt/var/cache/pacman/pkg || echo "(no cache in the new system)"
# Steamify's own downloads (the CEC driver: GitHub rate-limits it)
mkdir -p $CACHE/steamify /mnt/var/cache/steamify && mount --bind $CACHE/steamify /mnt/var/cache/steamify || true
fi
# What Calamares does after its users step (shellprocess_steamify): Steamify's
# setup for the new user. The headless installer skips it, so a Steamify ISO
# would otherwise leave plain CachyOS. VM_STEAMIFY: the page's choice (ids,
# comma-separated; empty = its default, everything on; none = skip).
if [[ -x /usr/local/bin/steamify-install && "${VM_STEAMIFY:-}" != skip ]]; then
echo "== steamify-install (${VM_STEAMIFY:-defaults})"
/usr/local/bin/steamify-install /mnt "$VM_USER" "${VM_STEAMIFY:-}"
cat "/mnt/var/log/steamify-install.log" 2>/dev/null
grep -q '^exit: 0' /mnt/var/log/steamify-install.log 2>/dev/null || fail "steamify-install did not finish cleanly"
else
echo "== no steamify-install on this ISO (or VM_STEAMIFY=skip): plain CachyOS"
fi
mountpoint -q /mnt/var/cache/pacman/pkg && umount /mnt/var/cache/pacman/pkg
echo "== test VM setup"
cp /tmp/vminstall-post.sh /tmp/vminstall.env /mnt/root/
arch-chroot /mnt bash /root/vminstall-post.sh || fail "test VM setup exited with $?"
rm -f /mnt/root/vminstall-post.sh /mnt/root/vminstall.env
cp "$LOG" /mnt/var/log/vminstall.log
sync
finish ok
sleep 2
systemctl poweroff
+64
View File
@@ -0,0 +1,64 @@
#!/bin/bash
# Runs inside the freshly installed system (arch-chroot, from
# vminstall-live.sh): what guest-ssh-setup.sh and the old manual steps did,
# so the VM is ssh-ready without touching it: sshd with the host's key,
# passwordless sudo, the test autologin into Plasma, tmux and shellcheck.
set -euo pipefail
# shellcheck disable=SC1091
. /root/vminstall.env
echo "$VM_HOSTNAME" > /etc/hostname
pacman -S --needed --noconfirm openssh tmux shellcheck
systemctl enable sshd
home="/home/$VM_USER"
install -d -m 700 -o "$VM_USER" -g "$VM_USER" "$home/.ssh"
printf '%s\n' "$VM_PUBKEY" > "$home/.ssh/authorized_keys"
chown "$VM_USER:$VM_USER" "$home/.ssh/authorized_keys"
chmod 600 "$home/.ssh/authorized_keys"
# Test VM only: the host drives it without a password.
echo "$VM_USER ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/99-test-vm
chmod 440 /etc/sudoers.d/99-test-vm
# Log straight into Plasma (gamescope doesn't render in the VM). The
# scripts expect plasma-login-manager, like a CachyOS install from Calamares.
# Not when Steamify's conversion is on (steamify-install ran on a Steamify
# ISO): it made SDDM boot into gamescope, and that is what gets tested; sshd
# doesn't need a desktop.
if [[ -f /etc/sddm.conf.d/10-gamescope-autologin.conf ]]; then
echo "Steamify's gaming-mode login (SDDM) kept."
else
pacman -Q plasma-login-manager >/dev/null 2>&1 || pacman -S --needed --noconfirm plasma-login-manager
systemctl disable sddm.service 2>/dev/null || true
systemctl enable plasmalogin.service
mkdir -p /etc/plasmalogin.conf.d
printf '[Autologin]\nUser=%s\nSession=plasma.desktop\n' "$VM_USER" > /etc/plasmalogin.conf.d/00-test-autologin.conf
fi
# cachyos-installer's limine.conf has no default_entry: entry 1 is the
# "CachyOS" folder, so the first boot waits in the menu. 2 is its first
# kernel (linux-cachyos); limine-update keeps the line.
if [[ -f /boot/limine.conf ]] && ! grep -q '^default_entry:' /boot/limine.conf; then
sed -i 's/^timeout: .*/&\ndefault_entry: 2/' /boot/limine.conf
fi
# cachyos-installer's systemd-boot loader.conf has `#timeout 3` commented out and
# no default: the first boot would wait in the menu for ever. Boot the first
# kernel after 3 s.
if [[ -f /boot/loader/loader.conf ]] && ! grep -q '^timeout' /boot/loader/loader.conf; then
printf 'timeout 3\ndefault linux-cachyos.conf\n' >> /boot/loader/loader.conf
fi
# The installer enables ufw, which drops the host's ssh connections (found
# in the kernel log: [UFW BLOCK] DPT=22). Let ssh in; the rest stays as installed.
if command -v ufw >/dev/null; then
ufw allow 22/tcp || echo "(ufw rule failed: ssh may be blocked)"
fi
# English everywhere, whatever the live ISO picked.
echo 'LANG=en_US.UTF-8' > /etc/locale.conf
# mirror5.krfoss.org served broken signatures; skip it (vmreset.sh does too).
for f in /etc/pacman.d/*mirrorlist*; do sed -i '/krfoss/s/^Server/#Server/' "$f"; done
echo "Test VM setup done: $VM_USER@$VM_HOSTNAME"
+41
View File
@@ -0,0 +1,41 @@
# G1-G3 (--defaults --list), F1, F2, F6 (bad arguments): read-only, nothing may change.
before=$(ls -R ~/.local/state/steamify 2>/dev/null | md5sum)
rejects() { # rejects <expected message> <args...>: exit 1 and the message
local want="$1"; shift
out=$(bash steamify.sh "$@" 2>&1 < /dev/null); rc=$?
[[ $rc -eq 1 ]] && grep -q "$want" <<< "$out" && pass "steamify.sh $* -> exit 1 ($want)" || fail "steamify.sh $* -> exit $rc: $(sed 's/\x1b\[[0-9;]*m//g' <<< "$out" | grep -m1 -E 'ERROR|rror')"
}
# F1
rejects "needs a value" --defaults --options
rejects "Unknown item" --defaults --options bogus
rejects "not an item" --defaults --options boot
rejects "can't be part of the install" --defaults --options bios
rejects "needs a value" --defaults --boot
rejects "gamescope or desktop" --defaults --boot sideways
rejects "Unknown option" --defaults --skip x
# F2
rejects "needs the SteamOS conversion" --defaults --options theme --boot desktop
rejects "needs the SteamOS conversion" --defaults --boot desktop --options theme
# F6
rejects "needs the SteamOS conversion" --boot desktop
# G1: valid JSON, exit 0, no stderr
out=$(bash steamify.sh --defaults --list 2>/tmp/list.err); rc=$?
[[ $rc -eq 0 ]] && pass "G1 --list exit 0" || fail "G1 --list exit $rc"
[[ ! -s /tmp/list.err ]] && pass "G1 --list prints nothing on stderr" || fail "G1 --list stderr: $(head -c 100 /tmp/list.err)"
python3 - "$out" <<'PY'
import json, sys
def r(ok, msg): print(("PASS " if ok else "FAIL ") + msg)
try:
d = json.loads(sys.argv[1]); r(isinstance(d, list) and d, "G1 --list is a JSON array")
except Exception as e:
r(False, f"G1 --list is not JSON: {e}"); sys.exit()
ids = {i.get("id"): i for i in d}
# G2: actions and retired items are not offered; poweroff needs machine, boot needs gaming
r(not ({"bios", "kpin", "hdmi"} & set(ids)), "G2 bios, kpin and hdmi (actions, retired) are left out")
r(not ({"vram", "steamgame"} & set(ids)), "G2 vram and steamgame (unavailable here) are left out")
r("machine" in str(ids.get("poweroff", {}).get("parent", "")), "G2 poweroff has the parent machine")
r("gaming" in str(ids.get("boot", {}).get("parent", "")), "G2 boot has the parent gaming")
PY
# G3 and nothing changed: the errors and --list must not touch the state
after=$(ls -R ~/.local/state/steamify 2>/dev/null | md5sum)
[[ "$before" == "$after" ]] && pass "F1/F2/F6/G1 changed nothing" || fail "the state directory changed"
+8
View File
@@ -0,0 +1,8 @@
# F3: every item and --boot desktop: all on, the boot-desktop unit enabled, VRAM left out with a warning.
info "applying Steamify (installs packages, builds kernel modules: a few minutes)..."
out=$(sf --defaults --options gaming,theme,glyphs,single,launcher,notify,vram,cec,machine,poweroff --boot desktop); rc=$?
[[ $rc -eq 0 ]] && pass "F3 exit 0" || fail "F3 exit $rc"
expect_on gaming theme glyphs single launcher notify machine poweroff cec
grep -qi "VRAM booster" <<< "$out" && pass "F3 VRAM booster left out with a warning" || fail "F3 no VRAM warning"
is vram && fail "F3 vram is on (the VM has no dmem region)" || pass "F3 vram stays off"
systemctl is-enabled steamify-boot-desktop.service >/dev/null 2>&1 && pass "F3 boot-desktop unit enabled" || fail "F3 boot-desktop unit not enabled"
+7
View File
@@ -0,0 +1,7 @@
# F4: --options theme,single,machine --boot desktop: single brings the conversion; the rest stays off.
info "applying Steamify (installs packages, builds kernel modules: a few minutes)..."
sf --defaults --options theme,single,machine --boot desktop >/dev/null; rc=$?
[[ $rc -eq 0 ]] && pass "F4 exit 0" || fail "F4 exit $rc"
expect_on gaming theme single machine
expect_off poweroff cec glyphs launcher notify
systemctl is-enabled steamify-boot-desktop.service >/dev/null 2>&1 && pass "F4 boot-desktop unit enabled" || fail "F4 boot-desktop unit not enabled"
+8
View File
@@ -0,0 +1,8 @@
# F5: --options launcher,vram,poweroff: poweroff brings Steam Machine support; conversion off, plasmalogin unchanged.
dm_before=$(current_display_manager)
info "applying Steamify (installs packages, builds kernel modules: a few minutes)..."
sf --defaults --options launcher,vram,poweroff >/dev/null; rc=$?
[[ $rc -eq 0 ]] && pass "F5 exit 0" || fail "F5 exit $rc"
expect_on machine poweroff launcher
expect_off gaming
[[ "$(current_display_manager)" == "$dm_before" ]] && pass "F5 display manager unchanged ($dm_before)" || fail "F5 display manager changed: $dm_before -> $(current_display_manager)"
+13
View File
@@ -0,0 +1,13 @@
# F7: on an install, --boot desktop, again, --boot gamescope: only Boot into changes; the menu ticks stay.
info "applying Steamify (installs packages, builds kernel modules: a few minutes)..."
sf --defaults --options gaming,theme,single,launcher,notify,machine,poweroff >/dev/null
base=$(ticks | sed 's/boot //')
sf --boot desktop >/dev/null; rc=$?
[[ $rc -eq 0 ]] && pass "F7 --boot desktop exit 0" || fail "F7 --boot desktop exit $rc"
systemctl is-enabled steamify-boot-desktop.service >/dev/null 2>&1 && pass "F7 boot-desktop unit enabled" || fail "F7 unit not enabled after --boot desktop"
[[ "$(ticks | sed 's/boot //')" == "$base" ]] && pass "F7 the menu ticks are unchanged" || fail "F7 ticks changed: [$base] -> [$(ticks)]"
out=$(sf --boot desktop); grep -qi "already" <<< "$out" && pass "F7 second --boot desktop says already" || fail "F7 no 'Already starting in desktop': $(tail -n 2 <<< "$out" | tr '\n' ' ')"
sf --boot gamescope >/dev/null; rc=$?
[[ $rc -eq 0 ]] && pass "F7 --boot gamescope exit 0" || fail "F7 --boot gamescope exit $rc"
systemctl is-enabled steamify-boot-desktop.service >/dev/null 2>&1 && fail "F7 unit still enabled after --boot gamescope" || pass "F7 boot-desktop unit disabled again"
[[ "$(ticks | sed 's/boot //')" == "$base" ]] && pass "F7 ticks unchanged after --boot gamescope" || fail "F7 ticks changed after gamescope"
+16
View File
@@ -0,0 +1,16 @@
# H1 (LED driver) and H5 (power-off module) with the faked Steam Machine.
info "applying Steamify (installs packages, builds kernel modules: a few minutes)..."
sf --defaults --options gaming,theme,glyphs,single,launcher,notify,cec,machine,poweroff >/dev/null; rc=$?
[[ $rc -eq 0 ]] && pass "Steamify applied (exit 0)" || fail "Steamify exit $rc"
# H1
lsmod | grep -q '^leds_valve' && pass "H1 leds_valve loaded" || fail "H1 leds_valve not loaded"
n=$(compgen -G "/sys/class/leds/valve-leds*" | wc -l); [[ $n -eq 17 ]] && pass "H1 17 valve-leds nodes" || fail "H1 $n valve-leds nodes"
own=$(stat -c %U /sys/class/leds/valve-leds*/brightness 2>/dev/null | sort -u | tr '\n' ' '); [[ "$own" == "$(id -un) " ]] && pass "H1 the nodes belong to the user" || fail "H1 node owner: $own"
# H5
for k in $(ls /usr/lib/modules); do
compgen -G "/usr/lib/modules/$k/updates/dkms/steamify-fremont-poweroff.ko*" >/dev/null && pass "H5 power-off module built for $k" || fail "H5 no power-off module for $k"
done
sudo modprobe -r steamify_fremont_poweroff 2>/dev/null
out=$(sudo modprobe steamify_fremont_poweroff 2>&1); rc=$?
# in the VM there is no GPIO wake bit: it loads, or answers "No such device"
[[ $rc -eq 0 ]] || grep -q "No such device" <<< "$out" && pass "H5 the module loads (or: No such device, no GPIO wake bit)" || fail "H5 modprobe: $out"
+10
View File
@@ -0,0 +1,10 @@
# H4: Boot into desktop, reboot: Plasma straight after boot; Boot into gaming: SDDM autologin into gamescope (session file).
conf=/etc/sddm.conf.d/10-gamescope-autologin.conf
info "applying Steamify (a few minutes)..."
guest 'sf --defaults --options gaming,theme,single,machine --boot gamescope >/dev/null; echo done' | tail -n 1 | grep -q done && pass "H4 conversion applied" || fail "H4 apply"
guest "grep -h '^Session=' $conf" | grep -qi gamescope && pass "H4 gaming: the SDDM autologin session is gamescope" || fail "H4 gaming session: $(guest "grep -h Session= $conf")"
guest 'sf --boot desktop >/dev/null; echo done' | tail -n 1 | grep -q done && pass "H4 --boot desktop" || fail "H4 --boot desktop"
reboot_guest && pass "H4 the VM is back after the reboot" || { fail "H4 no ssh after the reboot"; return 0 2>/dev/null || exit 0; }
up=""; for _ in $(seq 40); do vm_ssh "pgrep -u $VM_USER -x plasmashell" >/dev/null 2>&1 && { up=1; break; }; sleep 3; done
[[ -n "$up" ]] && pass "H4 Plasma straight after the boot (boot into desktop)" || fail "H4 no plasmashell after the reboot"
guest 'sf --boot gamescope >/dev/null; echo done' | tail -n 1 | grep -q done && pass "H4 --boot gamescope" || fail "H4 --boot gamescope"
+4
View File
@@ -0,0 +1,4 @@
# R1.7 / H2: HDMI-CEC with vivid (fake TV and remote): scripts/vmcec.sh prints PASS/FAIL/SKIP (2 SKIP in the VM: wake from TV, sleep).
info "applying Steamify (a few minutes)..."
guest 'sf --defaults --options gaming,theme,glyphs,single,launcher,notify,cec,machine,poweroff >/dev/null; sudo modprobe vivid; echo done' | tail -n 1 | grep -q done && pass "CEC: Steamify applied, vivid loaded" || fail "CEC setup"
"$repo/scripts/vmcec.sh" --no-sleep 2>&1 | sed 's/\x1b\[[0-9;]*m//g' | grep -E '^(PASS|FAIL|SKIP)|PASS|FAIL' | sed -E 's/^[^A-Z]*(PASS|FAIL|SKIP)/\1 CEC:/' | cut -c1-140
+5
View File
@@ -0,0 +1,5 @@
# H3: BIOS update, the whole flow with WIZARD_BIOS_DRY_RUN=1 (never flashes). The VM reports an older BIOS.
# env: BIOS_VERSION=F7F0107
out=$(printf '11\n\ny\ny\nUPDATE\nm\nq\nn\n' | WIZARD_BIOS_DRY_RUN=1 bash /mnt/steamify.sh 2>&1 | sed 's/\x1b\[[0-9;]*m//g')
grep -qi "dry run" <<< "$out" && pass "H3 the dry run went through" || fail "H3 no dry-run message: $(tail -n 3 <<< "$out" | tr '\n' ' ' | cut -c1-160)"
grep -q '^\[ERROR\]' <<< "$out" && fail "H3 errors: $(grep -B4 -m1 '^\[ERROR\]' <<< "$out" | tr '\n' '|' | cut -c1-400)" || pass "H3 no errors"
+30
View File
@@ -0,0 +1,30 @@
# R1.7 (CEC driver cache): Valve's CEC driver is downloaded once and kept in /var/cache/steamify, named after
# its checksum; a new pin downloads its own file and removes the old one, a damaged file is replaced.
# Offline: a private cache folder, and the host's cached copy as the "download" (file://), so GitHub's rate
# limit can't fail this block. Needs the host cache (vmsuite.sh binds ~/vms/pkg-cache/steamify).
real="/var/cache/steamify/cros-ec-cec-${CEC_DRIVER_SHA256:0:12}.c"
[[ -f "$real" ]] || { skip "R1.7 cache: no cached CEC driver on the host (~/vms/pkg-cache/steamify)"; exit 0; }
dir=$(mktemp -d); sudo chown root: "$dir"; sudo chmod 755 "$dir" # like /var/cache/steamify: the user checks the cache
export CEC_DRIVER_URL="file://$real" CEC_DRIVER_CACHE="$dir/cros-ec-cec-${CEC_DRIVER_SHA256:0:12}.c"
errs=""
run() { out=$(cec_driver_enable 2>&1 | sed 's/\x1b\[[0-9;]*m//g'); errs+=$(grep -B6 '^\[ERROR\]' <<< "$out" | tr '\n' '|'); } # sets out
# 1. new pin: an older pin's file is there, this one isn't
sudo touch "$dir/cros-ec-cec-000000000000.c"
run
grep -q "Downloading Valve" <<< "$out" && pass "R1.7 cache: a new pin downloads its driver" || fail "R1.7 cache: no download for a new pin"
echo "$CEC_DRIVER_SHA256 $CEC_DRIVER_CACHE" | sha256sum -c --quiet - 2>/dev/null && pass "R1.7 cache: the driver is kept, checksum OK" || fail "R1.7 cache: not kept after the download"
[[ ! -e "$dir/cros-ec-cec-000000000000.c" ]] && pass "R1.7 cache: the older pin's file is removed" || fail "R1.7 cache: the older pin's file is still there"
[[ -d "$CEC_DKMS_SRC" ]] && pass "R1.7 cache: the driver is in DKMS" || fail "R1.7 cache: no DKMS source"
# 2. cache hit: no download
run
grep -q "Downloading Valve" <<< "$out" && fail "R1.7 cache: downloaded again although it's cached" || pass "R1.7 cache: the cached driver is used, no download"
# 3. a damaged file under the right name: downloaded again and replaced
echo broken | sudo tee "$CEC_DRIVER_CACHE" >/dev/null
run
grep -q "Downloading Valve" <<< "$out" && echo "$CEC_DRIVER_SHA256 $CEC_DRIVER_CACHE" | sha256sum -c --quiet - 2>/dev/null &&
pass "R1.7 cache: a damaged cached driver is replaced" || fail "R1.7 cache: a damaged cached driver stays"
[[ -z "$errs" ]] && pass "R1.7 cache: no errors in the three runs" || fail "R1.7 cache: errors: $(cut -c1-600 <<< "$errs")"
sudo rm -rf "$dir"
+7
View File
@@ -0,0 +1,7 @@
# R2.1: the ISO's installer step: a new user who never logged in gets plain `--defaults` (scripts/vminstallsim.sh).
info "installer simulation: a new user, --defaults (a few minutes)..."
out=$("$repo/scripts/vminstallsim.sh" --fresh 2>&1 | sed 's/\x1b\[[0-9;]*m//g')
grep -q '^exit: 0' <<< "$out" && pass "R2.1 exit 0" || fail "R2.1 $(grep -m1 '^exit' <<< "$out")"
grep -q '^\[ERROR\]' <<< "$out" && fail "R2.1 errors: $(grep -m1 '^\[ERROR\]' <<< "$out")" || pass "R2.1 no errors"
[[ $(grep -c '^Turning on' <<< "$out") -ge 5 ]] && pass "R2.1 every default item turned on ($(grep -c '^Turning on' <<< "$out"))" || fail "R2.1 only $(grep -c '^Turning on' <<< "$out") items turned on"
vm_ssh 'sudo ls /home/isotest/.config/autostart/ 2>/dev/null' | grep -qi steamify && pass "R2.1 the first-login autostart is in place" || fail "R2.1 no first-login autostart"
+43
View File
@@ -0,0 +1,43 @@
# R1.1-R1.6, R1.10: the menu on an existing desktop, driven with scripted input (each row builds on the last).
menu() { printf "$1" | bash /mnt/steamify.sh 2>&1 | sed 's/\x1b\[[0-9;]*m//g'; }
# R1.1: first-run ticks: everything but Boot into desktop and BIOS
out=$(menu 'q\n')
for n in 1 3 4 5 6 7 8 9 10; do grep -qE "^ +$n +[a-z-]+ +\[x\]" <<< "$out" && pass "R1.1 row $n ticked" || fail "R1.1 row $n not ticked"; done
grep -qE "^ +11 .*\[ \]" <<< "$out" && pass "R1.1 Update BIOS not ticked" || fail "R1.1 Update BIOS ticked"
grep -q "Boot into: \[gamescope\]" <<< "$out" && pass "R1.1 boots into gamescope by default" || fail "R1.1 boot default is not gamescope"
# R1.2: everything OK, back in the menu with all on
info "applying Steamify (installs packages, builds kernel modules: a few minutes)..."
out=$(menu '\ny\nm\nq\nn\n')
grep -q '^\[ERROR\]' <<< "$out" && fail "R1.2 errors: $(grep -m1 '^\[ERROR\]' <<< "$out")" || pass "R1.2 no errors"
grep -q "^\[OK\]" <<< "$out" && pass "R1.2 applied (the run printed [OK] lines)" || fail "R1.2 printed no [OK] line"
expect_on gaming theme glyphs single launcher notify cec machine poweroff
# R1.3: the state (what vmstate.sh prints)
[[ "$(current_display_manager)" == sddm ]] && [[ -f /etc/sddm.conf.d/10-gamescope-autologin.conf ]] && pass "R1.3 SDDM with autologin" || fail "R1.3 no SDDM autologin"
n=$(compgen -G "/sys/class/leds/valve-leds*" | wc -l); [[ $n -eq 17 ]] && pass "R1.3 17 LED nodes" || fail "R1.3 $n LED nodes"
own=$(stat -c %U /sys/class/leds/valve-leds*/brightness 2>/dev/null | sort -u | tr '\n' ' '); [[ "$own" == "$(id -un) " ]] && pass "R1.3 LEDs owned by the user" || fail "R1.3 LED owner: $own"
[[ $(dkms status | grep -c ': installed') -ge $((3 * $(ls /usr/lib/modules | wc -w))) ]] && pass "R1.3 DKMS installed for every kernel" || fail "R1.3 DKMS: $(dkms status | tr '\n' ';')"
systemctl is-active --quiet steamos-manager && pass "R1.3 steamos-manager active" || systemctl --user is-active --quiet steamos-manager && pass "R1.3 steamos-manager active (user)" || fail "R1.3 steamos-manager not active"
# R1.4: nothing to do
out=$(menu '\nq\n'); grep -qi "already the way you want it" <<< "$out" && pass "R1.4 already the way you want it" || fail "R1.4 message missing"
# R1.5: re-apply: no errors, no duplicates in the journals
out=$(menu 'a\ny\nm\nq\nn\n')
grep -q '^\[ERROR\]' <<< "$out" && fail "R1.5 errors: $(grep -m1 '^\[ERROR\]' <<< "$out")" || pass "R1.5 re-apply without errors"
dup=""; for f in ~/.local/state/steamify/*; do [ -f "$f" ] && d=$(cut -f1-3 "$f" | sort | uniq -d) && [ -n "$d" ] && dup+="$(basename "$f"): $d; "; done
[[ -z "$dup" ]] && pass "R1.5 no duplicates in the journals" || fail "R1.5 DUPLICATES $dup"
# R1.6: --backend status
ver=$(sed -n 's/^VERSION=//p' /mnt/steamify.sh | head -n 1)
js=$(bash /mnt/steamify.sh --backend status 2>/dev/null)
python3 - "$js" "$ver" <<'PY'
import json, sys
def r(ok, m): print(("PASS " if ok else "FAIL ") + m)
try:
d = json.loads(sys.argv[1]); r(True, "R1.6 --backend status is valid JSON")
except Exception as e:
r(False, f"R1.6 --backend status is not JSON: {e}"); sys.exit()
r(sys.argv[2] in json.dumps(d), f"R1.6 the JSON has the current version {sys.argv[2]}")
PY
# R1.10: an item older in features.state shows as an update, and the run brings it up to date
f=~/.local/state/steamify/features.state
[[ -f "$f" ]] && sed -i 's/^cec=.*/cec=2.1.0/' "$f" && grep -q '^cec=2.1.0' "$f" && pass "R1.10 cec set to 2.1.0" || fail "R1.10 cannot set cec older in $f"
out=$(menu 'q\n'); grep -q "HDMI-CEC.*(update)" <<< "$out" && pass "R1.10 the menu shows HDMI-CEC (update)" || fail "R1.10 no (update) on HDMI-CEC"
out=$(menu '\ny\nm\nq\nn\n'); grep -q '^cec=2.1.0' "$f" && fail "R1.10 cec still 2.1.0 after the run" || pass "R1.10 cec brought up to date"
+8
View File
@@ -0,0 +1,8 @@
# Sourced in front of a *.host.sh block (scripts/vmsuite.sh): the block runs on the host with the VM's ssh helpers.
. "$repo/scripts/common.sh"
info() { echo "INFO $*"; }
pass() { echo "PASS $*"; }; fail() { echo "FAIL $*"; }; skip() { echo "SKIP $*"; }
# guest <cmd...>: a command in the guest with the Plasma session environment and Steamify's libs
guest() { { cat "$repo/share/vmtest/prelude.sh"; echo "$*"; } | vm_ssh -o ConnectTimeout=6 -o LogLevel=ERROR bash -s 2>&1 | sed 's/\x1b\[[0-9;]*m//g'; }
waitssh() { local _; for _ in $(seq 60); do vm_ssh -o ConnectTimeout=4 'uptime -p' >/dev/null 2>&1 && return 0; sleep 5; done; return 1; }
reboot_guest() { vm_ssh 'sudo systemctl reboot' >/dev/null 2>&1; sleep 25; waitssh; }
+18
View File
@@ -0,0 +1,18 @@
# Sent to the guest in front of every block (scripts/vmsuite.sh): helpers and Steamify's libs.
# ssh has no Plasma session: the theme item (Vapor layout) needs one (vmrun.sh does the same).
export XDG_RUNTIME_DIR=/run/user/$(id -u) WAYLAND_DISPLAY=wayland-0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus
info() { echo "INFO $*"; }
pass() { echo "PASS $*"; }; fail() { echo "FAIL $*"; }; skip() { echo "SKIP $*"; }
sudo mountpoint -q /mnt || sudo mount -t 9p -o trans=virtio,version=9p2000.L repo /mnt
cd /mnt || exit 1
export SCRIPT_DIR=/mnt # steamify.sh sets it; the libs alone read patches/ and services/ from it
for lib in common state packages login-manager single-user steam-desktop steam-machine fremont-poweroff vram-booster hdmi-refresh cec boot-session vapor-theme steamos-extras bios desktop-shortcut wizard-shortcut steam-game update-notifier; do
# shellcheck source=/dev/null
. "lib/$lib.sh" 2>/dev/null
done
sf() { bash /mnt/steamify.sh "$@" < /dev/null 2>&1 | sed 's/\x1b\[[0-9;]*m//g'; return "${PIPESTATUS[0]}"; }
# is <item>: Steamify's own status function (what the menu ticks); isnt <item>
is() { "${1}_status" 2>/dev/null; }
expect_on() { local i; for i in "$@"; do is "$i" && pass "$i is on" || fail "$i is off"; done; }
expect_off() { local i; for i in "$@"; do is "$i" && fail "$i is on" || pass "$i is off"; done; }
ticks() { local i; for i in gaming theme glyphs single launcher notify vram cec machine poweroff boot; do is "$i" && printf '%s ' "$i"; done; echo; }
+32
View File
@@ -0,0 +1,32 @@
# R1.8 (and the matrix in the cachyos-vm-testing skill): switch items off and on and check the state each time.
A=gaming,theme,glyphs,single,launcher,notify,cec,machine,poweroff
apply() { info "apply: $1"; sf --defaults --options "$1" >/dev/null; [[ $? -eq 0 ]] && pass "options $1: exit 0" || fail "options $1: failed"; }
apply "$A"
expect_on gaming theme glyphs single launcher notify cec machine poweroff
[[ "$(current_display_manager)" == sddm ]] && pass "baseline: SDDM (single user)" || fail "baseline display manager: $(current_display_manager)"
# theme off
apply "${A/theme,/}"
expect_off theme; expect_on gaming single machine
# theme on again
apply "$A"; expect_on theme
# single user off: plasmalogin
apply "${A/single,/}"
expect_off single; expect_on gaming
[[ "$(current_display_manager)" == plasmalogin ]] && pass "single off: plasmalogin" || fail "single off: display manager $(current_display_manager)"
# single user on: SDDM again
apply "$A"; expect_on single
[[ "$(current_display_manager)" == sddm ]] && pass "single on: back to SDDM" || fail "single on: display manager $(current_display_manager)"
# Steam Machine support and power-off fix off: the driver goes, then comes back
apply gaming,theme,glyphs,single,launcher,notify,cec
expect_off machine poweroff
[[ ! -f /etc/modules-load.d/steamify-fremont-poweroff.conf ]] && pass "machine off: the power-off modules-load entry is gone" || fail "machine off: modules-load entry still there"
apply "$A"; expect_on machine poweroff
[[ $(dkms status | grep -c ': installed') -ge $((3 * $(ls /usr/lib/modules | wc -w))) ]] && pass "machine on: DKMS installed for every kernel" || fail "machine on: DKMS $(dkms status | tr '\n' ';')"
# HDMI-CEC off and on
apply "${A/cec,/}"; expect_off cec
apply "$A"; expect_on cec
# conversion off: single user goes with it, plasmalogin's autologin is CachyOS's again
apply launcher,notify
expect_off gaming single theme
[[ "$(current_display_manager)" == plasmalogin ]] && pass "conversion off: plasmalogin" || fail "conversion off: display manager $(current_display_manager)"
sed -n '/^\[Autologin\]/,/^\[/p' /etc/plasmalogin.conf 2>/dev/null | grep -q 'Session=plasma' && pass "conversion off: plasmalogin autologin is Session=plasma" || skip "conversion off: no plasmalogin Autologin section (a VM from vminstall.sh has no /etc/plasmalogin.conf, see TESTPLAN Known gaps)"