Compare commits

...
105 Commits
Author SHA1 Message Date
theupriserandClaude Sonnet 5.5 d041d452ca test: unit tests of steam-shortcuts.py and steamify-notifier.py
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss5gKj7XG5JPi27XwGBGTs
2026-10-03 13:18:15 +02:00
theupriserandClaude Sonnet 5.5 8ef8fa3ba2 test: the helpers read Steamify's lib list from steamify.sh instead of their own copy
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss5gKj7XG5JPi27XwGBGTs
2026-10-03 12:53:40 +02:00
theupriserandClaude Sonnet 5.5 738160882a test: menu-test.sh, a golden-output test of the menu rules, the plan and the app's JSON (REPO points at the checkout)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 11:19:01 +02:00
theupriser 2ac2b0d5ca docs: steamify-branch-cleanup: a released release/X.Y.Z branch is deleted once a tag or main contains it
Sync git.upriser.nl mirror / sync (push) Skipped
2026-10-02 14:41:40 +02:00
theupriser b3a58cf62c docs: CHANGELOG 0.5.0: the full test run, the ISO pipeline, tests moved here, the 2.11.0 test results
Sync git.upriser.nl mirror / sync (push) Skipped
2026-10-02 14:06:35 +02:00
theupriser 66ec27d4e2 test: vmstate reports Steam's autostart entry (silent/plain/none) instead of the old unit
Sync git.upriser.nl mirror / sync (push) Skipped
2026-10-02 09:28:05 +02:00
theupriser 899e9af06c test: the Steamify unit tests and the NVIDIA hardware test move here from steamify-cachyos (REPO points at the checkout)
Sync git.upriser.nl mirror / sync (push) Skipped
2026-10-02 07:53:25 +02:00
theupriser 660558ab45 notes: Steamify 2.10.0
Sync git.upriser.nl mirror / sync (push) Skipped
2026-10-01 21:05:27 +02:00
theupriser 5d168ea44f tests: nvidia suite (fake NVIDIA PC), controller block, name-based menu rows; get-iso.sh uses CachyOS's own mirror and resumes; notes
Sync git.upriser.nl mirror / sync (push) Skipped
2026-10-01 21:03:20 +02:00
theupriser fa67d65a96 nvidia: Plasma session with Big Picture autostart works
Sync git.upriser.nl mirror / sync (push) Skipped
2026-10-01 18:33:24 +02:00
theupriser cbd6920b96 nvidia: TTY-only KWin stutters, Big Picture in Plasma is perfect
Sync git.upriser.nl mirror / sync (push) Skipped
2026-10-01 18:26:25 +02:00
theupriser 74c859ef8d nvidia: direct scanout confirmed as the cause, a Plasma-less KWin session works
Sync git.upriser.nl mirror / sync (push) Skipped
2026-10-01 18:11:32 +02:00
theupriser 0052566ace nvidia: results of the hardware test on the RTX 5080 PC
Sync git.upriser.nl mirror / sync (push) Skipped
2026-10-01 18:02:17 +02:00
theupriser d045b82bae docs: nvidia/: TODO, research and test instructions for the NVIDIA fix (moved here from the steamify-cachyos branch); AGENTS.md: feature notes live in this repo
Sync git.upriser.nl mirror / sync (push) Skipped
2026-10-01 15:38:51 +02:00
theupriser 51c4fac70d docs: the ISO repo's PRs and vmisobuild.sh target master (feat/steamify is gone)
Sync git.upriser.nl mirror / sync (push) Skipped
2026-10-01 15:36:34 +02:00
theupriser 343ff1edda feat: vmtest.sh runs the plain-PC install check (generic) for every loader; vmprogress.sh knows the new jobs
Sync git.upriser.nl mirror / sync (push) Skipped
2026-09-30 14:47:40 +02:00
theupriser cb390c2dc0 docs: TESTPLAN results: the official 2.9.6 ISO, 341 pass, 0 fail
Sync git.upriser.nl mirror / sync (push) Skipped
2026-09-30 14:45:20 +02:00
theupriser b2eb003cc6 fix: the waitssh comment broke the one-line function (syntax error)
Sync git.upriser.nl mirror / sync (push) Skipped
2026-09-30 14:38:34 +02:00
theupriser d1dc96f29e fix: vmbootloadertest.sh waits up to 15 minutes for the first ssh (a first boot is slow with five VMs at once)
Sync git.upriser.nl mirror / sync (push) Skipped
2026-09-30 14:38:19 +02:00
theupriser d948227c19 docs: MAX_PARALLEL=5 ran fine on the 9800X3D (TODO R2 done, comments)
Sync git.upriser.nl mirror / sync (push) Skipped
2026-09-30 13:47:52 +02:00
theupriser 4824004215 docs: TODO: CI tests dropped (they run locally), the quick install test, new open items
Sync git.upriser.nl mirror / sync (push) Skipped
2026-09-30 13:46:43 +02:00
theupriser a1f8089c7c docs: the steamify-iso-release skill: dispatch-started build, steamify_ref, build badge and its secrets, replaced tags, parallel jobs on the runner
Sync git.upriser.nl mirror / sync (push) Skipped
2026-09-30 13:04:08 +02:00
theupriser d82d0f8b2f docs: the steamify-iso-release skill: daily names, kind input, replacement and retention, sync by workflow, always master
Sync git.upriser.nl mirror / sync (push) Skipped
2026-09-30 11:37:14 +02:00
theupriser 540139b967 feat: show progress while the tests run (install and steamify.sh heartbeats, unbuffered output)
Sync git.upriser.nl mirror / sync (push) Skipped
2026-09-30 10:51:24 +02:00
theupriser e929444399 fix: vminstall.sh is headless in CI (no display for QEMU's GTK window)
Sync git.upriser.nl mirror / sync (push) Skipped
2026-09-30 10:30:01 +02:00
theupriser 459832c3f5 feat: vmbootloadertest.sh --quick [--generic]: a Steam Machine gets all its items and modules, a plain PC none
Sync git.upriser.nl mirror / sync (push) Skipped
2026-09-30 10:12:35 +02:00
theupriser f6c87fddc7 fix: vmbootloadertest.sh --quick installs on a VM that reports Steam Machine hardware
Sync git.upriser.nl mirror / sync (push) Skipped
2026-09-30 10:11:00 +02:00
theupriser 913a8f0830 fix: vminstall.sh: show the QEMU, serial and install logs when the install fails
Sync git.upriser.nl mirror / sync (push) Skipped
2026-09-30 10:07:43 +02:00
theupriser 232c92fcd7 fix: vminstall.sh removes the read-only ISO files it extracted (failed as a non-root host)
Sync git.upriser.nl mirror / sync (push) Skipped
2026-09-30 09:50:28 +02:00
theupriser 092e888c3f fix: the VM's ssh key is looked up at every call (a clean host failed the first-boot check: common.sh was sourced before the key existed)
Sync git.upriser.nl mirror / sync (push) Skipped
2026-09-30 09:43:05 +02:00
theupriser fafa38fbb4 feat: vmbootloadertest.sh --quick: only what the ISO's install must get right (B1-B5)
Sync git.upriser.nl mirror / sync (push) Skipped
2026-09-30 09:38:30 +02:00
theupriser 6c0c80fb9a fix: a test that aborts fails (exit status of the piped block), vminstall reads the ISO with bsdtar (no loop partitions in CI containers)
Sync git.upriser.nl mirror / sync (push) Skipped
2026-09-30 09:02:16 +02:00
theupriser 80f4d68536 ci: sync the git.upriser.nl mirror on every branch and tag
Sync git.upriser.nl mirror / sync (push) Skipped
2026-09-30 08:54:35 +02:00
theupriser 594ce35cf8 feat: VM_INSTALL_MEM sets the RAM of the installer VM only
Sync git.upriser.nl mirror / sync (push) Skipped
2026-09-30 08:25:07 +02:00
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
78 changed files with 5023 additions and 180 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 master && ./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 master && ./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,27 @@
---
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`. A `release/X.Y.Z` branch is deleted (local and remote) once its release is published and its tip is contained in a tag or in `main` (the user said so on 2026-10-02: the tags keep the history). Check each one first (`git tag --contains <tip>`, `git merge-base --is-ancestor`): a release branch with a commit that is in no tag and not in `main` (`release/2.9.2`: a docs commit) is kept and reported. The open release's branch stays until its PR is merged.
- 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 `master` 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 deleted after their release (see the rules above; the tag keeps the history). 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,134 @@
---
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 with the first naming (`v2.9.3-dev.2026.09.29-2044`); since 2026-09-30 the names are by the day, like
CachyOS (below; workflow changes in ISO repo PR #15, steamify-cachyos PR #69). The workflows always run from the ISO
repo's `master`; `feat/steamify` is no longer needed.
## The flow (one tag names everything)
1. **GitHub, `iso-1-github-tag.yml`** (ISO repo `theupriser/steamify-cachyos-live-iso`, `workflow_dispatch` only, input
`kind`: `release`, `dev` or `auto` = release on master, dev elsewhere): takes Steamify's newest release (`X.Y.Z`),
the day **now in UTC** and makes an **annotated tag** plus a GitHub release with the changelog notes and the
*direct* download link on Gitea. Like CachyOS by the day, no time: real `vX.Y.Z-YYMMDD`, dev (pre-release)
`vX.Y.Z-dev-YYMMDD`. GitHub is the only place that reads a clock.
- **One ISO per day and kind** (space): a second build the same day deletes the earlier GitHub release and tag,
syncs the mirror and waits until the mirror dropped the tag (it drops tag, release and ISO together), then tags
again (else the new tag is only an update there and starts no build).
- **Retention:** at most 10 dev and 10 real releases (`gh release delete --cleanup-tag`); the mirror follows.
2. **The mirror syncs** (git.upriser.nl is a pull mirror of GitHub with **no interval of its own**: every repo's
`git-upriser-sync.yml` calls its mirror-sync API on every branch and tag push, and iso-1's last step after the tag;
secrets `GIT_UPRISER_URL`, `GIT_UPRISER_TOKEN`): the tag arrives.
3. **Gitea, `iso-2-gitea-build.yml`** (`workflow_dispatch` only, started by iso-1's last step through Gitea's dispatch API with
`{"ref": "refs/tags/<tag>"}` (the **full** ref: a bare tag name answers 404) and the input `steamify_ref`; a tag push
starts nothing; 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). A leftover release of the same tag is replaced. Two jobs,
build and release; no tests there (the runner is small): VM tests run locally, `scripts/vmtest.sh`.
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 master -f kind=dev|release`), or
by a new version: `steamify-cachyos`' `bundle.yml` step "Start the Steamify ISO's release" (a version published
from `main` -> `kind=release`, an unreleased version pushed to `release/**` -> `kind=dev`; 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.6-dev-260930`: file `steamify-cachyos-2.9.6-dev-260930-x86_64.iso`,
label `STEAMIFY_2_9_6_260930` (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 20 minutes (release, tag, sync in seconds, build ~18).
## Which Steamify goes on the ISO, and the build badge
- `iso-1` input **`steamify_ref`** (a Steamify branch or tag; empty = the newest *published* release). A dev ISO from a
release branch passes the branch (`bundle.yml` does), so `release/2.9.6` gives a 2.9.6 ISO before `v2.9.6` exists;
without it every dev ISO carries the last published version (it kept producing 2.9.5). iso-1 reads `VERSION` from
that ref's `steamify.sh`; iso-2 clones the ref and runs `steamify-prepare.sh <checkout>`.
- **Build badge in the GitHub release:** iso-1 writes `![ISO build](...running-yellow)` at the top of the notes; iso-2's
last job `report` (`if: always()`) turns it into succeeded / failed / cancelled (shields.io static badge, linked to the
Gitea run) through the GitHub API. Secret **`GH_RELEASE_TOKEN`** on the Gitea repo (Settings -> Actions -> Secrets):
fine-grained GitHub token, only `steamify-cachyos-live-iso`, *Contents: read and write*; expires, so renew it. Without
it the badge stays "running". Gitea's own `badge.svg` says "no status" for these runs (they run on a tag ref).
- Secrets in all: GitHub `steamify-cachyos`: `ISO_DISPATCH_TOKEN`, `GIT_UPRISER_URL`, `GIT_UPRISER_TOKEN`; GitHub ISO
repo: `GIT_UPRISER_URL`, `GIT_UPRISER_TOKEN`; Gitea ISO repo: `GH_RELEASE_TOKEN`.
## 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.
- **A replaced tag starts no run on the mirror.** One ISO per day and kind: iso-1 deletes the day's GitHub release and
tag and tags again; the mirror takes the new tag object (same name) but Gitea starts nothing and keeps the old release
(same id, new creation time) with the old ISO. So the build is started by dispatch, after the mirror has the new tag
object (`GET .../tags/<tag>` id == `git rev-parse refs/tags/<tag>`); iso-2 deletes a leftover release of the tag before
it publishes. Retention: at most 10 dev + 10 real releases.
- **Gitea runner and parallel jobs:** it ignores `max-parallel`, and jobs that start in the same second clone the same
action into one shared folder and break each other (`actions/cache` failed): keep parallel jobs free of `uses:`.
(The VM tests no longer run there at all: they run locally, `scripts/vmtest.sh`.)
- **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 -R theupriser/steamify-cachyos-live-iso --ref master -f kind=dev` (test) or
`-f kind=release`. Normally `bundle.yml` does this when a version is published (a release branch push -> dev).
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. Retention deletes the oldest automatically (10 dev, 10 real). By hand: `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/tests right after any branch or tag lands on GitHub (the mirror has no sync interval of its own).
on:
push:
branches: ['**']
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"
+11 -3
View File
@@ -5,16 +5,23 @@
!/README.md
!/CHANGELOG.md
!/run.sh
!/run.ps1
!/get-iso.sh
!/get-iso.ps1
!/add-ssh-key.ps1
!/TESTPLAN.md
!/TODO.md
!/tests
!/nvidia/
!/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 +33,4 @@ share/*
*.log
*.tmp
host-keys.pub
__pycache__/
+66
View File
@@ -0,0 +1,66 @@
# 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.
- Notes, TODOs, research and test instructions for a feature (e.g. `nvidia/`: the NVIDIA fix) live in THIS repo, never in
`steamify-cachyos`: the product repo keeps only code, its own docs (`TECHNICAL.md`, `CHANGELOG.md`) and, on an unreleased
branch, a `.no-release-yet` marker that points here. Not even temporarily: it would stay in that repo's history.
- 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 `master` (the ISO repo has no other long-lived branch; `feat/steamify` is gone).
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`; 5 worked on the 9800X3D). 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 `master`)
- 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.
+52
View File
@@ -4,6 +4,58 @@ 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.5.0 - 2026-10-02
The whole automated test (boot loaders plus suites), the Steamify ISO pipeline,
the NVIDIA notes, and the Steamify unit tests and hardware test moved here.
- **feat: `scripts/vmtest.sh` runs everything: the boot loader test for every loader the ISO advertises, the plain-PC install check and the `cli`, `menu`, `hw`, `installer`, `toggles` and `nvidia` suites** (`ee20bd9`, `c91277a`, `fe5251f`, `b91ef91`, `6d264ac`, `343ff1e`, `5d168ea`)
- `vmbootloadertest.sh --quick [--generic]`: only what the ISO's install must get right, a Steam Machine gets all its items and modules, a plain PC none (`fafa38f`, `459832c`)
- faster and parallel runs (`MAX_PARALLEL`), a remote runner, progress lines while the tests run (`0c84b8b`, `540139b`)
- the boot entry check (BootCurrent) after every reboot (`27b7445`), the CEC driver cache blocks (`e6119ca`), `vmview.py` (`4cc4046`)
- **feat: `scripts/vminstall.sh` installs any boot loader and runs the Steamify ISO's Steamify step, with a package cache** (`8646b3c`)
- parallel installs under a lock, never root as the guest user, headless in CI, stops at once when the live system reports a failed install (`9d53740`, `d5cfadf`, `e929444`, `a34efb0`)
- **feat: `scripts/vmisobuild.sh` builds the Steamify ISO in the test VM** (`bc06bc2`)
- **feat: the `progress-report` skill and `scripts/vmprogress.sh`** (`fd9ecf4`)
- **docs: skills and AGENTS.md: the ISO release pipeline, the WSL build host, the VM tests, token-cheap ISO-VM testing** (`0d0279b`, `a1f8089`, `cb247b9`, `0052f1b`)
- **docs: `nvidia/`: TODO, research and test instructions for the NVIDIA fix, with the results of the hardware test on the RTX 5080 PC** (`d045b82`, `0052566`, `fa67d65`)
- **test: the Steamify unit tests (`controllers-test.sh`, `nvidia-test.sh`) and the NVIDIA hardware test move here from steamify-cachyos, in `tests/`; `REPO` points at the checkout** (`899e9af`)
- **test: `vmstate.sh` reports Steam's autostart entry (silent, plain, none) instead of the old systemd unit** (`66ec27d`)
- Steamify 2.11.0 tested here before its release: three ISO installs (defaults, everything on, Fremont everything on) and the full suite on a plain CachyOS VM, 269 checks passed, 0 failed, 1 known skip. Found on the way: the first-login skip failed in a bundle (`SCRIPT_DIR` unbound) and the Steam Machine serial step failed in the installer's chroot (read-only `/sys`); both fixed in Steamify.
- Known: `vmtest.sh` and `vmsuite.sh` write fixed files in `/tmp` (`/tmp/vmtest-*.out`, `/tmp/vmsuite-*-reset.out`, `/tmp/vminstall-iso.lock`): a second user on the same host gets "Permission denied" on files the first one left.
## 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:
### The same key on the Steam Machine
```bash
ssh-keygen -t ed25519
scripts/steammachine-addkey.sh [user@host] # default: steammachine (~/.ssh/config)
```
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:
```bash
ssh-copy-id -p 2222 <vm-user>@localhost
```
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.
+174
View File
@@ -0,0 +1,174 @@
# 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.10.0 Gaming on NVIDIA (`feature/nvidia-gaming-fix`; suite `nvidia`, fake NVIDIA PC)
gamescope's session is broken on NVIDIA (see `nvidia/HARDWARE-RESULTS.md`), so on an NVIDIA PC the conversion is replaced by
"Gaming on NVIDIA" (Steam on the Plasma desktop, started at login, optionally in Big Picture) and single user mode logs in to Plasma.
The VM has no NVIDIA GPU: `fake_nvidia` (`share/vmtest/prelude.sh`) fakes one through Steamify's `NVIDIA_DRM_DIR` test hook and installs
fake NVIDIA modules for every kernel. What no VM can show (real driver, the picture, Big Picture on a real GPU) is checked on the real PC.
| # | Step | Expect |
|---|---|---|
| N1 | `--defaults --list` and `--backend status` with the fake GPU (`nvidia/10-n1-menu.sh`) | `nvidia`, `bigpicture` (parent `nvidia`), `single` offered and preselected; `gaming`, `boot`, `glyphs` hidden; without the fake GPU the conversion is offered and `nvidia` is not |
| N2 | `--defaults --options nvidia,bigpicture,single` (`nvidia/20-n2-n5-lifecycle.host.sh`) | exit 0; Steam installed; unit with `-gamepadui` enabled; `loginMode=emptySession`; parameters in `/etc/default/limine` and `/boot/limine.conf`; early-load file, hook and script; modules in the initramfs; SDDM enabled and `zzz-steamify-autologin.conf` (Plasma session) |
| N3 | reboot | Plasma straight in (no greeter), parameters on `/proc/cmdline`, the Steam unit started, `loginMode` kept |
| N4 | `--defaults --options notify` | exit 0; Steam removed (Steamify installed it), unit and autologin file gone, `loginMode` back to not set, parameters/early-load/hook gone, plasma-login-manager the login manager again |
| N5 | reboot | Plasma up through the test autologin, no NVIDIA parameters |
| N6 | real RTX 5080 PC | **done by hand 2026-10-01** (earlier prototype): Big Picture on the Plasma desktop smooth and clean; the apply/boot of the final component is the user's next step |
### F2.10.0 Extended controller support (`feature/extended-controller-support`; suite `cli`, block `60-c1-controllers.sh`)
Opt-in item: `xpadneo-dkms`, `xone-dkms` and `xone-dongle-firmware` from the CachyOS repo, DKMS-built for every kernel. The VM has no
dongle or controller: it covers the packages, the headers and the builds, not the hardware.
| # | Step | Expect |
|---|---|---|
| C1 | `--defaults --options extended_controller_support` | exit 0; the three packages installed; `dkms status` shows xone and xpadneo installed for every kernel; the modules (`xone_dongle`, `hid_xpadneo`) exist for each |
| C2 | the same again | quiet, nothing installed again |
| C3 | `--defaults --options notify` | exit 0; the three packages removed again |
| C4 | real PC with the Xbox dongle | **by hand, 2026-10-01 earlier**: the dongle works with the AUR `xone-dkms-git` (modules `xone_dongle`, `xone_gip_gamepad` loaded); status counts `xone-dkms-git` as on |
### 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) |
| 2026-09-30 | Steamify 2.9.6 (`steamify-cachyos` main `d93f9ea`), the official ISO `steamify-cachyos-2.9.6-260930-x86_64.iso` from git.upriser.nl (sha256 `4a94f308…aec41`), dev `main`; on the PC, headless (`CI=1`/`VM_HEADLESS=1`), `MAX_PARALLEL=5` | everything automated: B1-B8 (3 loaders, `--install` from the new ISO), cli, menu, hw, installer, toggles (base VM from the same ISO with `VM_STEAMIFY=skip`) | **341 pass, 0 fail** (toggles 42 +1 expected skip, hw 77, cli 55, menu 36, installer 4, Limine 43, systemd-boot 42, GRUB 42). Found on the way (test setup, not Steamify or the ISO): with five VMs at once the loaders' first boot took longer than `waitssh` waited (5 min), so all three stopped with `FAIL no SSH` although the VMs were up; `waitssh` now waits up to 15 minutes and the loaders were run again (install kept) |
| 2026-10-01 | Steamify `feature/extended-controller-support` (7dae14c; on top of `feature/nvidia-gaming-fix` d0e4d8c, 2.9.7), plain CachyOS ISO 260809 (VM `~/vms/steamify-vm`, `vminstall.sh --iso`), dev `main` + the nvidia suite; on the PC (native CachyOS, KVM), headless | N1-N5 (new suite `nvidia`), C1-C3 (cli block 60), R1.1-R1.6/R1.10, H1-H5 and the CEC/CEC cache blocks, R2.1, toggles | **269 pass, 0 fail**: cli 65, nvidia 43, menu 37, hw 78, installer 4, toggles 42. Found on the way (test side, not Steamify): R1.1 and H3 pressed menu rows by number and the new Extended controller support row shifted them (now by name); the N4 check for the parameter in `/boot/limine.conf` must ignore snapper's snapshot entries, which keep the command line of their time. The kernel part (parameters, early-load file, hook, initramfs) behaves in a real Limine VM; the real driver and picture are only checked on the RTX 5080 PC |
+88
View File
@@ -0,0 +1,88 @@
# TODO
## Tests in CI: dropped 2026-09-30, the VM tests run locally
Decision: `scripts/vmtest.sh` runs on the PC before the release PR, not on GitHub or Gitea runners (too slow, too heavy, and
not reliable: some GitHub runners had no `/dev/kvm`, nested virtualization made installs slow, the 3 GB ISO had to come
from the mirror on every run; the Gitea machine has 8 threads). The attempt (steamify-cachyos PR #66, closed, branch
`feature/vmtest-ci`) left these behind: `vminstall.sh` reads the ISO with `bsdtar` (no loop devices), `CI=1` makes it
headless, the VM's ssh key is looked up at every call, an aborted test counts as a failure, progress lines while a test runs.
- [x] TESTPLAN.md: section B for the boot loader test (B1-B8), pointing at `scripts/vmtest.sh`
- [x] `vmbootloadertest.sh --quick [--generic]`: only what the ISO's install must get right (B1-B5) on a Steam Machine VM
(`--fremont`) or a plain PC (`--generic`: the Steam Machine items and modules must be absent)
- [ ] A helper that fetches the newest ISO release of the mirror (git.upriser.nl) into
`../steamify-cachyos-live-iso/out/desktop/`, for `vmtest.sh --install` (the ISO is no longer kept locally)
- [ ] Document `--quick` / `--generic` in the `vm-install` skill and TESTPLAN.md (which B rows it covers)
- [ ] Steamify `AGENTS.md`: "run `scripts/vmtest.sh` before the release PR into main" (decided 2026-09-30)
## 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
- [x] R2. `MAX_PARALLEL=5` ran fine on the PC (9800X3D, 8c/16t, 64 GB; reported 2026-09-30). The default stays 3 for smaller
machines. Not tried: 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" }
+4 -2
View File
@@ -4,7 +4,8 @@
set -euo pipefail
cd "$(dirname "$0")"
mirror="${MIRROR:-https://nl.mirror.cx/cachyos}"
# CachyOS's own mirror: nl.mirror.cx gave about 1 MB/s here, this one 80 MB/s (MIRROR=... to use another).
mirror="${MIRROR:-https://mirror.cachyos.org}"
# The download page embeds the current ISO URL; take the release from it.
release="$(curl -fsL https://cachyos.org/download/ |
@@ -22,8 +23,9 @@ if [[ -f cachyos.iso && "$(cat cachyos.iso.version 2>/dev/null)" != "$name" ]];
rm -f cachyos.iso
fi
curl -fL -C - -o cachyos.iso "$url"
# The version is written before the download: a partial file of this release is resumed, not deleted as "an older release".
echo "$name" > cachyos.iso.version
curl -fL -C - -o cachyos.iso "$url"
echo "Verifying checksum..."
curl -fsL "$url.sha256" | awk '{print $1" cachyos.iso"}' | sha256sum -c
+70
View File
@@ -0,0 +1,70 @@
# NVIDIA fix: results on the RTX 5080 PC (2026-10-01)
PC: RTX 5080 (GB203, 10de:2c02) + AMD Granite Ridge iGPU, MSI MAG 341C OLED on the NVIDIA DP-1 (3440x1440, 175 Hz),
CachyOS, Limine, kernel 7.2.8-1-cachyos, nvidia-open 615.71.09, gamescope 3.16.31-1, gamescope-session-cachyos 1.1.6-1.
## Result: the kernel-parameter fix does not cure the picture
- Baseline (before): gaming mode corrupted without HDR; with HDR it worked but not at 175 Hz and not full screen.
- After `apply` + reboot (`modeset = Y`, `fbdev = Y`, NVIDIA modules in the initramfs): flickers heavily, artifacts, still no 175 Hz.
`modeset`/`fbdev` were already `Y` live before the fix (driver 615), so the fix changed little.
- Cause is not the parameters: NVIDIA's open bug "display modes above 2560x1440@120 with HDR cause flickering/corruption within
gamescope-session" (internal bug 5240452, April 2025, no fix as of March 2026; 555 up to 595+, RTX 3070 Ti/4090/5090; corruption is
display-side only):
https://forums.developer.nvidia.com/t/display-modes-above-2560x1440p-120hz-with-hdr-enabled-cause-flickering-corruption-within-gamescope-session/295314
## Bugs found in the fix on this PC (fixed in commit a5e9015 on feature/nvidia-gaming-fix)
- CachyOS writes Limine's line as `KERNEL_CMDLINE[default]+="..."`; the sed only matched `=`, so apply stopped with "Couldn't add the NVIDIA parameters".
- The kernel treats `nvidia_drm.modeset=1` and `nvidia-drm.modeset=1` alike; the check only knew the hyphen.
- tests/nvidia-hardware-test.sh did not set `SCRIPT_DIR`, so patch_file read `/patches/...`.
## What gamescope did (journal, gaming mode)
- Picks the RTX 5080 (card1), DP-1, mode list has 3440x1440@175/144/100. It selected @60 by default; with
`-W 3440 -H 1440 -r 175` added (a copy of /usr/lib/steamos/gamescope-session, whose last line is `-O ... \` so args need a copy)
it selected @175 and did not crash, but the picture was the same. Steam then sets the Xwayland size to 1920x1080 (not full screen).
- `vkGetPhysicalDeviceFormatProperties2 returned zero modifiers` for 2 16-bit formats: harmless here.
- A gamescope SIGABRT (`double free or corruption`) at session exit, backtrace through libnvidia-eglcore and libvulkan_radeon.
- The PC froze once and was reset after a gaming-mode session (no kernel error logged).
## What works: nested gamescope inside KDE (Wayland), borderless
- `gamescope -W 3440 -H 1440 -r 175 -f ...`: artifacts once the window is focused and fullscreen (unfocused it is fine).
Hypothesis, not proven: KWin's direct scanout of the fullscreen window.
- `gamescope -W 3440 -H 1440 -r 175 -b -e --hdr-enabled -- steam -gamepadui` (borderless): clean, full screen, about 175 Hz.
- So on NVIDIA the separate gamescope session (the SteamOS conversion) is blocked by the driver bug; a nested borderless gamescope works.
Whether Steamify gets a nested option for NVIDIA is the user's decision.
## Cause confirmed: KWin's direct scanout; a session without Plasma works (2026-10-01, later)
- A TTY session of only `kwin_wayland --drm` + `gamescope -b` + `steam -gamepadui` (no Plasma shell, no panel) artifacted too
(KWin hands the covering window straight to the display).
- Same session with `KWIN_DRM_NO_DIRECT_SCANOUT=1` (exists in KWin 6.7.5's libkwin; not in the kwin_wayland binary's strings):
NO artifacts, full screen, about 175 Hz (gamescope logs `Changed refresh to: 174.962hz`). Animations felt a bit clunky (cause unknown:
an extra composition step without scanout, no CAP_SYS_NICE for gamescope, no VRR, nested frame pacing; other switches to try:
`KWIN_DRM_DISABLE_TRIPLE_BUFFERING`, `KWIN_DRM_PREFER_COLOR_DEPTH`).
- Launcher used for the test: `~/.local/bin/steamify-nvidia-session-test` (run from a free TTY; KWin as the compositor,
`gamescope -W 3440 -H 1440 -r 175 -b -e --hdr-enabled -- steam -gamepadui`, a watchdog ends it after 90 s).
Lesson: the first version's timeout left KWin/gamescope/Steam running (the cleanup came after a pipeline that waits for them);
`pkill -f '<pattern>'` in a command that contains the pattern kills the caller.
- Open: a real session entry (`/usr/share/wayland-sessions/`, needs sudo) and the product decision: an NVIDIA session mode in Steamify
instead of the gamescope DRM session. Not built, not in the product repo.
## Smoothness: the TTY-only KWin session stutters, Plasma does not; Big Picture in Plasma is perfect (2026-10-01, evening)
- TTY session (`kwin_wayland --drm` under `dbus-run-session`, no Plasma): the mouse and windows stutter every 1-2 s, with gamescope
(clunky) and without it (`steam -gamepadui` as KWin's only client). Same in both: not gamescope, not Steam. Plasma's own KWin is smooth.
- Tried without effect: `KWIN_DRM_DISABLE_TRIPLE_BUFFERING=1`. Measured: the test KWin used about 2% CPU, no spikes, but its main thread
dropped to 0% about every 2 s (blocked, not busy); scheduling (RR main thread, nice -12) and capabilities equal to Plasma's KWin;
NVIDIA is boot_vga; no relevant environment differences found. Cause of the stutter not found.
- Plain `steam -gamepadui` (Big Picture, no gamescope) in the normal Plasma session: **perfect** (smooth, no artifacts, no env var needed).
- Direction: for NVIDIA PCs a Plasma session that shows only Steam's Big Picture (no desktop, no panel) instead of the gamescope
session. Gives up gamescope's features (HDR handling, FSR, overlay; most `-steamos3` menus need gamescope).
## Plasma session with Big Picture started at login: works (2026-10-01, night)
- `~/.config/autostart/steamify-bigpicture.desktop` (`Exec=steam -gamepadui`) in the normal Plasma Wayland session, SDDM autologin set to
`plasma.desktop` (`pkexec /usr/lib/steamos/steam-set-session plasma.desktop`): Big Picture opens by itself, smooth, no artifacts, no
KWin environment variables needed. Exiting Steam leaves the Plasma desktop, no relogin.
- A separate session entry (prototype in the scratchpad, not installed: `steamify-bigpicture-session` marker + autostart helper, session
file in `/usr/share/wayland-sessions`) only decides WHEN Big Picture starts; the plain autostart gave the same result.
## State of the PC after the test (to undo)
- The fix is applied (Limine file backup `/etc/default/limine.bak-gamescope-wizard`, early-load file, pacman hook): undo in INSTRUCTIONS.md.
- Test launcher `~/.local/bin/steamify-nvidia-session-test`, log `~/steamify-nvidia-session-test.log`.
- User override of the gaming session: `~/.config/systemd/user/gamescope-session.service.d/steamify-nvidia-test.conf` and
`~/.local/share/steamify/nvidia-test/`.
+87
View File
@@ -0,0 +1,87 @@
# Testing the NVIDIA fix on a real PC
For the PC with an RTX 5080 (or any RTX 20 series or newer card) that shows a corrupted picture when
gaming mode (gamescope) starts. Product branch: `feature/nvidia-gaming-fix` in steamify-cachyos, not released.
This file lives in steamify-cachyos-dev (`nvidia/INSTRUCTIONS.md`); background and open work: `nvidia/TODO.md`; research
with sources: `nvidia/RESEARCH.md` (github.com/theupriser/steamify-cachyos-dev).
## What the fix does
With a supported NVIDIA GPU (RTX 20 series or newer, same check as the VRAM booster) and its driver installed it
- adds `nvidia-drm.modeset=1 nvidia-drm.fbdev=1` to the kernel command line (Limine, systemd-boot or GRUB;
the file is backed up as `<file>.bak-gamescope-wizard`),
- loads the NVIDIA modules in the initramfs, but only while every installed kernel has them (a pacman hook,
`/etc/pacman.d/hooks/85-steamify-nvidia-initramfs.hook`, decides again at every kernel change),
- rebuilds the initramfs and boot entries and checks that the parameters really are in them.
It takes effect after a reboot. Older cards (GTX 10 series and older) are left alone.
## Before you start
- CachyOS with the NVIDIA driver installed (`nvidia-open`), a user with sudo, `git`.
- Be at the PC and able to see the screen: the last step needs your eyes.
- Know the way back (below) in case the PC doesn't boot to a picture: a TTY (Ctrl+Alt+F3) or the boot menu's
other kernel entry.
## Steps
1. Get the code:
```
git clone -b feature/nvidia-gaming-fix https://github.com/theupriser/steamify-cachyos
cd steamify-cachyos
```
(An existing checkout: `git fetch && git checkout feature/nvidia-gaming-fix && git pull`.)
2. Look first, change nothing:
```
bash tests/nvidia-hardware-test.sh check
```
3. Apply the fix (asks for your sudo password), then **reboot**:
```
bash tests/nvidia-hardware-test.sh apply
```
4. After the reboot, check again:
```
bash tests/nvidia-hardware-test.sh check
```
5. Start gaming mode, look at the screen, then (from the desktop or a TTY):
```
bash tests/nvidia-hardware-test.sh visual
```
It asks whether the picture was clean and for one line on what you saw.
6. Send back `~/steamify-nvidia-report.txt` (every run adds to it), or start a new session in the checkout and
say "read nvidia/TODO.md in steamify-cachyos-dev and the report".
(`./steamify.sh` from the same checkout also applies the fix, automatically, as part of the SteamOS conversion:
there is no menu option for it yet. The script above shows more.)
## Reading `check`
| Line | Good result | If not |
|---|---|---|
| `PASS a supported NVIDIA GPU ... is detected` | your 5080 | `FAIL ... older than RTX 20`: the card is skipped on purpose. `FAIL no NVIDIA GPU with its driver`: driver not installed (nouveau?) |
| `NVIDIA PCI device id 0x... (not on a legacy list ...)` | RTX 20 or newer | on a legacy list: older card |
| `no chwd legacy lists found` | (note only) | the check cannot tell old from new cards; every card counts as supported. Tell me, it's open item 4 in `nvidia/TODO.md` |
| `PASS nvidia-drm.modeset=1 is on the running kernel's command line` (and `fbdev=1`) | after step 3 and the reboot | "in the boot loader's file but not booted yet": reboot, or the boot entry used is another one |
| `PASS nvidia_drm modeset = Y` (and `fbdev = Y`) | after the reboot | `unreadable`: it needs sudo, enter the password |
| `... early-load drop-in is used / skipped` | note only: skipped means a kernel without the NVIDIA modules is installed | nothing to do |
## Reading the result
- Parameters on, `modeset = Y`, `fbdev = Y`, picture clean: the fix works.
- Parameters on and `Y`, picture still corrupted: not this fix. The report has the gamescope and NVIDIA log lines and
the versions of `nvidia-open`, `nvidia-utils`, `gamescope` and `gamescope-session-cachyos`: the cause is probably the
gamescope/driver combination for Blackwell (see `nvidia/RESEARCH.md`).
- No picture at all after the reboot: undo it (next section) from a TTY or the other kernel entry, and send the report.
## Undo
Turning the SteamOS conversion off in the wizard removes the fix too. On its own, from the checkout:
```
SCRIPT_DIR=$PWD bash -c 'source lib/common.sh; source lib/hdmi-refresh.sh; source lib/vram-booster.sh; source lib/nvidia.sh; nvidia_disable'
```
It removes the parameters, the early-load file, the pacman hook and its script, and rebuilds the boot entries (reboot after).
By hand: delete ` nvidia-drm.modeset=1 nvidia-drm.fbdev=1` from `/etc/default/limine` (`KERNEL_CMDLINE[default]`),
`/etc/sdboot-manage.conf` (`LINUX_OPTIONS`) or `/etc/default/grub` (`GRUB_CMDLINE_LINUX_DEFAULT`), or restore the
`.bak-gamescope-wizard` copy next to it; remove `/etc/mkinitcpio.conf.d/90-steamify-nvidia.conf`,
`/etc/pacman.d/hooks/85-steamify-nvidia-initramfs.hook` and `/usr/local/libexec/steamify-nvidia-initramfs`; then
`sudo limine-mkinitcpio` (Limine) or `sudo mkinitcpio -P` and `sudo sdboot-manage gen` / `sudo grub-mkconfig -o /boot/grub/grub.cfg`.
## What is and isn't proven
Proven in VMs (QEMU/KVM, CachyOS ISO 260809): the parameters land and survive a reboot on Limine, systemd-boot and GRUB;
disabling restores the original; on Limine the pacman hook keeps the early-load file right across kernel reinstalls
(with fake NVIDIA modules). Not proven: that the fix cures the 5080's picture, the real NVIDIA modules in the
initramfs, DKMS hook order, systemd-boot/GRUB with a pacman transaction, and the RTX 20+ check against a real chwd list.
Unit test (no hardware): `bash tests/nvidia-test.sh`.
+64
View File
@@ -0,0 +1,64 @@
# NVIDIA and gamescope: research notes (for steamify-cachyos branch `feature/nvidia-gaming-fix`)
Written 2026-10-01 for the NVIDIA fix (`lib/nvidia.sh`). Delete the `nvidia/` folder when the feature is released, or move what is still useful to steamify-cachyos' `TECHNICAL.md`.
"Verified" means seen in this session (a VM run or a fetched page); the rest is from search results or memory.
## The user's PC
RTX 5080 (Blackwell). Blackwell only works with NVIDIA's open kernel modules (`nvidia-open`), driver 570+.
The corrupted picture at gaming mode start is not diagnosed yet: the fix adds kernel parameters, the real cause
may be the gamescope/driver combination. `tests/nvidia-hardware-test.sh` collects the facts.
## What the fix changes and why (reasoning, not proven for gamescope)
- gamescope runs the display itself through DRM/KMS. With NVIDIA that needs `nvidia-drm.modeset=1`; `nvidia-drm.fbdev=1`
gives the console a framebuffer from `nvidia-drm`, so the hand-off from the console to gamescope is clean.
- Loading the NVIDIA modules in the initramfs (early KMS) only changes how early boot looks (native resolution,
splash, passphrase prompt), not speed. It is not needed for gamescope, which starts after login (believed, not proven).
- No source found that mentions these parameters for gamescope; the fix is a standard NVIDIA/Wayland setup, applied
automatically. Whether it cures the 5080's picture is only known after `tests/nvidia-hardware-test.sh`.
## Which cards (decision 2026-10-01: RTX 20 series or newer only, same check as the VRAM booster)
The code skips a card whose PCI id is on chwd's legacy lists (`vram_nvidia_legacy_id`); the notes below are why older
cards are not promised.
- Open kernel modules: Turing (RTX 20, GTX 16) and newer only; they need the GSP processor first built into Turing.
Maxwell, Pascal and Volta (GTX 900/10 series, Titan V) only work with the proprietary driver, whose legacy branch
is 580. Source: [NVIDIA README, open kernel modules](https://download.nvidia.com/XFree86/Linux-x86_64/560.35.03/README/kernel_open.html),
[NVIDIA datacenter driver guide, kernel modules](https://docs.nvidia.com/datacenter/tesla/driver-installation-guide/kernel-modules.html).
- `nvidia-drm.modeset` and `fbdev` are options of the `nvidia-drm` module in both flavours, so the fix applies to
any card whose driver provides `nvidia_drm` (what `nvidia_present` tests: GPU vendor 0x10de + `modinfo nvidia_drm`).
Whether gamescope then works on an older card is a different question, and no source says the fix helps there.
- Not NVIDIA's proprietary driver: nouveau has no `nvidia_drm`, so the fix does nothing. The CachyOS handheld ISO boots
GTX 10xx and older with nouveau until the NVIDIA driver is installed
([CachyOS forum](https://discuss.cachyos.org/t/information-experimental-cachyos-handheld-edition/203)).
## Known gamescope problems that are not this fix
- **GTX 1050 Ti (Pascal), driver 580.119.02, gamescope newer than 3.16.16:** the CachyOS handheld session fails to start when
a display is on the GPU's HDMI port (back to the TTY, loop). Workarounds: downgrade `gamescope` and `lib32-gamescope`
to 3.16.16, or use the motherboard's video port. Fixed upstream? unknown.
[CachyOS forum](https://discuss.cachyos.org/t/no-display-on-cachyos-handheld-edition-on-nvidia-with-gamescope-3-16-16/20935)
- **VRS on PCs:** CachyOS' gamescope-session enables variable rate shading (`STEAM_USE_DYNAMIC_VRS=1`,
`RADV_FORCE_VRS_CONFIG_FILE`, `echo 1x1 > ...` in `/usr/lib/steamos/gamescope-session`). It broke rendering (missing
floors/lighting, bad post-processing) on a PC with an AMD RX 6800 XT. RADV is AMD's driver: not the NVIDIA corruption.
Workaround: comment those three lines, or per game `env -u RADV_FORCE_VRS_CONFIG_FILE STEAM_USE_DYNAMIC_VRS=0 %command%`.
[CachyOS forum](https://discuss.cachyos.org/t/cachyos-gamescope-session-asset-missing-vrs-fix/34971)
- gamescope's DRM backend needs Vulkan DRM format modifiers; with the open Mesa driver NVK that came in Mesa 24.1
([GamingOnLinux](https://www.gamingonlinux.com/2024/05/nvk-driver-gets-drm-format-modifiers-to-work-with-gamescope-in-mesa-24-1)).
The proprietary driver had format-modifier trouble with gamescope too (GitHub issues
[ValveSoftware/gamescope#1662](https://github.com/ValveSoftware/gamescope/issues/1662),
[#1516](https://github.com/ValveSoftware/gamescope/issues/1516): titles seen only, not read).
## What was verified in VMs (QEMU/KVM, CachyOS ISO 260809)
- Kernel parameters land and survive a reboot on Limine, systemd-boot and GRUB; disable restores the original.
- Limine's tool is a compiled program: an appended `KERNEL_CMDLINE[default]+=" ..."` line was pasted into the kernel
command line as text; the fix edits the existing `KERNEL_CMDLINE[default]="..."` line.
- `mkinitcpio` fails on a MODULES entry a kernel lacks; `limine-mkinitcpio` then skips that kernel's initramfs and boot
entry (parameters included); systemd-boot/GRUB image: "may not be complete" (not run with failing modules).
- Real CachyOS/Limine pacman hooks: `10-limine-snapper-lock`, `60-limine-mkinitcpio-remove-pre`, `60-mkinitcpio-remove`,
`80-limine-efi-deploy`, `90-limine-mkinitcpio-remove-post`, `90-mkinitcpio-install`; Steamify's hook is `85-`
(DKMS's is `71-dkms-install`, not installed in the VM). A mirror older than the ISO "downgrades" kernels on reinstall.
- The hook keeps the early-load drop-in right across kernel changes (see `nvidia/TODO.md`).
## Still unknown (needs the hardware)
- Does the fix cure the 5080's corrupted picture? Which gamescope/driver versions does the PC have?
- Does it help, harm or do nothing on Maxwell/Pascal/Volta cards? No older card available yet.
- Real NVIDIA modules in the initramfs (VMs only had renamed fake modules); DKMS hook order on a PC with
`nvidia-open-dkms`.
+136
View File
@@ -0,0 +1,136 @@
# TODO: NVIDIA (status 2026-10-01, evening: READ THIS FIRST, the sections below are the older plan)
Version: bumped to **2.10.0** (2026-10-01, "chore: Version 2.10.0" on `feature/nvidia-gaming-fix`; `feature/extended-controller-support` is stacked on it).
The old plan below says 2.9.7 / `release/2.9.7`: read that as 2.10.0 / `release/2.10.0`, which doesn't exist yet (create it from `main` when releasing).
The kernel-parameter fix did not cure gamescope's picture; the cause is NVIDIA's own bug (see `HARDWARE-RESULTS.md`). The product now does this
instead (branch `feature/nvidia-gaming-fix`, 2.10.0, still behind `.no-release-yet`):
- On an NVIDIA PC the SteamOS conversion (`gaming`, `boot`, `glyphs`) is hidden (shown only while already on, so it can be turned off).
- `nvidia` "Gaming on NVIDIA": Steam installed when missing, started at login on the Plasma desktop (user unit); sub-option `bigpicture`
(a checkbox, not yet the choice row "How should Steam start up: Normal / Big Picture"): `-gamepadui` + Plasma starts with an empty session.
RTX 20+ also keeps the old kernel parameters and early-load initramfs (user asked for it: "it may need it").
- `single` (single user mode) also works there: its own SDDM autologin into the Plasma session (`zzz-steamify-autologin.conf`).
- Tested in VMs (QEMU/KVM, fake NVIDIA GPU through `NVIDIA_DRM_DIR`, fake modules): suite `nvidia`, TESTPLAN N1-N5, 43 pass; regression suites green.
- Tested by hand on the RTX 5080 (earlier prototypes): Big Picture in the normal Plasma session smooth and clean; autologin and the final component NOT yet run there.
Open: run the final wizard on the RTX 5080 PC (turn the conversion off, tick Gaming on NVIDIA + Single user mode, reboot); the real choice row
instead of the checkbox (the choice row is hard-wired to `boot` in the TUI, the app and `--boot`); games don't start fullscreen under Plasma
(idea: a KWin rule forcing fullscreen for `steam_app_*`, may bring the direct-scanout artifacts back: test on the PC); apps that start later
(CachyOS Hello) may still cover Big Picture; hint in the menu on why the item is missing while the conversion is on; changelog hashes;
screenshot retake and the ISO's installer page for the new rows before a release. Separate branch `feature/extended-controller-support`
(xone dongle + xpadneo, opt-in) is stacked on this one.
# TODO: NVIDIA fix for gaming mode
Product code: branch `feature/nvidia-gaming-fix` in steamify-cachyos (github.com/theupriser/steamify-cachyos). These notes live here, in
steamify-cachyos-dev, on purpose: they are not part of the product and must not be in its history. The branch only has
a `.no-release-yet` marker pointing here.
When the feature is released, move what is still useful to steamify-cachyos' `TECHNICAL.md` and delete this folder (`nvidia/`).
Written 2026-10-01 so a new session can pick this up.
## Problem
A PC with an NVIDIA RTX 5080 (Blackwell, open kernel modules) shows a corrupted
image when gaming mode (gamescope) starts. Cause not confirmed. Steamify had no
NVIDIA handling at all. Likely: `nvidia-drm.modeset=1` / `nvidia-drm.fbdev=1`
missing, or a gamescope/driver mismatch.
## What is built (all on the product branch, pushed)
- `lib/nvidia.sh`: part of the SteamOS conversion (`gaming_enable` / `gaming_disable`).
With an NVIDIA GPU + driver it adds `nvidia-drm.modeset=1 nvidia-drm.fbdev=1` to the
kernel command line (Limine, systemd-boot, GRUB; backup first) and, only when every
installed kernel has `nvidia_drm`, `/etc/mkinitcpio.conf.d/90-steamify-nvidia.conf`.
Rebuilds initramfs + boot entries, verifies the config and the generated entries
have the parameters (error otherwise). Undone on disable. Description: `TECHNICAL.md`.
- `FEATURE_VERSION[gaming]=2.9.7` so installs that already have the conversion re-apply it.
- Test hooks `NVIDIA_DRM_DIR`, `NVIDIA_MODULES_DIR`, `NVIDIA_LIMINE_CONF`,
`NVIDIA_SDBOOT_DIR`, `NVIDIA_GRUB_CFG`: only set by tests; defaults are the real paths.
## Tested
- `bash tests/nvidia-test.sh`: 22 checks against a fake RTX 5080 beside an iGPU, a stub
`modinfo`, temp boot loader files. All pass.
- Real VMs (QEMU/KVM, Limine, systemd-boot, GRUB): enable, reboot, kernel command line
has the parameters; disable, reboot, original command line. All pass. Found and fixed:
Limine pasted an appended `+=` line as text into the cmdline; a kernel without the
NVIDIA modules made limine-mkinitcpio skip its boot entry.
## Kernel changes (done, VM-tested 2026-10-01, Limine)
The early-load drop-in is kept right by a pacman hook (`patches/steamify-nvidia-initramfs.{sh,hook}`,
installed by `nvidia_enable`, `/etc/pacman.d/hooks/85-steamify-nvidia-initramfs.hook`). Real pacman
transactions in a Limine VM: the hook runs between "Updating module dependencies" and "Updating
linux initcpios" (real hook names there: 60-*-remove, 80-limine-efi-deploy, 90-limine-mkinitcpio-remove-post,
90-mkinitcpio-install; DKMS would be 71-): the drop-in is present while every kernel has the modules, removed
when the LTS kernel loses them, back when they return; initramfs nvidia files 1 -> 5 -> 1 -> 5 -> 1
(1 = baseline, 4 modules added); no ERROR/skipping in any rebuild; cmdline kept the parameters after a reboot;
disable removes hook, script and drop-in. Found on the way: an empty hook/script was installed when
`patch_file` failed (now an error). Only tested with fake modules (renamed copies of a small module), on
Limine; systemd-boot and GRUB use the same hook but were not run with pacman transactions.
## Opt-out toggle (new request, 2026-10-01: not built yet)
When a compatible NVIDIA card is listed there should be a menu option, on by default (opt-out), named
"NVIDIA compatibility", as a sub-option of the SteamOS conversion, directly under "Boot into" (`boot`).
Today the fix runs unconditionally inside `gaming_enable`/`gaming_disable`.
- [ ] New component `nvidia` in `lib/menu.sh`: `COMPONENTS` right after `boot`, `PARENT[nvidia]=gaming`, `LABEL`,
`FEATURE_VERSION[nvidia]`, `component_available` -> `nvidia_available` (= `nvidia_present`); not in
`NO_PRESELECT` (ticked by default, `feature_new` ticks it for installs that already have the conversion).
`nvidia_status` from the system (hook + parameters present), `nvidia_enable`/`nvidia_disable` = the code now
called from `gaming_enable`/`gaming_disable` (remove those calls). Check `toggle_component` dependencies,
`feature_record_unticked`, the plan texts ("This will: ...").
- [ ] The app: `ui/qml/Texts.qml` item texts, `AppState.qml`, the screen rows, `lib/backend.sh` if it lists ids;
`--defaults [--options <ids>]` (the Steam Machine ISO's installer pages: it names the ids) and `--skip`.
- [ ] README rows/screenshot rule (a release that adds a menu row retakes `assets/screenshot-menu.png`), TECHNICAL.md,
CHANGELOG, `tests/nvidia-test.sh` (status/enable/disable through the component), VM menu test in
steamify-cachyos-dev (`share/vmtest/menu`, TESTPLAN.md row).
- [x] What "compatible" means (decided 2026-10-01): RTX 20 series or newer, the same check as the VRAM booster
(`vram_nvidia_legacy_id`: a card on chwd's legacy lists `/var/lib/chwd/ids/nvidia-*.ids` is older; without the
lists every card counts). `nvidia_present` = such a card + `nvidia_drm` available; the toggle's `nvidia_available`
is the same function. Older cards (GTX 10/9 series) are left alone: no promise they work with gamescope.
## Older cards and other research
Decision 2026-10-01: support RTX 20 series or newer only, with the VRAM booster's check (done in `lib/nvidia.sh`).
The user had hoped older GPUs would work too; not promised. Full notes with sources: **`nvidia/RESEARCH.md`**.
Short version: open kernel modules need Turing+, Maxwell/Pascal/Volta need the proprietary driver (580 legacy branch);
`nvidia-drm.modeset/fbdev` exist in both, so the fix applies to any card with `nvidia_drm`, but nothing found says gamescope
works better or worse there (a GTX 1050 Ti has a known gamescope>3.16.16 start problem on HDMI, not ours). Cannot be promised:
needs a test on a real older card. Design for the toggle: on by default for any card with `nvidia_drm`, the label says
what it does, `tests/nvidia-hardware-test.sh` reports GPU and driver so results can be compared.
## Still to do
1. [ ] **On the NVIDIA PC** (only place the real fix can be judged; the full steps, how to read the results and the undo
are in `nvidia/INSTRUCTIONS.md`), from the steamify-cachyos checkout on that branch:
`bash tests/nvidia-hardware-test.sh check` (before), `... apply` (asks sudo), reboot,
`... check` (after: `nvidia_drm modeset/fbdev = Y`), boot gaming mode, then
`... visual` (was the picture clean?). Everything goes to `~/steamify-nvidia-report.txt`:
paste it into the session.
2. [ ] If the parameters apply but the picture is still corrupted: look at the report's
gamescope/NVIDIA log lines and the versions (`nvidia-open`, `nvidia-utils`, `gamescope`,
`gamescope-session-cachyos`); the cause is then probably a gamescope/driver mismatch
for Blackwell, not this fix.
3. [ ] Not covered by any test: the early-load drop-in with the REAL NVIDIA modules (the VMs only have renamed
fake ones), and systemd-boot/GRUB with a pacman transaction (only Limine was run).
4. [ ] **Verify the RTX 20+ check on the PC** (decided 2026-10-01, only unit-tested with a fake chwd list; this laptop and the VMs
have no `/var/lib/chwd/ids`): `bash tests/nvidia-hardware-test.sh check` prints the NVIDIA PCI id, whether it is on a chwd legacy
list, and whether the lists exist at all. Expect for the RTX 5080: "supported", "not on a legacy list". If it says "no chwd
legacy lists found" the check cannot tell old from new there (every card then counts as supported, like the VRAM booster):
look at what `chwd`/`/var/lib/chwd/ids` provide on CachyOS and whether another source (the open-driver device list, the
`nvidia-open` package) would be better for both the VRAM booster and this fix. Also try `check` on any older NVIDIA card
that turns up (GTX 10/9 series): it must say "older than RTX 20 ... not applied" and `apply` must change nothing.
5. [ ] Changelog: fill in the commit hashes of this feature's lines (next commit, per AGENTS.md).
6. [ ] Build the opt-out toggle (section above) before release.
7. [ ] When complete and tested: ask the user whether it may be released. On a yes delete
`.no-release-yet` (and this folder in the dev repo), open the PR into `release/2.9.7` (local branch only so
far, one commit `chore: Version 2.9.7`; push it then) and merge it. The user merges
`release/2.9.7` into `main`.
## Working rules (steamify-cachyos `AGENTS.md` and this repo's `AGENTS.md`)
- Never work on `main` or a `release/*` branch; no `Co-Authored-By` / "Generated with
Claude" lines in commits or PRs. No git identity on the dev laptop: use
`git -c user.name="Rick Peters" -c user.email="rickpeters@upriser.nl" commit`.
- Don't push `release/2.9.7` before the PR (a push can start a dev ISO build).
## If the VM tests need repeating (dev laptop, Ubuntu)
- `scripts/vminstall.sh` (steamify-cachyos-dev) with `VM_BOOTLOADER=limine|systemd-boot|grub`,
`CI=1`, a short `VM_DIR` (`~/vms/...`: socket paths over 108 bytes fail) and `VM_SSH_KEY`.
- This session's shell lacked the `kvm` group: run QEMU steps as `sg kvm -c "..."`.
- `/tmp/vminstall-iso.lock` belongs to another user and there is no `bsdtar`: use a copy of
`vminstall.sh` with another lock path and a `bsdtar` shim (Python `pycdlib` in a venv).
- Delete everything afterwards (`~/vms` incl. `pkg-cache`, the 3 GB ISO, keys).
-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[@]}"
+35 -2
View File
@@ -1,5 +1,38 @@
# 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)
# The VM's key, looked up at every call: vminstall.sh and vmbootloadertest.sh source this file before the key
# exists or is chosen (a clean host has no other key the VM would accept).
vm_key() {
if [[ -n "${VM_SSH_KEY:-}" ]]; then printf '%s\n' "$VM_SSH_KEY"
elif [[ -f "$VM_DIR/ssh-key" ]]; then cat "$VM_DIR/ssh-key"; fi
}
_vm_key_opts() { local k; k="$(vm_key)"; [[ -n "$k" ]] && printf '%s\n' -i "$k" -o IdentitiesOnly=yes; }
vm_ssh() { local -a k; mapfile -t k < <(_vm_key_opts); ssh -p "$VM_PORT" "${VM_SSH_OPTS[@]}" "${k[@]}" "$VM_USER@$VM_HOST" "$@"; }
vm_scp() { local -a k; mapfile -t k < <(_vm_key_opts); scp -q -P "$VM_PORT" "${VM_SSH_OPTS[@]}" "${k[@]}" "$@"; }
# 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."
+149
View File
@@ -0,0 +1,149 @@
#!/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] [--quick [--generic]] [--window]
# --install first install the VM (scripts/vminstall.sh, from the Steamify ISO)
# --quick only what the ISO's install must get right (B1-B5: boots through its own entry, OS name, loader,
# Steamify's state and modules as the install left them): no steamify.sh run, kernel update or reboots
# --generic with --quick: a plain PC, not a Steam Machine (no --fremont): its Steam Machine items must stay off
# --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; quick=false; generic=false
for a in "${@:2}"; do
case "$a" in --install) install=true ;; --quick) quick=true ;; --generic) generic=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
$generic && ! $quick && { echo "--generic needs --quick" >&2; exit 2; }
hw=fremont; $generic && hw=generic
suffix=""; $generic && suffix="-generic"
export VM_DIR="${VM_DIR:-$HOME/vms/bl-$loader$suffix}"
hwflags=(--fremont); $generic && hwflags=()
# Own ssh port per loader, so the loaders can run side by side (vmtest.sh).
off=0; $generic && off=10
case "$loader" in limine) export VM_PORT="${VM_PORT:-$((2401 + off))}" ;; systemd-boot) export VM_PORT="${VM_PORT:-$((2402 + off))}" ;; grub) export VM_PORT="${VM_PORT:-$((2403 + off))}" ;; 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" "HARDWARE=$hw" bash -s < "$guest/$1"; } # the guest's login shell is fish
# Up to ~15 minutes for the first ssh: a first boot is slow when several VMs run at once.
waitssh() { local _; for _ in $(seq 180); 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_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 "${hwflags[@]}" > 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
# --quick checks what the ISO's install leaves on a Steam Machine: install on a VM that reports Fremont hardware
# (a generic PC rightly gets no poweroff/CEC items and no DKMS modules).
fremont=(); $quick && ! $generic && fremont=(--fremont)
VM_BOOTLOADER="$loader" "$here/vminstall.sh" --iso "$iso" "${fremont[@]}" < /dev/null || exit 1
fi
step "start $loader as a $($generic && echo plain PC || echo Steam Machine)"
start_vm; waitssh || exit 1
gssh 'sudo cat /sys/class/dmi/id/product_name' 2>/dev/null
if $quick; then
step "the installed system: Steamify's state, OS name, boot loader"; gscript steamify-state.sh | check
step "Steamify's modules"; gscript modules.sh | check
step "boot and its boot entry"; gscript boot-check.sh | check
else
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.
n=0
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
(( n++ % 6 == 0 )) && echo " .. steamify.sh running: $(gssh 'tail -n 1 /tmp/steamify.log' 2>/dev/null | sed 's/\x1b\[[0-9;]*m//g' | cut -c1-110)"
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"
fi
step "DONE"
stop_vm
} 2>&1 | tee "$log" | tee >(sed -u "s/^/[$loader] /" >> "$TEST_LOG") | sed -u 's/\x1b\[[0-9;]*m//g'
rc=${PIPESTATUS[0]} # the test itself: an abort (exit inside the braces) must not pass as 0 failed checks
fails="$(grep -c '^FAIL' "$log")"
if [[ $rc -ne 0 && $fails -eq 0 ]]; then
echo "FAIL $loader aborted (exit $rc): $(grep -v '^$' "$log" | tail -n 1)" | tee -a "$log"; fails=1
fi
echo; echo "== $loader$suffix: $(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()
+226
View File
@@ -0,0 +1,226 @@
#!/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_INSTALL_MEM (RAM of the live installer VM only; default VM_MEM, else run.sh's 8G),
# 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
[[ -n "${CI:-}" ]] && export VM_HEADLESS=1 # no display in CI (QEMU fails with "OpenGL is not supported by the display")
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 bsdtar >/dev/null; then
# No loop device, no udisks, no root: libarchive reads the ISO9660 image directly (a CI container has no
# loop partition nodes, so the mounts below can't work there).
mnt="$(mktemp -d)"
cleanup_loop() { chmod -R u+rwX "$mnt" 2>/dev/null || true; rm -rf "$mnt" 2>/dev/null || true; } # the ISO's files come out read-only
trap cleanup_loop EXIT
bsdtar -xf "$iso" -C "$mnt" arch/boot/x86_64/vmlinuz-linux-cachyos arch/boot/x86_64/initramfs-linux-cachyos.img boot EFI 2>/dev/null || true
[[ -f "$mnt/arch/boot/x86_64/vmlinuz-linux-cachyos" ]] || { echo "bsdtar found no kernel in the ISO." >&2; exit 1; }
elif ! 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_MEM="${VM_INSTALL_MEM:-${VM_MEM:-}}" 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)
waited=0
for _ in $(seq 1080); do
kill -0 $qemu 2>/dev/null || break
# A sign of life every minute (CI shows nothing else for ~10 minutes): where the live installer is.
(( waited++ % 12 == 0 )) && echo " .. installing, $((waited * 5 / 60)) min: $(tail -n 1 "$www/install.log" 2>/dev/null | cut -c1-110)"
[[ -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
if [[ "$status" != ok ]]; then
echo "Install failed: $status. Log: $VM_DIR/install.log" >&2
# In CI only the job log survives: show why.
for f in vm-install.log serial.log install.log; do [[ -s "$f" ]] && { echo "--- $f (last 40 lines)" >&2; tail -n 40 "$f" | sed 's/\x1b\[[0-9;]*[A-Za-z]//g' >&2; }; done
exit 1
fi
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 (master), 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:-master}"
# 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 limine-generic systemd-boot-generic grub-generic cli menu hw installer toggles nvidia (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]=65 [menu]=37 [hw]=78 [installer]=4 [toggles]=42 [nvidia]=43 [limine-generic]=25 [systemd-boot-generic]=25 [grub-generic]=25)
once=false; jobs=()
for a in "$@"; do [[ "$a" == --once ]] && once=true || jobs+=("$a"); done
[[ ${#jobs[@]} -gt 0 ]] || jobs=(limine systemd-boot grub limine-generic systemd-boot-generic grub-generic cli menu hw installer toggles nvidia)
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|limine-generic|systemd-boot-generic|grub-generic) 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.
+2 -2
View File
@@ -16,7 +16,7 @@ export XDG_RUNTIME_DIR=/run/user/$uid DBUS_SESSION_BUS_ADDRESS=unix:path=/run/us
rm -f /tmp/vmshot.png
if [ -n "$clean" ]; then
# Steam as a whole: closing only its web helper leaves a black window.
# It comes back at the next login (steam-desktop-autostart).
# It comes back at the next login (Steam autostart entry).
steam -shutdown >/dev/null 2>&1 & sleep 5; pkill -u "$USER" -x steam; pkill -u "$USER" -x steamwebhelper
pkill -u "$USER" cachyos-hello; pkill -u "$USER" -x konsole
sleep 2
@@ -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"
+3 -3
View File
@@ -8,8 +8,8 @@ yn() { [ -e "$1" ] && echo y || echo n; }
echo "DM=$(systemctl show -p Id --value display-manager) sddm-autologin=$(yn /etc/sddm.conf.d/10-gamescope-autologin.conf) sync=$(yn /etc/systemd/system/sync-steamos-session.path) sudoers=$(sudo test -f /etc/sudoers.d/gamescope-session-switch && echo y || echo n)"
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 "steam-autostart=$(grep -qs "^Exec=.*-silent" ~/.config/autostart/steam.desktop && echo silent || ([ -f ~/.config/autostart/steam.desktop ] && echo plain || 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/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
+106
View File
@@ -0,0 +1,106 @@
#!/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 ;; nvidia) port=2306 ;; *) 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"
rc=${PIPESTATUS[0]} # an abort inside the braces must not pass as 0 failed checks
fails="$(grep -c '^FAIL' "$log")"
if [[ $rc -ne 0 && $fails -eq 0 ]]; then
echo "FAIL $suite aborted (exit $rc): $(grep -v '^$' "$log" | tail -n 1)" | tee -a "$log"; fails=1
fi
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'
+92
View File
@@ -0,0 +1,92 @@
#!/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
# generic the plain-PC install check for every loader (vmbootloadertest.sh --quick --generic: a PC that is not a
# Steam Machine must get no Steam Machine items and no DKMS modules); part of the default run
# <suite> cli, menu, hw, installer, toggles, nvidia (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 (5 worked on a 9800X3D / 64 GB); 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; want_generic=false
for a in "$@"; do
case "$a" in
--install) install=--install ;;
--window) window=--window ;;
--screen) screen=true ;;
boot) want_boot=true ;;
generic) want_generic=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 && ! $want_generic; then
want_boot=true; want_generic=true
for d in cli menu hw installer toggles nvidia; 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"
gloaders=() # the plain-PC install check: the same loaders as the boot loader test, or all advertised ones
if $want_generic; then if [[ ${#loaders[@]} -gt 0 ]]; then gloaders=("${loaders[@]}"); else read -r -a gloaders <<< "$advertised"; fi; fi
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 (4 GB RAM each in the suites, see vmsuite.sh); 5 ran fine on a 9800X3D with 64 GB
# 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
for l in "${gloaders[@]}"; do jobs_list+=("generic:$l"); done
jobtag() { [[ "${1%%:*}" == generic ]] && echo "${1#*:}-generic" || echo "${1#*:}"; } # files and names of a job
run_job() { # run_job <kind:name>: output in /tmp/vmtest-<tag>.out, exit status in /tmp/vmtest-<tag>.rc
local kind="${1%%:*}" name="${1#*:}" tag; tag="$(jobtag "$1")"
if [[ "$kind" == loader ]]; then bash "$here/vmbootloadertest.sh" "$name" $install $window > "/tmp/vmtest-$tag.out" 2>&1
elif [[ "$kind" == generic ]]; then bash "$here/vmbootloadertest.sh" "$name" $install --quick --generic $window > "/tmp/vmtest-$tag.out" 2>&1
else bash "$here/vmsuite.sh" "$name" $window > "/tmp/vmtest-$tag.out" 2>&1; fi
echo $? > "/tmp/vmtest-$tag.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
tag="$(jobtag "$job")"
sed -n '/^== /,$p' "/tmp/vmtest-$tag.out"
total=$((total + $(cat "/tmp/vmtest-$tag.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}; plain PC: ${gloaders[*]:-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."
+29
View File
@@ -0,0 +1,29 @@
#!/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)"
if [[ "${HARDWARE:-fremont}" == generic ]]; then
lsmod | grep -qE "^(steamify_fremont_poweroff|leds_valve)" && fail "Steam Machine modules loaded on a plain PC" || pass "no Steam Machine modules loaded (not a Steam Machine)"
else
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"
fi
[[ -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
+25
View File
@@ -0,0 +1,25 @@
#!/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 $*"; }
# HARDWARE=generic (a plain PC): none of Steamify's Steam Machine modules belong on it.
if [[ "${HARDWARE:-fremont}" == generic ]]; then
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 is installed for $k, but this is not a Steam Machine" || pass "no $m for $k (not a Steam Machine)"
done
done
[[ -f /etc/modules-load.d/steamify-fremont-poweroff.conf ]] && fail "modules-load.d has the power-off fix on a plain PC" || pass "no modules-load.d entry (not a Steam Machine)"
exit 0
fi
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"
+35
View File
@@ -0,0 +1,35 @@
#!/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 $(grep -oP '^for lib in \K.*(?=; do$)' steamify.sh); 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`.
# HARDWARE=generic (a plain PC): the Steam Machine items (poweroff, cec) must stay off.
for item in gaming theme glyphs single launcher notify; do
if "${item}_status" 2>/dev/null; then pass "$item is on (the menu shows it ticked)"; else fail "$item is off"; fi
done
for item in poweroff cec; do
if [[ "${HARDWARE:-fremont}" == generic ]]; then
if "${item}_status" 2>/dev/null; then fail "$item is on, but this is not a Steam Machine"; else pass "$item is off (not a Steam Machine)"; fi
else
if "${item}_status" 2>/dev/null; then pass "$item is on (the menu shows it ticked)"; else fail "$item is off"; fi
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"
+19
View File
@@ -0,0 +1,19 @@
# C1-C3: extended controller support: the repo's DKMS drivers (xone + dongle firmware, xpadneo) are installed and built for every
# installed kernel; a second run is quiet; turning it off removes them again. The VM has no dongle or controller: this covers
# the packages, the headers and the DKMS builds, not the hardware. Covers TESTPLAN C1 (on), C2 (again), C3 (off).
info "applying extended_controller_support (kernel headers and DKMS builds for every kernel: a few minutes)..."
sf --defaults --options extended_controller_support > /tmp/c1.log; rc=$?
[[ $rc -eq 0 ]] && pass "C1 exit 0" || { fail "C1 exit $rc: $(tail -n 12 /tmp/c1.log)"; exit 0; }
pacman -Q xpadneo-dkms xone-dongle-firmware >/dev/null 2>&1 && pacman -Q xone-dkms >/dev/null 2>&1 && pass "C1 xpadneo-dkms, xone-dkms and xone-dongle-firmware are installed" || fail "C1 packages: $(pacman -Qq | grep -E 'xone|xpadneo' | tr '\n' ' ')"
expect_on extended_controller_support
for kd in /usr/lib/modules/*/; do
k="$(basename "$kd")"; [[ -d "$kd/kernel" ]] || continue
n="$(dkms status -k "$k" 2>/dev/null | grep -E 'xone|xpadneo' | grep -c installed)"
[[ "$n" -ge 2 ]] && pass "C1 $k: DKMS built xone and xpadneo ($n modules installed)" || fail "C1 $k: DKMS: $(dkms status -k "$k" 2>&1 | tr '\n' ';')"
modinfo -k "$k" xone_dongle >/dev/null 2>&1 && modinfo -k "$k" hid_xpadneo >/dev/null 2>&1 && pass "C1 $k: xone_dongle and hid_xpadneo modules exist" || fail "C1 $k: modules missing"
done
sf --defaults --options extended_controller_support | grep -qiE 'nothing|already|exit: 0' && pass "C2 a second run is quiet" || info "C2 second run: $(sf --defaults --options extended_controller_support | tail -n 3)"
sf --defaults --options notify > /tmp/c3.log; rc=$?
[[ $rc -eq 0 ]] && pass "C3 turn-off exit 0" || fail "C3 exit $rc: $(tail -n 8 /tmp/c3.log)"
pacman -Q xpadneo-dkms xone-dkms xone-dongle-firmware >/dev/null 2>&1 && fail "C3 packages are still installed" || pass "C3 the three packages are removed again"
expect_off extended_controller_support
+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
+8
View File
@@ -0,0 +1,8 @@
# H3: BIOS update, the whole flow with WIZARD_BIOS_DRY_RUN=1 (never flashes). The VM reports an older BIOS.
# env: BIOS_VERSION=F7F0107
# The BIOS row's number by its name (a new menu item shifts the numbers).
n=$(printf 'q\n' | bash /mnt/steamify.sh 2>&1 | sed 's/\x1b\[[0-9;]*m//g' | sed -n 's/^ *\([0-9][0-9]*\) .*Update BIOS.*/\1/p' | head -n 1)
[[ -n "$n" ]] && pass "H3 Update BIOS is menu row $n" || { fail "H3 no Update BIOS row in the menu"; exit 0; }
out=$(printf "$n\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"
+48
View File
@@ -0,0 +1,48 @@
# 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')
# By name, not by row number (a new item shifts the numbers): the ticked ones, then the ones that are shown but never preselected.
for item in "SteamOS conversion" "SteamOS theme" "Steam Deck/Machine icons" "Single user mode" "Steamify shortcut" "Update notifications" "HDMI-CEC" "Steam Machine support" "Power-off fix"; do
grep -qE "^ +[0-9]+ +[a-z-]+ +\[x\] .*$item" <<< "$out" && pass "R1.1 $item ticked" || fail "R1.1 $item not ticked"
done
for item in "Extended controller support" "Update BIOS"; do
grep -qE "^ +[0-9]+ +[a-z-]+ +\[ \] .*$item" <<< "$out" && pass "R1.1 $item shown, not ticked" || fail "R1.1 $item missing or ticked"
done
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"
+18
View File
@@ -0,0 +1,18 @@
# N1: an NVIDIA PC (fake GPU): the menu hides the SteamOS conversion and its gamescope options, shows Gaming on NVIDIA, its
# Big Picture option and Single user mode. Covers TESTPLAN N1.
fake_nvidia
out="$(sf --defaults --list)"
ids="$(python3 -c 'import json,sys; d=json.load(sys.stdin); print(" ".join(sorted(i["id"] for i in d)))' <<< "$out" 2>/dev/null)"
[[ -n "$ids" ]] && pass "N1 --defaults --list is valid JSON: $ids" || { fail "N1 --defaults --list is not JSON: $(echo "$out" | head -c 300)"; exit 0; }
for i in nvidia bigpicture single; do [[ " $ids " == *" $i "* ]] && pass "N1 $i is offered" || fail "N1 $i is missing"; done
for i in gaming boot glyphs; do [[ " $ids " == *" $i "* ]] && fail "N1 $i is offered on an NVIDIA PC" || pass "N1 $i is hidden"; done
python3 -c '
import json,sys
d={i["id"]:i for i in json.load(sys.stdin)}
ok = d["bigpicture"]["parent"]=="nvidia" and d["nvidia"]["on"] and d["bigpicture"]["on"] and d["single"]["on"]
sys.exit(0 if ok else 1)' <<< "$out" && pass "N1 Big Picture is a sub-option of Gaming on NVIDIA, all three preselected" || fail "N1 parent or preselection wrong"
st="$(sf --backend status)"
python3 -c 'import json,sys; d=json.load(sys.stdin); ids=[i["id"] for i in d.get("items",d)]; sys.exit(1 if "gaming" in ids else 0)' <<< "$st" 2>/dev/null && pass "N1 --backend status (the app) has no conversion row" || fail "N1 --backend status lists the conversion"
# Without a NVIDIA GPU nothing changes: the conversion is offered, Gaming on NVIDIA is not.
out2="$(NVIDIA_DRM_DIR=/nonexistent sf --defaults --list)"
python3 -c 'import json,sys; ids=[i["id"] for i in json.load(sys.stdin)]; sys.exit(0 if "gaming" in ids and "nvidia" not in ids and "bigpicture" not in ids else 1)' <<< "$out2" && pass "N1 no NVIDIA GPU: the conversion is offered, Gaming on NVIDIA is not" || fail "N1 no-NVIDIA menu wrong"
@@ -0,0 +1,51 @@
# N2-N5: Gaming on NVIDIA + Big Picture + Single user mode on a fake NVIDIA PC, for real: pacman installs Steam, the SDDM autologin,
# the boot loader files and the initramfs, then a reboot into Plasma, then everything off again and a second reboot.
# Covers TESTPLAN N2 (apply), N3 (after the reboot), N4 (turn off), N5 (after the second reboot).
unit='~/.config/systemd/user/steamify-steam-autostart.service'
g() { guest "$@"; }
yes_() { [[ "$(g "$1 && echo YES || echo NO" | tail -n 1)" == YES ]]; } # a guest condition
info "N2 applying nvidia,bigpicture,single (Steam, SDDM, initramfs: several minutes)..."
rc="$(g 'fake_nvidia; sf --defaults --options nvidia,bigpicture,single > /tmp/n2.log; echo "rc=$?"' | tail -n 1)"
[[ "$rc" == rc=0 ]] && pass "N2 apply exit 0" || { fail "N2 apply: $rc: $(g 'tail -n 15 /tmp/n2.log')"; return 0 2>/dev/null || exit 0; }
yes_ 'pacman -Q steam >/dev/null 2>&1' && pass "N2 Steam is installed" || fail "N2 Steam missing"
yes_ "grep -qx 'ExecStart=/usr/bin/steam -gamepadui' $unit" && pass "N2 the autostart unit starts Steam with -gamepadui" || fail "N2 unit: $(g "cat $unit")"
yes_ 'systemctl --user is-enabled -q steamify-steam-autostart.service' && pass "N2 the unit is enabled" || fail "N2 unit not enabled"
[[ "$(g 'kreadconfig6 --file ksmserverrc --group General --key loginMode' | tail -n 1)" == emptySession ]] && pass "N2 Plasma starts with an empty session" || fail "N2 loginMode"
g 'is nvidia && is bigpicture && echo ON' | grep -qx ON && pass "N2 nvidia and bigpicture report on" || fail "N2 status functions"
yes_ "grep -q 'nvidia-drm.modeset=1 nvidia-drm.fbdev=1' /etc/default/limine" && pass "N2 the kernel parameters are in /etc/default/limine" || fail "N2 limine defaults"
yes_ "sudo grep -q 'nvidia-drm.fbdev=1' /boot/limine.conf" && pass "N2 and in the generated /boot/limine.conf" || fail "N2 limine.conf"
yes_ 'test -f /etc/mkinitcpio.conf.d/90-steamify-nvidia.conf && test -x /usr/local/libexec/steamify-nvidia-initramfs && test -f /etc/pacman.d/hooks/85-steamify-nvidia-initramfs.hook' && pass "N2 early-load file, hook and its script installed" || fail "N2 initramfs files"
yes_ 'sudo find /boot -name "initramfs*" -exec sh -c "lsinitcpio {} | grep -q nvidia" \; -print | grep -q .' && pass "N2 the NVIDIA modules are in the initramfs" || fail "N2 initramfs has no nvidia modules"
yes_ 'systemctl is-enabled -q sddm.service' && pass "N2 SDDM is the login manager" || fail "N2 sddm not enabled"
yes_ "grep -qx 'Session=plasma.desktop' /etc/sddm.conf.d/zzz-steamify-autologin.conf && grep -qx \"User=\$USER\" /etc/sddm.conf.d/zzz-steamify-autologin.conf" && pass "N2 SDDM autologin: the user into the Plasma session" || fail "N2 autologin file: $(g 'cat /etc/sddm.conf.d/zzz-steamify-autologin.conf')"
yes_ 'is single' && pass "N2 single user mode is on" || fail "N2 single not on"
g 'fake_nvidia; sf --defaults --options nvidia,bigpicture,single | tail -n 3' | grep -qiE 'nothing|already|exit: 0' && pass "N2 a second run is quiet" || info "N2 second run: $(g 'fake_nvidia; sf --defaults --options nvidia,bigpicture,single | tail -n 3')"
reboot_guest && pass "N3 the VM is back after the reboot" || { fail "N3 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 "N3 Plasma straight after the boot (SDDM autologin, no greeter)" || fail "N3 no plasmashell after the reboot"
yes_ '! pgrep -x sddm-greeter >/dev/null' && pass "N3 no greeter running" || fail "N3 sddm-greeter is running"
yes_ 'grep -q "nvidia-drm.modeset=1" /proc/cmdline && grep -q "nvidia-drm.fbdev=1" /proc/cmdline' && pass "N3 the kernel parameters are on the running command line" || fail "N3 cmdline: $(g 'cat /proc/cmdline')"
yes_ 'systemctl is-active -q sddm.service' && pass "N3 sddm is the running display manager" || fail "N3 sddm not active"
yes_ 'journalctl --user -b --no-pager -u steamify-steam-autostart.service | grep -qiE "Started|Starting"' && pass "N3 the Steam autostart unit was started at login" || fail "N3 unit never started: $(g 'systemctl --user status steamify-steam-autostart.service 2>&1 | head -n 8')"
[[ "$(g 'kreadconfig6 --file ksmserverrc --group General --key loginMode' | tail -n 1)" == emptySession ]] && pass "N3 the empty session setting survived" || fail "N3 loginMode after the reboot"
info "N4 turning everything off (only notify stays)..."
rc="$(g 'fake_nvidia; sf --defaults --options notify > /tmp/n4.log; echo "rc=$?"' | tail -n 1)"
[[ "$rc" == rc=0 ]] && pass "N4 turn-off exit 0" || { fail "N4 turn-off: $rc: $(g 'tail -n 15 /tmp/n4.log')"; return 0 2>/dev/null || exit 0; }
yes_ '! pacman -Q steam >/dev/null 2>&1' && pass "N4 Steam is removed again (Steamify installed it)" || fail "N4 Steam is still installed"
yes_ "! test -e $unit" && pass "N4 the autostart unit is gone" || fail "N4 unit still there"
[[ -z "$(g 'kreadconfig6 --file ksmserverrc --group General --key loginMode' | tail -n 1)" ]] && pass "N4 loginMode is back to not set" || fail "N4 loginMode: $(g 'kreadconfig6 --file ksmserverrc --group General --key loginMode')"
yes_ '! grep -q nvidia-drm /etc/default/limine' && pass "N4 the kernel parameters are gone from /etc/default/limine" || fail "N4 limine defaults still have them"
# Snapshot entries (snapper's, rootflags=.../.snapshots/N/snapshot) keep the command line of their time: a snapshot of the state with the
# parameters is meant to boot that state. Only the current boot entries count.
yes_ "! sudo grep 'cmdline:' /boot/limine.conf | grep -v '\.snapshots/' | grep -q nvidia-drm" && pass "N4 and from the current entries in /boot/limine.conf (snapshot entries keep their old command line)" || fail "N4 limine.conf still has them: $(g "sudo grep 'cmdline:' /boot/limine.conf | grep -v '\.snapshots/' | cut -c1-150" | tr '\n' '|')"
yes_ '! test -e /etc/mkinitcpio.conf.d/90-steamify-nvidia.conf && ! test -e /usr/local/libexec/steamify-nvidia-initramfs && ! test -e /etc/pacman.d/hooks/85-steamify-nvidia-initramfs.hook' && pass "N4 early-load file, hook and script removed" || fail "N4 initramfs files remain"
yes_ '! test -e /etc/sddm.conf.d/zzz-steamify-autologin.conf' && pass "N4 the SDDM autologin file is gone" || fail "N4 autologin file remains"
yes_ 'systemctl is-enabled -q plasmalogin.service && ! systemctl is-enabled -q sddm.service' && pass "N4 plasma-login-manager is the login manager again" || fail "N4 login manager: $(g 'systemctl show -p Id --value display-manager')"
yes_ '! is nvidia && ! is bigpicture && ! is single' && pass "N4 nvidia, bigpicture and single report off" || fail "N4 status still on"
reboot_guest && pass "N5 the VM is back after the second reboot" || { fail "N5 no ssh after the second 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 "N5 Plasma comes up (the test autologin of plasma-login-manager)" || fail "N5 no plasmashell after the second reboot"
yes_ '! grep -q "nvidia-drm" /proc/cmdline' && pass "N5 the kernel parameters are off the command line" || fail "N5 cmdline still has them"
+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; }
+38
View File
@@ -0,0 +1,38 @@
# 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 $(grep -oP '^for lib in \K.*(?=; do$)' steamify.sh); 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 nvidia bigpicture theme glyphs single launcher notify vram cec machine poweroff boot; do is "$i" && printf '%s ' "$i"; done; echo; }
# fake_nvidia: an NVIDIA PC for the nvidia suite. A fake RTX 5080 in a fake DRM tree (NVIDIA_DRM_DIR is Steamify's test hook, exported
# here: call it in every guest command that runs the wizard) and fake NVIDIA modules for every installed kernel (renamed copies of a
# tiny module, then depmod), so `modinfo nvidia_drm` finds one and the initramfs build has modules to put in.
fake_nvidia() {
local d=/var/tmp/fake-drm k kd src ext m
mkdir -p "$d/card1/device"
echo 0x10de > "$d/card1/device/vendor"; echo 0x030000 > "$d/card1/device/class"; echo 0x2c02 > "$d/card1/device/device"
for kd in /usr/lib/modules/*/; do
k="$(basename "$kd")"
[[ -d "$kd/kernel" ]] || continue
src="$(find "$kd/kernel" -name 'dummy.ko*' | head -n 1)"
[[ -n "$src" ]] || { echo "no dummy module for $k"; continue; }
ext="${src##*dummy.ko}"
for m in nvidia nvidia-modeset nvidia-uvm nvidia-drm; do
[[ -e "$kd/kernel/drivers/video/$m.ko$ext" ]] || sudo install -Dm644 "$src" "$kd/kernel/drivers/video/$m.ko$ext"
done
sudo depmod -a "$k"
done
export NVIDIA_DRM_DIR="$d"
}
+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)"
+85
View File
@@ -0,0 +1,85 @@
#!/bin/bash
# Tests lib/controllers.sh (extended controller support: xone, the Xbox wireless dongle, and xpadneo, Xbox Bluetooth) with a stub pacman
# and a temp home. No root, VM or hardware needed: bash tests/controllers-test.sh
# The Steamify checkout (REPO, default: the steamify-cachyos next to this repo)
REPO="${REPO:-$(cd "$(dirname "$0")/../../steamify-cachyos" && pwd)}"; cd "$REPO" || exit 1
T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT
fail=0
check() { if eval "$2"; then echo "ok $1"; else echo "FAIL $1"; fail=1; fi; }
SCRIPT_DIR="$PWD"; HOME="$T/home"; mkdir -p "$HOME"
source lib/common.sh; source lib/state.sh; source lib/controllers.sh; source lib/menu.sh
# Runs as the user, and refuses anything under the real system folders: a test must never touch them.
sudo() { local a; for a in "$@"; do case "$a" in /etc/*|/usr/*|/boot/*|/var/*) echo "REFUSED sudo $*" >&2; return 1 ;; esac; done; "$@"; }
info() { :; }; ok() { :; }; warn() { :; }; err() { echo "ERR $*"; }
kwriteconfig6() { local f="" k="" v=""; while [[ $# -gt 0 ]]; do case "$1" in --file) f="$2"; shift ;; --key) k="$2"; shift ;; --group) shift ;; *) v="$1" ;; esac; shift; done; mkdir -p "$(dirname "$f")"; sed -i "/^$k=/d" "$f" 2>/dev/null; printf '%s=%s\n' "$k" "$v" >> "$f"; }
kreadconfig6() { local f="" k=""; while [[ $# -gt 0 ]]; do case "$1" in --file) f="$2"; shift ;; --key) k="$2"; shift ;; --group|--default) shift ;; esac; shift; done; sed -n "s/^$k=//p" "$f" 2>/dev/null; }
# A pretend package database.
PKGS=""; PACMAN_LOG="$T/pacman.log"; : > "$PACMAN_LOG"; PACMAN_FAIL=0
pacman() {
case "$1" in
-Q) local p; for p in "${@:2}"; do [[ " $PKGS " == *" $p "* ]] || return 1; done ;;
-S) [[ $PACMAN_FAIL == 1 ]] && return 1; echo "S ${*:4}" >> "$PACMAN_LOG"; PKGS+=" ${*:4}" ;;
-Rns) echo "R ${*:3}" >> "$PACMAN_LOG"; local p; for p in ${*:3}; do PKGS=" ${PKGS// $p / } "; done ;;
esac
}
HEADERS=0; install_kernel_headers() { HEADERS=$((HEADERS + 1)); }
check "off to begin with" '! extended_controller_support_status'
extended_controller_support_enable
check "installs the repo's xpadneo-dkms, xone-dkms and the dongle firmware" 'pacman -Q xpadneo-dkms xone-dkms xone-dongle-firmware'
check "kernel headers are made sure of first" '[[ $HEADERS == 1 ]]'
check "what we installed is recorded" '[[ "$(state_get extended_controller_support installed_pkgs)" == "xpadneo-dkms xone-dkms xone-dongle-firmware" ]]'
check "it is on" extended_controller_support_status
n="$(wc -l < "$PACMAN_LOG")"; extended_controller_support_enable
check "a second run installs nothing again" '[[ "$(wc -l < "$PACMAN_LOG")" == "$n" ]]'
extended_controller_support_disable
check "off: the three packages are removed again" '! pacman -Q xpadneo-dkms 2>/dev/null && ! pacman -Q xone-dkms 2>/dev/null && ! pacman -Q xone-dongle-firmware 2>/dev/null && [[ -z "$(state_get extended_controller_support installed_pkgs)" ]]'
# The AUR's xone-dkms-git conflicts with the repo's xone-dkms: it counts, and is kept.
PKGS=" xone-dkms-git xone-dongle-firmware xpadneo-dkms"; : > "$PACMAN_LOG"
check "xone-dkms-git counts as xone: already on" extended_controller_support_status
extended_controller_support_enable; extended_controller_support_disable
check "with xone-dkms-git: nothing installed and nothing removed" '[[ ! -s "$PACMAN_LOG" ]] && pacman -Q xone-dkms-git xpadneo-dkms xone-dongle-firmware'
PKGS=" xone-dkms-git"
extended_controller_support_enable
check "xone-dkms-git present: only the missing ones are installed, never xone-dkms" 'grep -q "^S xpadneo-dkms xone-dongle-firmware$" "$PACMAN_LOG" && ! pacman -Q xone-dkms 2>/dev/null'
extended_controller_support_disable
check "and off removes only those two, xone-dkms-git stays" 'pacman -Q xone-dkms-git && ! pacman -Q xpadneo-dkms 2>/dev/null'
# A failed install is an error and records nothing.
PKGS=""; PACMAN_FAIL=1
check "a failed pacman is an error" '! extended_controller_support_enable >/dev/null'
check "...and records nothing" '[[ -z "$(state_get extended_controller_support installed_pkgs)" ]]'
PACMAN_FAIL=0
# Packages that were already there are never removed by turning it off.
PKGS=" xpadneo-dkms xone-dkms xone-dongle-firmware"; : > "$PACMAN_LOG"
extended_controller_support_enable; extended_controller_support_disable
check "packages you had before stay" 'pacman -Q xpadneo-dkms xone-dkms xone-dongle-firmware && [[ ! -s "$PACMAN_LOG" ]]'
# The menu.
check "offered on every PC" 'component_available extended_controller_support'
check "not preselected: it builds kernel modules for every kernel" '[[ " ${NO_PRESELECT[*]} " == *" extended_controller_support "* ]]'
check "a top-level item with a label" '[[ -z "${PARENT[extended_controller_support]:-}" && -n "${LABEL[extended_controller_support]}" ]]'
# The "new" badge: an opt-in option added after setup shows it, but is never ticked for you.
component_selectable() { return 0; }
declare -A CURRENT=([gaming]=1 [nvidia]=0 [extended_controller_support]=0 [vram]=0 [boot]=0)
rm -f "$STATE_DIR/features.state"
check "new opt-in option (conversion on, no record): the new badge" 'feature_new_optin extended_controller_support'
check "...but it is not ticked for you (not a new default option)" '! feature_new extended_controller_support'
check "...and the menu label says (new)" '[[ "$(menu_label extended_controller_support)" == *"(new)"* ]]'
check "a new default option is still ticked for you, and is not the opt-in kind" 'feature_new vram && ! feature_new_optin vram'
check "Boot into never gets the badge (it is a choice row)" '! feature_new_optin boot'
state_set_features() { kwriteconfig6 --file "$STATE_DIR/features.state" --group State --key "$1" "$2"; }
state_set_features extended_controller_support off
check "once a run recorded it as off, the badge is gone" '! feature_new_optin extended_controller_support'
rm -f "$STATE_DIR/features.state"
CURRENT[gaming]=0; CURRENT[nvidia]=1
check "on an NVIDIA PC (Gaming on NVIDIA on, no conversion) it shows too" 'feature_new_optin extended_controller_support'
CURRENT[nvidia]=0
check "with neither the conversion nor Gaming on NVIDIA on (a fresh PC) nothing is new" '! feature_new_optin extended_controller_support && ! feature_new vram'
CURRENT[gaming]=1; CURRENT[extended_controller_support]=1
check "already on: not new" '! feature_new_optin extended_controller_support'
exit $fail
+170
View File
@@ -0,0 +1,170 @@
== pc first run
wanted: gaming glyphs theme single launcher notify
disable:
enable: gaming glyphs theme single launcher notify
== pc first run, untick gaming
wanted: theme launcher notify
disable:
enable: theme launcher notify
== pc single on
wanted: gaming theme single launcher notify
disable:
enable: gaming theme single launcher notify
== pc single then gaming off
wanted: theme launcher notify
disable:
enable: theme launcher notify
== pc boot desktop
wanted: gaming boot glyphs theme single launcher notify
disable:
enable: gaming boot glyphs theme single launcher notify
== pc boot desktop then gaming off
wanted: theme launcher notify
disable:
enable: theme launcher notify
== pc silent only
wanted: gaming silent glyphs theme single launcher notify
disable:
enable: gaming silent glyphs theme single launcher notify
== pc all on, theme off
wanted: gaming boot silent glyphs single launcher notify
disable: theme
enable: gaming silent notify
== pc launcher off
wanted:
disable: launcher
enable:
== pc steamgame on without launcher
wanted: theme launcher steamgame notify
disable:
enable: theme launcher steamgame notify
== machine first run
wanted: gaming glyphs theme single launcher steamgame notify vram cec machine poweroff
disable:
enable: gaming glyphs theme single launcher steamgame notify vram cec machine poweroff
== machine silent hidden until boot desktop
wanted: gaming silent glyphs theme single launcher notify vram machine
disable:
enable: gaming silent glyphs theme single launcher notify vram machine
== machine boot desktop keeps silent
wanted: gaming boot silent glyphs theme single launcher notify vram machine
disable:
enable: gaming boot silent glyphs theme single launcher notify vram machine
== machine off
wanted: gaming glyphs theme single launcher notify vram
disable: poweroff machine
enable: gaming glyphs theme single launcher notify vram
== machine poweroff off
wanted: gaming glyphs theme single launcher notify vram machine
disable: poweroff
enable: gaming glyphs theme single launcher notify vram machine
== machine poweroff on brings machine
wanted: gaming glyphs theme single launcher notify vram machine poweroff
disable:
enable: gaming glyphs theme single launcher notify vram machine poweroff
== machine kpin removal
wanted: gaming glyphs theme single launcher notify vram machine poweroff
disable: kpin
enable: gaming glyphs theme single launcher notify vram machine poweroff
== nvidia first run
wanted: nvidia bigpicture theme single launcher notify
disable:
enable: nvidia bigpicture theme single launcher notify
== nvidia bigpicture off
wanted: nvidia theme single launcher notify
disable: bigpicture
enable: nvidia theme single launcher notify
== nvidia nvsilent on
wanted: nvidia nvsilent theme single launcher notify
disable: bigpicture
enable: nvidia nvsilent theme single launcher notify
== nvidia nvsilent then bigpicture
wanted: nvidia bigpicture theme single launcher notify
disable:
enable: nvidia bigpicture theme single launcher notify
== nvidia off
wanted: theme single launcher notify
disable: nvsilent bigpicture nvidia
enable: theme single launcher notify
== nvidia single
wanted: nvidia bigpicture theme launcher notify
disable:
enable: nvidia bigpicture theme launcher notify
== nvidia gaming already on
wanted: nvidia theme launcher notify
disable: gaming
enable: nvidia theme launcher notify
== outdated features
wanted: gaming glyphs theme single launcher notify
disable:
enable: gaming glyphs single launcher notify
== reapply (a)
wanted: gaming glyphs theme single launcher notify
disable:
enable: gaming glyphs theme single launcher notify
== backend_apply single
{"event":"plan","disable":[],"enable":["gaming","single"]}
== backend_apply boot
{"event":"plan","disable":[],"enable":[]}
== backend_apply poweroff
{"event":"plan","disable":[],"enable":["machine","poweroff"]}
== backend_apply silent
{"event":"plan","disable":[],"enable":["gaming","silent"]}
== backend_apply kpin
{"event":"plan","disable":[],"enable":[]}
== backend_apply gaming boot
{"event":"plan","disable":[],"enable":["gaming","boot"]}
== backend_apply launcher steamgame
{"event":"plan","disable":[],"enable":["launcher","steamgame"]}
== backend_apply nvidia bigpicture
{"event":"plan","disable":[],"enable":["nvidia","bigpicture"]}
== backend_apply nvidia nvsilent
{"event":"plan","disable":[],"enable":["nvidia","nvsilent"]}
== backend_apply nvidia bigpicture nvsilent
{"event":"plan","disable":[],"enable":["nvidia","bigpicture"]}
== backend_apply nvidia single
{"event":"plan","disable":[],"enable":["single"]}
== backend_apply nvidia boot
{"event":"plan","disable":[],"enable":[]}
== defaults_list []
{"hideWhen":"","hint":"boot into gaming mode, Steam on the desktop","id":"gaming","kind":"toggle","label":"SteamOS conversion","needs":"","on":true,"parent":"","selectable":true}
{"hideWhen":"","hint":"","id":"boot","kind":"choice","label":"Boot into the desktop instead of gaming mode","needs":"","on":false,"parent":"gaming","selectable":true}
{"hideWhen":"","hint":"in the tray at login on the desktop, no window","id":"silent","kind":"toggle","label":"Start Steam silently in desktop mode","needs":"","on":false,"parent":"gaming","selectable":true}
{"hideWhen":"","hint":"Deck button icons in gaming mode","id":"glyphs","kind":"toggle","label":"Install Steam Deck/Machine icons","needs":"","on":true,"parent":"gaming","selectable":true}
{"hideWhen":"","hint":"Vapor look (cachyos-vapor)","id":"theme","kind":"toggle","label":"Install SteamOS theme","needs":"","on":true,"parent":"","selectable":true}
{"hideWhen":"","hint":"no password, lock screen or log out (SDDM)","id":"single","kind":"toggle","label":"Single user mode","needs":"","on":true,"parent":"","selectable":true}
{"hideWhen":"","hint":"the app on the desktop, Steamify Terminal in the launcher","id":"launcher","kind":"toggle","label":"Steamify shortcut","needs":"","on":true,"parent":"","selectable":true}
{"hideWhen":"","hint":"a notification when there's a new Steamify, never updates by itself","id":"notify","kind":"toggle","label":"Update notifications","needs":"","on":true,"parent":"","selectable":true}
{"hideWhen":"","hint":"Xbox wireless dongle (xone), Xbox controllers over Bluetooth (xpadneo)","id":"extended_controller_support","kind":"toggle","label":"Extended controller support","needs":"","on":false,"parent":"","selectable":true}
{"hideWhen":"","hint":"use Steam with the TV remote, TV on/off with the PC (experimental)","id":"cec","kind":"toggle","label":"HDMI-CEC","needs":"","on":false,"parent":"","selectable":true}
== backend_status []
{"bios":null,"firstRun":true,"items":[{"hint":"boot into gaming mode, Steam on the desktop","id":"gaming","kind":"toggle","label":"SteamOS conversion","needs":"","new":false,"on":false,"parent":"","selectable":true,"update":false,"wanted":true},{"hint":"","id":"boot","kind":"choice","label":"Boot into the desktop instead of gaming mode","needs":"","new":false,"on":false,"parent":"gaming","selectable":true,"update":false,"wanted":false},{"hint":"in the tray at login on the desktop, no window","id":"silent","kind":"toggle","label":"Start Steam silently in desktop mode","needs":"","new":false,"on":false,"parent":"gaming","selectable":true,"update":false,"wanted":false},{"hint":"Deck button icons in gaming mode","id":"glyphs","kind":"toggle","label":"Install Steam Deck/Machine icons","needs":"","new":false,"on":false,"parent":"gaming","selectable":true,"update":false,"wanted":true},{"hint":"Vapor look (cachyos-vapor)","id":"theme","kind":"toggle","label":"Install SteamOS theme","needs":"","new":false,"on":false,"parent":"","selectable":true,"update":false,"wanted":true},{"hint":"no password, lock screen or log out (SDDM)","id":"single","kind":"toggle","label":"Single user mode","needs":"","new":false,"on":false,"parent":"","selectable":true,"update":false,"wanted":true},{"hint":"the app on the desktop, Steamify Terminal in the launcher","id":"launcher","kind":"toggle","label":"Steamify shortcut","needs":"","new":false,"on":false,"parent":"","selectable":true,"update":false,"wanted":true},{"hint":"a notification when there's a new Steamify, never updates by itself","id":"notify","kind":"toggle","label":"Update notifications","needs":"","new":false,"on":false,"parent":"","selectable":true,"update":false,"wanted":true},{"hint":"Xbox wireless dongle (xone), Xbox controllers over Bluetooth (xpadneo)","id":"extended_controller_support","kind":"toggle","label":"Extended controller support","needs":"","new":false,"on":false,"parent":"","selectable":true,"update":false,"wanted":false},{"hint":"use Steam with the TV remote, TV on/off with the PC (experimental)","id":"cec","kind":"toggle","label":"HDMI-CEC","needs":"","new":false,"on":false,"parent":"","selectable":true,"update":false,"wanted":false}],"pinnedKernel":"7.1.6-1","steamMachine":false,"version":"0.0.0"}
== defaults_list [fremont steam vram]
{"hideWhen":"","hint":"boot into gaming mode, Steam on the desktop","id":"gaming","kind":"toggle","label":"SteamOS conversion","needs":"","on":true,"parent":"","selectable":true}
{"hideWhen":"","hint":"","id":"boot","kind":"choice","label":"Boot into the desktop instead of gaming mode","needs":"","on":false,"parent":"gaming","selectable":true}
{"hideWhen":"","hint":"in the tray at login on the desktop, no window","id":"silent","kind":"toggle","label":"Start Steam silently in desktop mode","needs":"","on":false,"parent":"gaming","selectable":true}
{"hideWhen":"","hint":"Deck button icons in gaming mode","id":"glyphs","kind":"toggle","label":"Install Steam Deck/Machine icons","needs":"","on":true,"parent":"gaming","selectable":true}
{"hideWhen":"","hint":"Vapor look (cachyos-vapor)","id":"theme","kind":"toggle","label":"Install SteamOS theme","needs":"","on":true,"parent":"","selectable":true}
{"hideWhen":"","hint":"no password, lock screen or log out (SDDM)","id":"single","kind":"toggle","label":"Single user mode","needs":"","on":true,"parent":"","selectable":true}
{"hideWhen":"","hint":"the app on the desktop, Steamify Terminal in the launcher","id":"launcher","kind":"toggle","label":"Steamify shortcut","needs":"","on":true,"parent":"","selectable":true}
{"hideWhen":"","hint":"Steamify in your Steam library, for the controller and gaming mode","id":"steamgame","kind":"toggle","label":"Add as non-Steam game","needs":"","on":true,"parent":"launcher","selectable":true}
{"hideWhen":"","hint":"a notification when there's a new Steamify, never updates by itself","id":"notify","kind":"toggle","label":"Update notifications","needs":"","on":true,"parent":"","selectable":true}
{"hideWhen":"","hint":"Xbox wireless dongle (xone), Xbox controllers over Bluetooth (xpadneo)","id":"extended_controller_support","kind":"toggle","label":"Extended controller support","needs":"","on":false,"parent":"","selectable":true}
{"hideWhen":"","hint":"the game in front keeps its VRAM, background apps make room","id":"vram","kind":"toggle","label":"VRAM booster","needs":"","on":true,"parent":"","selectable":true}
{"hideWhen":"","hint":"use Steam with the TV remote, TV on/off with the PC (experimental)","id":"cec","kind":"toggle","label":"HDMI-CEC","needs":"","on":true,"parent":"","selectable":true}
{"hideWhen":"","hint":"LED bar driver, hardware settings in Steam","id":"machine","kind":"toggle","label":"Steam Machine support","needs":"","on":true,"parent":"","selectable":true}
{"hideWhen":"","hint":"the Steam Machine stays off after shutting down","id":"poweroff","kind":"toggle","label":"Power-off fix","needs":"","on":true,"parent":"machine","selectable":true}
== backend_status [fremont steam vram]
{"bios":null,"firstRun":true,"items":[{"hint":"boot into gaming mode, Steam on the desktop","id":"gaming","kind":"toggle","label":"SteamOS conversion","needs":"","new":false,"on":false,"parent":"","selectable":true,"update":false,"wanted":true},{"hint":"","id":"boot","kind":"choice","label":"Boot into the desktop instead of gaming mode","needs":"","new":false,"on":false,"parent":"gaming","selectable":true,"update":false,"wanted":false},{"hint":"in the tray at login on the desktop, no window","id":"silent","kind":"toggle","label":"Start Steam silently in desktop mode","needs":"","new":false,"on":false,"parent":"gaming","selectable":true,"update":false,"wanted":false},{"hint":"Deck button icons in gaming mode","id":"glyphs","kind":"toggle","label":"Install Steam Deck/Machine icons","needs":"","new":false,"on":false,"parent":"gaming","selectable":true,"update":false,"wanted":true},{"hint":"Vapor look (cachyos-vapor)","id":"theme","kind":"toggle","label":"Install SteamOS theme","needs":"","new":false,"on":false,"parent":"","selectable":true,"update":false,"wanted":true},{"hint":"no password, lock screen or log out (SDDM)","id":"single","kind":"toggle","label":"Single user mode","needs":"","new":false,"on":false,"parent":"","selectable":true,"update":false,"wanted":true},{"hint":"the app on the desktop, Steamify Terminal in the launcher","id":"launcher","kind":"toggle","label":"Steamify shortcut","needs":"","new":false,"on":false,"parent":"","selectable":true,"update":false,"wanted":true},{"hint":"Steamify in your Steam library, for the controller and gaming mode","id":"steamgame","kind":"toggle","label":"Add as non-Steam game","needs":"","new":false,"on":false,"parent":"launcher","selectable":true,"update":false,"wanted":true},{"hint":"a notification when there's a new Steamify, never updates by itself","id":"notify","kind":"toggle","label":"Update notifications","needs":"","new":false,"on":false,"parent":"","selectable":true,"update":false,"wanted":true},{"hint":"Xbox wireless dongle (xone), Xbox controllers over Bluetooth (xpadneo)","id":"extended_controller_support","kind":"toggle","label":"Extended controller support","needs":"","new":false,"on":false,"parent":"","selectable":true,"update":false,"wanted":false},{"hint":"the game in front keeps its VRAM, background apps make room","id":"vram","kind":"toggle","label":"VRAM booster","needs":"","new":false,"on":false,"parent":"","selectable":true,"update":false,"wanted":true},{"hint":"use Steam with the TV remote, TV on/off with the PC (experimental)","id":"cec","kind":"toggle","label":"HDMI-CEC","needs":"","new":false,"on":false,"parent":"","selectable":true,"update":false,"wanted":true},{"hint":"LED bar driver, hardware settings in Steam","id":"machine","kind":"toggle","label":"Steam Machine support","needs":"","new":false,"on":false,"parent":"","selectable":true,"update":false,"wanted":true},{"hint":"the Steam Machine stays off after shutting down","id":"poweroff","kind":"toggle","label":"Power-off fix","needs":"","new":false,"on":false,"parent":"machine","selectable":true,"update":false,"wanted":true}],"pinnedKernel":"7.1.6-1","steamMachine":true,"version":"0.0.0"}
== defaults_list [nvidia steam]
{"hideWhen":"","hint":"Steam on the desktop, started at login","id":"nvidia","kind":"toggle","label":"Gaming on NVIDIA","needs":"","on":true,"parent":"","selectable":true}
{"hideWhen":"","hint":"the controller-friendly Steam (untick: Steam's normal window)","id":"bigpicture","kind":"toggle","label":"Steam starts in Big Picture","needs":"","on":true,"parent":"nvidia","selectable":true}
{"hideWhen":"bigpicture","hint":"in the tray at login, no window (untick: Steam's normal window)","id":"nvsilent","kind":"toggle","label":"Start Steam silently","needs":"","on":false,"parent":"nvidia","selectable":true}
{"hideWhen":"","hint":"Vapor look (cachyos-vapor)","id":"theme","kind":"toggle","label":"Install SteamOS theme","needs":"","on":true,"parent":"","selectable":true}
{"hideWhen":"","hint":"no password, lock screen or log out (SDDM)","id":"single","kind":"toggle","label":"Single user mode","needs":"","on":true,"parent":"","selectable":true}
{"hideWhen":"","hint":"the app on the desktop, Steamify Terminal in the launcher","id":"launcher","kind":"toggle","label":"Steamify shortcut","needs":"","on":true,"parent":"","selectable":true}
{"hideWhen":"","hint":"a notification when there's a new Steamify, never updates by itself","id":"notify","kind":"toggle","label":"Update notifications","needs":"","on":true,"parent":"","selectable":true}
{"hideWhen":"","hint":"Xbox wireless dongle (xone), Xbox controllers over Bluetooth (xpadneo)","id":"extended_controller_support","kind":"toggle","label":"Extended controller support","needs":"","on":false,"parent":"","selectable":true}
{"hideWhen":"","hint":"use Steam with the TV remote, TV on/off with the PC (experimental)","id":"cec","kind":"toggle","label":"HDMI-CEC","needs":"","on":false,"parent":"","selectable":true}
== backend_status [nvidia steam]
{"bios":null,"firstRun":true,"items":[{"hint":"Steam on the desktop, started at login","id":"nvidia","kind":"toggle","label":"Gaming on NVIDIA","needs":"","new":false,"on":false,"parent":"","selectable":true,"update":false,"wanted":true},{"hint":"the controller-friendly Steam (untick: Steam's normal window)","id":"bigpicture","kind":"toggle","label":"Steam starts in Big Picture","needs":"","new":false,"on":false,"parent":"nvidia","selectable":true,"update":false,"wanted":true},{"hint":"in the tray at login, no window (untick: Steam's normal window)","id":"nvsilent","kind":"toggle","label":"Start Steam silently","needs":"","new":false,"on":false,"parent":"nvidia","selectable":true,"update":false,"wanted":false},{"hint":"Vapor look (cachyos-vapor)","id":"theme","kind":"toggle","label":"Install SteamOS theme","needs":"","new":false,"on":false,"parent":"","selectable":true,"update":false,"wanted":true},{"hint":"no password, lock screen or log out (SDDM)","id":"single","kind":"toggle","label":"Single user mode","needs":"","new":false,"on":false,"parent":"","selectable":true,"update":false,"wanted":true},{"hint":"the app on the desktop, Steamify Terminal in the launcher","id":"launcher","kind":"toggle","label":"Steamify shortcut","needs":"","new":false,"on":false,"parent":"","selectable":true,"update":false,"wanted":true},{"hint":"a notification when there's a new Steamify, never updates by itself","id":"notify","kind":"toggle","label":"Update notifications","needs":"","new":false,"on":false,"parent":"","selectable":true,"update":false,"wanted":true},{"hint":"Xbox wireless dongle (xone), Xbox controllers over Bluetooth (xpadneo)","id":"extended_controller_support","kind":"toggle","label":"Extended controller support","needs":"","new":false,"on":false,"parent":"","selectable":true,"update":false,"wanted":false},{"hint":"use Steam with the TV remote, TV on/off with the PC (experimental)","id":"cec","kind":"toggle","label":"HDMI-CEC","needs":"","new":false,"on":false,"parent":"","selectable":true,"update":false,"wanted":false}],"pinnedKernel":"7.1.6-1","steamMachine":false,"version":"0.0.0"}
+135
View File
@@ -0,0 +1,135 @@
#!/bin/bash
# Golden-output test for the menu rules (lib/menu.sh, lib/backend.sh): what
# toggling does to WANTED, what plan_changes picks, and the JSON the app and
# the installer read. Hardware and package state are stubbed, so no root, VM
# or hardware is needed.
# bash tests/menu-test.sh compare with menu-test.golden
# bash tests/menu-test.sh --update rewrite the golden file (review the diff!)
# The Steamify checkout (REPO, default: the steamify-cachyos next to this repo)
REPO="${REPO:-$(cd "$(dirname "$0")/../../steamify-cachyos" && pwd)}"; HERE="$(cd "$(dirname "$0")" && pwd)"; cd "$REPO" || exit 1
T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT
SCRIPT_DIR="$PWD"; HOME="$T/home"; mkdir -p "$HOME"
VERSION=0.0.0
for lib in $(grep -oP '^for lib in \K.*(?=; do$)' steamify.sh); do
source "lib/$lib.sh"
done
info() { :; }; ok() { :; }; warn() { :; }; err() { :; }
# From the entry point.
RESTART_FOR_LOGIN=false; restart_needed() { return 1; }
# Hardware profile and what is "on", set per scenario.
HW=""; ON=""
has() { [[ " $HW " == *" $1 "* ]]; }
nvidia_present() { has nvidia; }
nvidia_available() { has nvidia; }
bigpicture_available() { has nvidia; }
machine_available() { has fremont; }
detect_valve_fremont() { has fremont; }
vram_available() { has vram; }
vram_selectable() { return 0; }
steamgame_available() { has steam; }
kpin_available() { has fremont && [[ " $ON " == *" kpin "* ]]; }
hdmi_available() { return 1; }
bios_available() { return 1; }
bios_selectable() { return 1; }
cec_repair() { return 1; }
launcher_repair() { return 1; }
bios_lookup_newest() { :; }
kernel_overview() { :; }
os_version_refresh() { :; }
for c in "${COMPONENTS[@]}"; do
eval "${c}_status() { [[ \" \$ON \" == *\" $c \"* ]]; }"
done
show() {
# show <label>: WANTED as the ticked ids, then the plan.
local c w=""
for c in "${COMPONENTS[@]}"; do [[ "${WANTED[$c]:-0}" == 1 ]] && w+=" $c"; done
echo "$1"
echo " wanted:$w"
plan_changes
echo " disable: ${TO_DISABLE[*]:-}"
echo " enable: ${TO_ENABLE[*]:-}"
}
scenario() {
# scenario <name> <hardware> <on> [toggle...]
local name="$1"; HW="$2"; ON="$3"; shift 3
rm -rf "$STATE_DIR"; REAPPLY=false
detect_components
local t
for t in "$@"; do toggle_component "$t"; done
show "== $name"
}
run() {
scenario "pc first run" "" ""
scenario "pc first run, untick gaming" "" "" gaming
scenario "pc single on" "" "" gaming single
scenario "pc single then gaming off" "" "" single gaming
scenario "pc boot desktop" "" "" boot
scenario "pc boot desktop then gaming off" "" "" boot gaming
scenario "pc silent only" "" "gaming" silent
scenario "pc all on, theme off" "" "gaming boot silent theme glyphs single launcher steamgame notify" theme
scenario "pc launcher off" "" "launcher steamgame" launcher
scenario "pc steamgame on without launcher" "steam" "" gaming launcher steamgame
scenario "machine first run" "fremont steam vram" ""
scenario "machine silent hidden until boot desktop" "fremont steam vram" "gaming silent"
scenario "machine boot desktop keeps silent" "fremont steam vram" "gaming silent" boot
scenario "machine off" "fremont steam vram" "gaming machine poweroff" machine
scenario "machine poweroff off" "fremont steam vram" "gaming machine poweroff" poweroff
scenario "machine poweroff on brings machine" "fremont steam vram" "gaming" poweroff
scenario "machine kpin removal" "fremont steam vram" "gaming machine poweroff kpin"
scenario "nvidia first run" "nvidia steam" ""
scenario "nvidia bigpicture off" "nvidia steam" "nvidia bigpicture" bigpicture
scenario "nvidia nvsilent on" "nvidia steam" "nvidia bigpicture" nvsilent
scenario "nvidia nvsilent then bigpicture" "nvidia steam" "nvidia" nvsilent bigpicture
scenario "nvidia off" "nvidia steam" "nvidia bigpicture nvsilent" nvidia
scenario "nvidia single" "nvidia steam" "" single
scenario "nvidia gaming already on" "nvidia steam" "gaming" gaming
# Feature versions: an old record is ticked and re-applied.
HW=""; ON="gaming theme"; rm -rf "$STATE_DIR"; REAPPLY=false
state_set features gaming 2.1.0; state_set features theme 2.1.0
detect_components; show "== outdated features"
REAPPLY=true; ON="gaming theme launcher"; detect_components; show "== reapply (a)"
REAPPLY=false
# The app's own rules (backend_apply's normalisation) go through the same plan.
HW="fremont steam vram"; ON=""; rm -rf "$STATE_DIR"
detect_components
local sel
for sel in "single" "boot" "poweroff" "silent" "kpin" "gaming boot" "launcher steamgame"; do
echo "== backend_apply $sel"
backend_sudo() { return 0; }
backend_run_component() { return 0; }
# shellcheck disable=SC2086
backend_apply $sel | jq -c 'select(.event == "plan")'
done
HW="nvidia steam"; ON=""; detect_components
for sel in "bigpicture" "nvsilent" "bigpicture nvsilent" "single" "boot"; do
echo "== backend_apply nvidia $sel"
# shellcheck disable=SC2086
backend_apply $sel | jq -c 'select(.event == "plan")'
done
# JSON for the app and the installer page.
local hw
for hw in "" "fremont steam vram" "nvidia steam"; do
HW="$hw"; ON=""; rm -rf "$STATE_DIR"; detect_components
echo "== defaults_list [$hw]"
defaults_list | jq -S -c '.[]'
echo "== backend_status [$hw]"
backend_status | jq -S -c 'del(.kernel, .cecDevices, .leds)'
done
}
if [[ "${1:-}" == --update ]]; then
run > "$HERE/menu-test.golden" 2>&1
echo "wrote $HERE/menu-test.golden ($(wc -l < "$HERE/menu-test.golden") lines)"
elif diff -u "$HERE/menu-test.golden" <(run 2>&1); then
echo "ok menu rules, plan and JSON match the golden file"
else
echo "FAIL menu rules changed (diff above; --update if intended)"; exit 1
fi
+96
View File
@@ -0,0 +1,96 @@
#!/bin/bash
# Test of the NVIDIA fix on a real PC with an NVIDIA GPU (the unit test fakes
# one: tests/nvidia-test.sh). Run from this repo (REPO points at the Steamify checkout):
# bash tests/nvidia-hardware-test.sh check read-only: what is detected, what the live kernel uses, what apply would change
# bash tests/nvidia-hardware-test.sh apply really applies the fix (sudo), then reboot and run check again
# bash tests/nvidia-hardware-test.sh visual after trying gaming mode: was the picture clean? (asks you)
# Every run appends to ~/steamify-nvidia-report.txt: paste that file when asking for help.
# The Steamify checkout (REPO, default: the steamify-cachyos next to this repo)
REPO="${REPO:-$(cd "$(dirname "$0")/../../steamify-cachyos" && pwd)}"; cd "$REPO" || exit 1
# patch_file (lib/common.sh) reads the hook script from $SCRIPT_DIR/patches.
SCRIPT_DIR="$PWD"
REPORT="${NVIDIA_REPORT:-$HOME/steamify-nvidia-report.txt}"
source lib/common.sh; source lib/hdmi-refresh.sh; source lib/vram-booster.sh; source lib/nvidia.sh
mode="${1:-check}"
fail=0
say() { echo "$*" | tee -a "$REPORT"; }
pass() { say "PASS $*"; }
bad() { say "FAIL $*"; fail=1; }
note() { say " $*"; }
root_cat() { cat "$1" 2>/dev/null || sudo cat "$1" 2>/dev/null || echo "(unreadable)"; }
say ""; say "===== $(date '+%F %T') mode=$mode kernel=$(uname -r)"
report_system() {
say "-- GPU and software"
lspci -nn 2>/dev/null | grep -iE 'vga|3d|display' | while read -r l; do note "$l"; done
command -v nvidia-smi >/dev/null && note "driver: $(nvidia-smi --query-gpu=name,driver_version --format=csv,noheader 2>&1 | head -n 1)"
for p in nvidia-open nvidia-utils nvidia-dkms gamescope gamescope-session-cachyos linux-cachyos-nvidia-open; do
v="$(pacman -Q "$p" 2>/dev/null)" && note "$v"
done
note "boot loader file: $(nvidia_boot_file || true)"
note "cmdline: $(cat /proc/cmdline)"
}
check_state() {
say "-- Steamify's view"
if nvidia_present; then pass "a supported NVIDIA GPU (RTX 20 series or newer) with its driver is detected"
elif nvidia_legacy_gpu; then bad "the NVIDIA GPU is older than RTX 20 (on chwd's legacy lists in $VRAM_CHWD_IDS): the fix is not applied"
else bad "no NVIDIA GPU with its driver detected: the fix does nothing here"; fi
for c in /sys/class/drm/card[0-9]*/device; do
[[ "$(cat "$c/vendor" 2>/dev/null)" == 0x10de ]] && note "NVIDIA PCI device id $(cat "$c/device") $(vram_nvidia_legacy_id "$(cat "$c/device")" && echo '(on a chwd legacy list)' || echo '(not on a legacy list: RTX 20 or newer)')"
done
ls "$VRAM_CHWD_IDS"/nvidia-*.ids >/dev/null 2>&1 || note "no chwd legacy lists found: every NVIDIA card counts as supported"
local p
for p in $NVIDIA_PARAMS; do
if grep -qw -- "$p" /proc/cmdline; then pass "$p is on the running kernel's command line"
elif nvidia_param_set "$p"; then bad "$p is in the boot loader's file but not booted yet: reboot"
else bad "$p is missing (run: bash tests/nvidia-hardware-test.sh apply)"; fi
done
say "-- the live NVIDIA driver"
local m
for m in modeset fbdev; do
v="$(root_cat /sys/module/nvidia_drm/parameters/$m)"
if [[ "$v" == Y ]]; then pass "nvidia_drm $m = Y"; else bad "nvidia_drm $m = $v (want Y)"; fi
done
lsmod | grep -q '^nvidia_drm' && pass "nvidia_drm is loaded" || bad "nvidia_drm is not loaded"
local c found=0
for c in /sys/class/drm/card[0-9]*/device; do
[[ "$(cat "$c/vendor" 2>/dev/null)" == 0x10de ]] || continue
found=1; note "NVIDIA card: ${c%/device} driver=$(basename "$(readlink -f "$c/driver" 2>/dev/null)")"
for s in "${c%/device}"/card*-*/status; do [[ -f "$s" ]] && note " $(basename "$(dirname "$s")"): $(cat "$s")"; done
done
[[ $found == 1 ]] || bad "no NVIDIA card in /sys/class/drm"
say "-- initramfs"
if nvidia_modules_everywhere; then note "every installed kernel has nvidia_drm: the early-load drop-in is used"
else note "not every installed kernel has nvidia_drm: the early-load drop-in is skipped (parameters still apply)"; fi
nvidia_initramfs_ok && note "NVIDIA modules are in the initramfs config" || note "NVIDIA modules are not in the initramfs config"
say "-- what apply would change"
if nvidia_params_missing; then note "adds $NVIDIA_PARAMS to $(nvidia_boot_file)"; else note "kernel parameters: nothing to change"; fi
if ! nvidia_initramfs_ok && nvidia_modules_everywhere; then note "writes $NVIDIA_INITRAMFS_CONF"; fi
}
case "$mode" in
check)
report_system; check_state ;;
apply)
report_system
say "-- applying"
if nvidia_enable 2>&1 | tee -a "$REPORT"; [[ ${PIPESTATUS[0]} == 0 ]]; then pass "nvidia_enable finished"
else bad "nvidia_enable failed (output above)"; fi
say "Now reboot, then run: bash tests/nvidia-hardware-test.sh check" ;;
visual)
report_system
say "-- try gaming mode now (log in to it or reboot into it), look at the screen, then answer"
read -rp "Is the gaming mode picture clean (no corruption, no flicker)? [y/n/partly] " a
read -rp "What did you see (one line, e.g. 'garbled colours at start, fine after login')? " what
say "visual result: $a: $what"
[[ "$a" == y ]] && pass "picture clean" || bad "picture not clean ($a)"
say "-- gamescope and NVIDIA lines from this boot's log"
{ journalctl -b --no-pager 2>/dev/null || sudo journalctl -b --no-pager 2>/dev/null; } |
grep -iE 'gamescope|nvidia|drm|nvrm|wlserver|vulkan' | tail -n 80 | tee -a "$REPORT" | tail -n 5
note "(more in $REPORT)" ;;
*) echo "usage: $0 check|apply|visual"; exit 2 ;;
esac
say ""; say "Report: $REPORT"
exit $fail
+207
View File
@@ -0,0 +1,207 @@
#!/bin/bash
# Tests lib/nvidia.sh ("Gaming on NVIDIA" and its Big Picture option) and the menu rules around it, against a fake RTX 5080
# (PCI 10de:2c02) beside an Intel iGPU, a stub modinfo, pacman and systemctl, and a temp home. No root, VM or NVIDIA
# hardware needed: bash tests/nvidia-test.sh
# The Steamify checkout (REPO, default: the steamify-cachyos next to this repo)
REPO="${REPO:-$(cd "$(dirname "$0")/../../steamify-cachyos" && pwd)}"; cd "$REPO" || exit 1
T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT
fail=0
check() { if eval "$2"; then echo "ok $1"; else echo "FAIL $1"; fail=1; fi; }
SCRIPT_DIR="$PWD"; HOME="$T/home"; mkdir -p "$HOME"
source lib/common.sh; source lib/state.sh; source lib/login-manager.sh; source lib/single-user.sh; source lib/hdmi-refresh.sh; source lib/vram-booster.sh; source lib/nvidia.sh; source lib/menu.sh
# Runs as the user, and refuses anything under the real system folders: a test must never touch them.
sudo() { local a; for a in "$@"; do case "$a" in /etc/*|/usr/*|/boot/*|/var/*) echo "REFUSED sudo $*" >&2; return 1 ;; esac; done; "$@"; }
info() { :; }; ok() { :; }; warn() { :; }; err() { echo "ERR $*"; }
# KDE's config tools on plain files under the temp home (a relative file name is relative to ~/.config, as in KDE).
kwriteconfig6() {
local f="" k="" v="" del=0
while [[ $# -gt 0 ]]; do case "$1" in --file) f="$2"; shift ;; --key) k="$2"; shift ;; --group) shift ;; --delete) del=1 ;; *) v="$1" ;; esac; shift; done
[[ "$f" == /* ]] || f="$HOME/.config/$f"; mkdir -p "$(dirname "$f")"
if [[ $del == 1 ]]; then sed -i "/^$k=/d" "$f" 2>/dev/null; else sed -i "/^$k=/d" "$f" 2>/dev/null; printf '%s=%s\n' "$k" "$v" >> "$f"; fi
}
kreadconfig6() {
local f="" k="" d="" r
while [[ $# -gt 0 ]]; do case "$1" in --file) f="$2"; shift ;; --key) k="$2"; shift ;; --default) d="$2"; shift ;; --group) shift ;; esac; shift; done
[[ "$f" == /* ]] || f="$HOME/.config/$f"
r="$(sed -n "s/^$k=//p" "$f" 2>/dev/null)"; printf '%s\n' "${r:-$d}"
}
# A pretend package database and systemd: what the tests assert on.
PKGS=""; PACMAN_LOG="$T/pacman.log"; : > "$PACMAN_LOG"; ENABLED=0
pacman() {
case "$1" in
-Q) [[ " $PKGS " == *" $2 "* ]] ;;
-S) echo "S $*" >> "$PACMAN_LOG"; PKGS+=" ${*: -1}" ;;
-Rns) echo "R $*" >> "$PACMAN_LOG"; PKGS=" ${PKGS//${*: -1}/} " ;;
esac
}
user_systemctl() {
case "$1" in
enable) ENABLED=1 ;;
disable) ENABLED=0 ;;
is-enabled) [[ $ENABLED == 1 ]] ;;
esac
return 0
}
FAKE_DRIVER=1; modinfo() { [[ "$FAKE_DRIVER" == 1 ]]; }
GAMING_ON=0; gaming_status() { [[ "$GAMING_ON" == 1 ]]; }
UNIT="$HOME/.config/systemd/user/$NVIDIA_UNIT"
mkdir -p "$T/drm/card0/device" "$T/drm/card1/device"
echo 0x8086 > "$T/drm/card0/device/vendor"; echo 0x030000 > "$T/drm/card0/device/class"
echo 0x10de > "$T/drm/card1/device/vendor"; echo 0x030000 > "$T/drm/card1/device/class"
echo 0x2c02 > "$T/drm/card1/device/device"
NVIDIA_DRM_DIR="$T/drm"
backup_file() { [[ -f "$1.bak" ]] || cp "$1" "$1.bak"; }
REBUILDS=0; nvidia_rebuild_boot() { REBUILDS=$((REBUILDS + 1)); }
: > "$T/cmdline"; NVIDIA_CMDLINE="$T/cmdline" # the running kernel's command line: without the parameters
NVIDIA_INITRAMFS_CONF="$T/90-steamify-nvidia.conf"
NVIDIA_SCRIPT="$T/libexec/steamify-nvidia-initramfs"; NVIDIA_HOOK="$T/hooks/85-steamify-nvidia-initramfs.hook"
mkdir -p "$T/mods/6.1.0-cachyos/kernel"; : > "$T/mods/6.1.0-cachyos/kernel/nvidia-drm.ko.zst"; : > "$T/mods/6.1.0-cachyos/pkgbase"
NVIDIA_MODULES_DIR="$T/mods"
NVIDIA_LIMINE_CONF="$T/none/limine.conf"; NVIDIA_SDBOOT_DIR="$T/none/entries"; NVIDIA_GRUB_CFG="$T/none/grub.cfg" # not generated here: not checked
mkdir -p "$T/chwd"; VRAM_CHWD_IDS="$T/chwd"
# The component's kernel step edits this boot loader file (systemd-boot's) in the tests, never a real one.
printf 'LINUX_OPTIONS="quiet"\n' > "$T/sdboot-manage.conf"; nvidia_boot_file() { echo "$T/sdboot-manage.conf"; }
# --- detection
check "finds the NVIDIA card as card1 beside an iGPU" nvidia_present
FAKE_DRIVER=0; check "no nvidia_drm module (nouveau): not an NVIDIA PC" '! nvidia_present'
FAKE_DRIVER=1; NVIDIA_DRM_DIR="$T/none"; check "no NVIDIA GPU: not an NVIDIA PC" '! nvidia_present'
NVIDIA_DRM_DIR="$T/drm"
# --- which items the menu shows
check "NVIDIA PC: the SteamOS conversion and its gamescope options are hidden" '! component_available gaming && ! component_available boot && ! component_available glyphs'
check "NVIDIA PC: Gaming on NVIDIA and Big Picture are shown" 'component_available nvidia && component_available bigpicture'
GAMING_ON=1
check "NVIDIA PC with the conversion already on: it is shown, so it can be turned off" 'component_available gaming && component_available boot'
check "...and Gaming on NVIDIA is hidden (both would start Steam)" '! component_available nvidia && ! component_available bigpicture'
GAMING_ON=0; NVIDIA_DRM_DIR="$T/none"
check "no NVIDIA: the conversion is shown as before" 'component_available gaming && component_available boot'
check "...and Gaming on NVIDIA is hidden" '! component_available nvidia && ! component_available bigpicture'
NVIDIA_DRM_DIR="$T/drm"
# --- "Add as non-Steam game" is for gaming mode's controller: not offered on an NVIDIA PC (unless already on)
steamgame_available() { return 0; }; STEAMGAME_ON=0; steamgame_status() { [[ "$STEAMGAME_ON" == 1 ]]; }
check "NVIDIA PC: Add as non-Steam game is hidden" '! component_available steamgame'
STEAMGAME_ON=1; check "...unless it is already on (so it can be turned off)" 'component_available steamgame'; STEAMGAME_ON=0
NVIDIA_DRM_DIR="$T/none"; check "no NVIDIA: it is offered as before" 'component_available steamgame'
steamgame_available() { return 1; }; check "no NVIDIA, no Steam account yet: still not offered" '! component_available steamgame'
steamgame_available() { return 0; }; NVIDIA_DRM_DIR="$T/drm"
# --- turning it on
check "off to begin with" '! nvidia_status && ! bigpicture_status'
nvidia_enable
check "Steam is installed when missing, and recorded as ours" 'grep -q "^S " "$PACMAN_LOG" && [[ "$(state_get nvidia installed_pkgs)" == steam ]]'
check "the autostart unit is written: Steam without arguments" 'grep -q "^ExecStart=/usr/bin/steam $" "$UNIT"'
check "it is on" 'nvidia_status && ! bigpicture_status'
n="$(wc -l < "$PACMAN_LOG")"; nvidia_enable
check "a second run installs nothing again" '[[ "$(wc -l < "$PACMAN_LOG")" == "$n" ]]'
# --- Big Picture
bigpicture_enable
check "Big Picture: Steam starts with -gamepadui" 'grep -q "^ExecStart=/usr/bin/steam -gamepadui$" "$UNIT" && bigpicture_status'
nvidia_enable
check "turning Gaming on NVIDIA on again keeps Big Picture" 'bigpicture_status'
check "Big Picture: Plasma starts with an empty session" '[[ "$(kreadconfig6 --file ksmserverrc --group General --key loginMode)" == emptySession ]]'
bigpicture_disable
check "Big Picture off: Steam's normal window again" '! bigpicture_status && nvidia_status'
check "...and the session setting is back as it was (not set)" '[[ -z "$(kreadconfig6 --file ksmserverrc --group General --key loginMode)" ]]'
# --- turning it off
bigpicture_enable; nvidia_disable
check "off: the unit is gone and Steam, which we installed, is removed" '! nvidia_status && [[ ! -e "$UNIT" ]] && ! pacman -Q steam && [[ -z "$(state_get nvidia installed_pkgs)" ]]'
check "Big Picture without the unit does nothing" 'bigpicture_enable && bigpicture_disable && [[ ! -e "$UNIT" ]]'
# --- Steam that was there already is never removed
PKGS=" steam"; : > "$PACMAN_LOG"
nvidia_enable; nvidia_disable
check "Steam installed before is kept, and not installed again" '[[ ! -s "$PACMAN_LOG" ]] && pacman -Q steam'
# --- kernel parameters and early modules (a supported card only: RTX 20 series or newer)
check "RTX 5080 (2c02) is supported: no chwd legacy lists" nvidia_supported
printf '1c03\n1b80\n' > "$T/chwd/nvidia-580.ids" # a GTX 1060 and a GTX 1080: chwd's 580xx legacy list
check "RTX 5080 is not on a legacy list" 'nvidia_supported && ! nvidia_legacy_gpu'
echo 0x1c03 > "$T/drm/card1/device/device"
check "GTX 1060 (older than RTX 20): no kernel setup, but still an NVIDIA PC" '! nvidia_supported && nvidia_legacy_gpu && nvidia_present'
echo 0x2c02 > "$T/drm/card1/device/device"
for loader in limine limineplus sdboot grub; do
case $loader in
limine) F="$T/limine"; printf 'KERNEL_CMDLINE[default]="quiet splash"\n' > "$F" ;;
# CachyOS writes its line as "+=" and may already have nvidia_drm.modeset=1 in it.
limineplus) F="$T/limine"; printf 'KERNEL_CMDLINE[default]+="quiet splash nvidia_drm.modeset=1"\n' > "$F" ;;
sdboot) F="$T/sdboot-manage.conf"; printf 'LINUX_OPTIONS="quiet splash"\n' > "$F" ;;
grub) F="$T/grub"; printf 'GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"\n' > "$F" ;;
esac
nvidia_boot_file() { echo "$F"; }
rm -f "$NVIDIA_INITRAMFS_CONF"; REBUILDS=0; ORIG="$(cat "$F")"
nvidia_kernel_enable
check "$loader: parameters added" 'grep -q "nvidia-drm.modeset=1" "$F" && grep -q "nvidia-drm.fbdev=1" "$F"'
check "$loader: initramfs drop-in written" 'grep -q nvidia_drm "$NVIDIA_INITRAMFS_CONF"'
check "$loader: script and pacman hook installed" '[[ -x "$NVIDIA_SCRIPT" ]] && grep -q "^Exec = $NVIDIA_SCRIPT sync" "$NVIDIA_HOOK"'
check "$loader: backup made, rebuilt once" '[[ -f "$F.bak" && $REBUILDS == 1 ]]'
SUM="$(cat "$F")"; nvidia_kernel_enable
check "$loader: second run changes nothing" '[[ "$(cat "$F")" == "$SUM" && $REBUILDS == 1 ]]'
nvidia_kernel_disable
check "$loader: disable restores the file, drops the drop-in, script and hook" '[[ "$(cat "$F")" == "$ORIG" && ! -f "$NVIDIA_INITRAMFS_CONF" && ! -e "$NVIDIA_SCRIPT" && ! -e "$NVIDIA_HOOK" ]]'
done
# A missing patches/ file must be an error, never an empty script or hook.
( SCRIPT_DIR="$T/no-patches"; NVIDIA_SCRIPT="$T/empty/script"; NVIDIA_HOOK="$T/empty/hook"; ! nvidia_hook_install 2>/dev/null && [[ ! -s "$T/empty/script" ]] ) && echo "ok a missing patch file is an error, nothing empty installed" || { echo "FAIL a missing patch file is an error, nothing empty installed"; fail=1; }
# The pacman hook's script decides again at every kernel change.
syncit() { NVIDIA_INITRAMFS_CONF="$NVIDIA_INITRAMFS_CONF" NVIDIA_MODULES_DIR="$NVIDIA_MODULES_DIR" bash "$NVIDIA_SCRIPT" sync; }
nvidia_hook_install; rm -f "$NVIDIA_INITRAMFS_CONF"
syncit; check "hook script: every kernel has the modules -> drop-in written" '[[ -f "$NVIDIA_INITRAMFS_CONF" ]]'
mkdir -p "$T/mods/6.6.0-lts/kernel"; : > "$T/mods/6.6.0-lts/pkgbase"
syncit; check "hook script: a new kernel without modules -> drop-in removed" '[[ ! -f "$NVIDIA_INITRAMFS_CONF" ]]'
: > "$T/mods/6.6.0-lts/kernel/nvidia-drm.ko.zst"
syncit; check "hook script: the modules arrive (DKMS built) -> drop-in back" '[[ -f "$NVIDIA_INITRAMFS_CONF" ]]'
rm -rf "$T/mods/6.6.0-lts"; mkdir -p "$T/mods/5.0.0-leftover"; : > "$T/mods/5.0.0-leftover/modules.dep"
syncit; check "hook script: a leftover folder of a removed kernel is ignored" '[[ -f "$NVIDIA_INITRAMFS_CONF" ]]'
rm -rf "$T/mods/5.0.0-leftover"; nvidia_kernel_disable
# A second kernel without the NVIDIA modules: no early-load drop-in, parameters still set.
mkdir -p "$T/mods/6.6.0-lts/kernel"; : > "$T/mods/6.6.0-lts/pkgbase"; F="$T/sdboot-manage.conf"; printf 'LINUX_OPTIONS="quiet"\n' > "$F"
nvidia_boot_file() { echo "$F"; }; rm -f "$NVIDIA_INITRAMFS_CONF"; REBUILDS=0; nvidia_kernel_enable
check "kernel without nvidia modules: no drop-in, parameters set" '[[ ! -f "$NVIDIA_INITRAMFS_CONF" ]] && grep -q "nvidia-drm.fbdev=1" "$F"'
rm -rf "$T/mods/6.6.0-lts"
# Generated boot entries without the parameters (the initramfs build failed): an error.
printf 'linux /vmlinuz root=/dev/sda1\n' > "$T/grub.cfg"; NVIDIA_GRUB_CFG="$T/grub.cfg"
F="$T/grub"; printf 'GRUB_CMDLINE_LINUX_DEFAULT="quiet"\n' > "$F"; nvidia_boot_file() { echo "$F"; }; rm -f "$NVIDIA_INITRAMFS_CONF"
check "boot entries without the parameters: error" '! nvidia_kernel_enable >/dev/null'
NVIDIA_GRUB_CFG="$T/none/grub.cfg"
# A config file without the setting to edit: an error, not "applied".
F="$T/sdboot-manage.conf"; printf '#LINUX_OPTIONS=""\n' > "$F"; nvidia_boot_file() { echo "$F"; }; REBUILDS=0
check "no setting to edit: error, nothing rebuilt" '! nvidia_kernel_enable >/dev/null && [[ $REBUILDS == 0 ]]'
FAKE_DRIVER=0; NVIDIA_DRM_DIR="$T/drm"; printf 'LINUX_OPTIONS="quiet"\n' > "$T/sdboot"; F="$T/sdboot"; REBUILDS=0; nvidia_kernel_enable
check "no NVIDIA driver: nothing touched" '[[ "$(cat "$F")" == "LINUX_OPTIONS=\"quiet\"" && $REBUILDS == 0 ]]'
# --- single user mode on NVIDIA: its own login step (SDDM, autologin into Plasma) without the conversion
FAKE_DRIVER=1; NVIDIA_DRM_DIR="$T/drm" # an earlier section ended without the driver
SW="$T/switches.log"; : > "$SW"; TARGET_USER=tester; SINGLE_AUTOLOGIN="$T/sddm.conf.d/zzz-steamify-autologin.conf"
switch_to_sddm() { echo sddm >> "$SW"; }; switch_to_plasmalogin() { echo plasmalogin >> "$SW"; }
remove_sddm_base_autologin() { :; } # would look at the real /etc/sddm.conf
GAMING_ON=0
check "single user mode is offered on NVIDIA without the conversion..." 'component_available single'
GAMING_ON=1; check "...and with it" 'component_available single'; GAMING_ON=0
single_login_enable
check "single user: SDDM, autologin of the user into the Plasma session" 'grep -qx "User=tester" "$SINGLE_AUTOLOGIN" && grep -qx "Session=plasma.desktop" "$SINGLE_AUTOLOGIN" && grep -qx "Relogin=true" "$SINGLE_AUTOLOGIN" && grep -qx sddm "$SW"'
c1="$(cat "$SINGLE_AUTOLOGIN")"; single_login_enable
check "single user login: a second run changes nothing" '[[ "$(cat "$SINGLE_AUTOLOGIN")" == "$c1" ]]'
single_login_disable
check "single user off: the file is gone and plasma-login-manager is the login manager again" '[[ ! -e "$SINGLE_AUTOLOGIN" ]] && grep -qx plasmalogin "$SW"'
: > "$SW"; GAMING_ON=1; single_login_enable
check "with the conversion on, single user mode leaves the login to it" '[[ ! -e "$SINGLE_AUTOLOGIN" && ! -s "$SW" ]]'
GAMING_ON=0
# --- the menu rules
declare -A WANTED=([nvidia]=1 [bigpicture]=1 [gaming]=0 [boot]=0 [single]=0 [machine]=0 [poweroff]=0 [launcher]=0 [steamgame]=0)
component_selectable() { return 0; }
toggle_component nvidia
check "unticking Gaming on NVIDIA unticks Big Picture" '[[ "${WANTED[nvidia]}" == 0 && "${WANTED[bigpicture]}" == 0 ]]'
toggle_component bigpicture
check "ticking Big Picture ticks Gaming on NVIDIA" '[[ "${WANTED[nvidia]}" == 1 && "${WANTED[bigpicture]}" == 1 ]]'
WANTED[single]=0; WANTED[gaming]=0; toggle_component single
check "ticking single user mode does not tick the conversion where it isn't offered" '[[ "${WANTED[single]}" == 1 && "${WANTED[gaming]}" == 0 ]]'
check "Big Picture follows Gaming on NVIDIA in the menu" '[[ " ${COMPONENTS[*]} " == *" nvidia bigpicture "* && "${PARENT[bigpicture]}" == nvidia ]]'
exit $fail
+161
View File
@@ -0,0 +1,161 @@
"""Unit tests of the Python helpers in steamify-cachyos/patches (stdlib only).
Run through tests/python-test.sh (REPO = the Steamify checkout), or:
REPO=../steamify-cachyos python3 -m unittest tests/patches_test.py -v
"""
import contextlib
import importlib.util
import io
import os
import struct
import tempfile
import unittest
from unittest import mock
REPO = os.environ.get("REPO") or os.path.join(os.path.dirname(__file__), "..", "..", "steamify-cachyos")
def load(name, filename):
spec = importlib.util.spec_from_file_location(name, os.path.join(REPO, "patches", filename))
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
shortcuts = load("steam_shortcuts", "steam-shortcuts.py")
notifier = load("steamify_notifier", "steamify-notifier.py")
class VdfTests(unittest.TestCase):
def test_roundtrip(self):
root = [("shortcuts", [("0", [("appid", 123), ("AppName", "A"), ("tags", [])])])]
self.assertEqual(shortcuts.parse(shortcuts.dump(root)), root)
def test_empty_data_is_an_empty_list(self):
self.assertEqual(shortcuts.parse(b""), [("shortcuts", [])])
def test_unknown_type_is_an_error(self):
with self.assertRaises(ValueError):
shortcuts.parse(b"\x07key\0")
def test_ints_are_unsigned_32_bit(self):
data = shortcuts.dump([("n", -1)])
self.assertEqual(struct.unpack_from("<I", data, 3)[0], 0xFFFFFFFF)
def test_keys_match_regardless_of_case(self):
entry = [("AppName", "x")]
self.assertEqual(shortcuts.get(entry, "appname"), "x")
shortcuts.put(entry, "APPNAME", "y")
self.assertEqual(entry, [("AppName", "y")])
def test_ours(self):
exe = "/home/u/.local/share/steamify/bin/run-app"
self.assertTrue(shortcuts.ours([("Exe", f'"{exe}"')], exe))
# An older install's start script counts as ours: updated, not doubled.
self.assertTrue(shortcuts.ours([("Exe", '"/home/u/.local/share/cachyos-gamescope-boot/bin/run-app"')], exe))
self.assertFalse(shortcuts.ours([("Exe", '"/usr/bin/other"')], exe))
self.assertFalse(shortcuts.ours([("Exe", '"/home/u/games/run-app"')], exe))
class ShortcutsCommandTests(unittest.TestCase):
def setUp(self):
self.dir = tempfile.TemporaryDirectory()
self.addCleanup(self.dir.cleanup)
self.vdf = os.path.join(self.dir.name, "userdata", "1", "config", "shortcuts.vdf")
self.exe = "/home/u/.local/share/steamify/bin/run-app"
def run_cmd(self, *args):
out = io.StringIO()
with contextlib.redirect_stdout(out), contextlib.redirect_stderr(io.StringIO()):
rc = shortcuts.main(["steam-shortcuts.py", *args])
return rc, out.getvalue().strip()
def entries(self):
return shortcuts.shortcuts(shortcuts.load(self.vdf))
def test_add_creates_the_file_and_an_entry(self):
rc, out = self.run_cmd("add", self.vdf, "Steamify", self.exe, "/icon.png", "steamify.desktop")
self.assertEqual(rc, 0)
self.assertTrue(out.startswith("added "))
(_, entry), = self.entries()
self.assertEqual(shortcuts.get(entry, "AppName"), "Steamify")
self.assertEqual(shortcuts.get(entry, "Exe"), f'"{self.exe}"')
# Steam's id for a shortcut has the top bit set.
self.assertGreaterEqual(shortcuts.get(entry, "appid"), 0x80000000)
self.assertEqual(out, f"added {shortcuts.get(entry, 'appid')}")
def test_add_twice_updates_instead_of_doubling(self):
self.run_cmd("add", self.vdf, "Steamify", self.exe, "/a.png", "a.desktop")
rc, out = self.run_cmd("add", self.vdf, "Steamify 2", self.exe, "/b.png", "b.desktop")
self.assertTrue(out.startswith("updated "))
entries = self.entries()
self.assertEqual(len(entries), 1)
self.assertEqual(shortcuts.get(entries[0][1], "AppName"), "Steamify 2")
self.assertEqual(shortcuts.get(entries[0][1], "icon"), "/b.png")
def test_other_entries_are_kept(self):
self.run_cmd("add", self.vdf, "Other", "/usr/bin/other", "", "")
self.run_cmd("add", self.vdf, "Steamify", self.exe, "", "")
self.assertEqual(len(self.entries()), 2)
self.assertEqual([k for k, _ in self.entries()], ["0", "1"])
def test_find(self):
self.assertEqual(self.run_cmd("find", self.vdf, self.exe)[0], 1)
_, out = self.run_cmd("add", self.vdf, "Steamify", self.exe, "", "")
rc, found = self.run_cmd("find", self.vdf, self.exe)
self.assertEqual((rc, found), (0, out.split()[1]))
def test_remove_by_appid_renumbers(self):
self.run_cmd("add", self.vdf, "Other", "/usr/bin/other", "", "")
_, out = self.run_cmd("add", self.vdf, "Steamify", self.exe, "", "")
self.assertEqual(self.run_cmd("remove", self.vdf, out.split()[1])[0], 0)
entries = self.entries()
self.assertEqual([k for k, _ in entries], ["0"])
self.assertEqual(shortcuts.get(entries[0][1], "AppName"), "Other")
def test_remove_of_nothing_leaves_the_file_alone(self):
self.run_cmd("add", self.vdf, "Steamify", self.exe, "", "")
with open(self.vdf, "rb") as f:
before = f.read()
self.assertEqual(self.run_cmd("remove", self.vdf, "999")[0], 0)
with open(self.vdf, "rb") as f:
self.assertEqual(f.read(), before)
def test_unknown_command(self):
self.assertEqual(self.run_cmd("bogus", self.vdf)[0], 2)
class NotifierTests(unittest.TestCase):
def test_version(self):
self.assertEqual(notifier.version("v2.11.1"), (2, 11, 1))
self.assertEqual(notifier.version("2.9.0"), (2, 9, 0))
self.assertIsNone(notifier.version("nightly"))
def test_versions_compare_as_numbers(self):
self.assertGreater(notifier.version("2.10.0"), notifier.version("2.9.0"))
def pending(self, new, seen="", skipped=""):
values = {"seen": seen, "skipped": skipped}
with mock.patch.object(notifier, "state", lambda key: values[key]):
return notifier.pending(new)
def test_newer_than_seen(self):
self.assertTrue(self.pending("2.12.0", seen="2.11.1"))
def test_same_or_older_is_not_pending(self):
self.assertFalse(self.pending("2.11.1", seen="2.11.1"))
self.assertFalse(self.pending("2.9.0", seen="2.11.1"))
def test_skipped_version_is_not_pending(self):
self.assertFalse(self.pending("2.12.0", seen="2.11.1", skipped="2.12.0"))
def test_nothing_seen_yet(self):
self.assertTrue(self.pending("2.12.0", seen=""))
def test_unparsable_tag_is_not_pending(self):
# A release tag like "nightly" must not crash the daily check.
self.assertFalse(self.pending("nightly", seen="2.11.1"))
if __name__ == "__main__":
unittest.main()
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
# Unit tests of the Python helpers in steamify-cachyos/patches (steam-shortcuts.py,
# steamify-notifier.py). No root, VM or hardware needed: bash tests/python-test.sh
# The Steamify checkout (REPO, default: the steamify-cachyos next to this repo)
REPO="${REPO:-$(cd "$(dirname "$0")/../../steamify-cachyos" && pwd)}"
export REPO
cd "$(dirname "$0")/.." || exit 1
if python3 -B -m unittest tests/patches_test.py 2>&1 | tail -n 25 | tee /dev/stderr | grep -q '^OK'; then
echo "ok Python helpers"
else
echo "FAIL Python helpers"; exit 1
fi