mirror of
https://github.com/theupriser/steamify-cachyos-dev.git
synced 2026-10-14 13:57:29 +02:00
Compare commits
71
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17ae88a4ad | ||
|
|
a9f20d69bf | ||
|
|
9a351ae639 | ||
|
|
0d0279bd20 | ||
|
|
195518f0da | ||
|
|
0447f35e7d | ||
|
|
d614539d9e | ||
|
|
847b024803 | ||
|
|
8df2e23314 | ||
|
|
e6119ca194 | ||
|
|
6aad17f55c | ||
|
|
e8c57827b1 | ||
|
|
4209f9625b | ||
|
|
07a7da1d67 | ||
|
|
a34efb0547 | ||
|
|
20373c9f05 | ||
|
|
27b7445f63 | ||
|
|
a82053d6bc | ||
|
|
37d65fc79d | ||
|
|
5b867f8bd2 | ||
|
|
e583494aa6 | ||
|
|
fd9ecf4b9f | ||
|
|
4cc4046716 | ||
|
|
9d537406bb | ||
|
|
7dd659dc09 | ||
|
|
d5cfadf3bb | ||
|
|
cb247b9cc3 | ||
|
|
6a53c957ba | ||
|
|
63fd11829d | ||
|
|
f03ad74295 | ||
|
|
0c84b8b926 | ||
|
|
ee20bd9825 | ||
|
|
6d264acaf2 | ||
|
|
b91ef9172e | ||
|
|
fe5251f758 | ||
|
|
508eb68ed5 | ||
|
|
c91277abf0 | ||
|
|
8646b3c30a | ||
|
|
fa776b1761 | ||
|
|
0ae7cce488 | ||
|
|
7db82b6494 | ||
|
|
52d5021d88 | ||
|
|
f19bcb9764 | ||
|
|
5954548247 | ||
|
|
bc06bc2534 | ||
|
|
0052f1ba22 | ||
|
|
61442d7ef8 | ||
|
|
6d73d49477 | ||
|
|
fe249e5108 | ||
|
|
4398a2b85f | ||
|
|
081080b5f5 | ||
|
|
b6b35d41f1 | ||
|
|
3ccbb62bd2 | ||
|
|
fec0ce5b8e | ||
|
|
d0038916e9 | ||
|
|
4905bba1f6 | ||
|
|
60a2698d02 | ||
|
|
21547dd7bc | ||
|
|
96cae35c6b | ||
|
|
e1dd75875b | ||
|
|
f8aac02d0a | ||
|
|
8d5cdaea99 | ||
|
|
f8e2473afa | ||
|
|
088ee83350 | ||
|
|
d626aafd84 | ||
|
|
6da46fc10f | ||
|
|
0464ede893 | ||
|
|
4aeb3de43f | ||
|
|
04204f1b9d | ||
|
|
e04db1ec5f | ||
|
|
a8d7554dce |
No files matched your search
@@ -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*`.
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,300 @@
|
||||
---
|
||||
name: steam-machine-iso
|
||||
description: Use when working on the Steam Machine CachyOS ISO (repo steamify-cachyos-live-iso) or Steamify's install-time mode (steamify.sh --defaults, --first-login) - building the ISO in podman (in the test VM, or on the Steam Machine), the Calamares Steamify step, simulating the installer in the test VM, and testing the first desktop login.
|
||||
---
|
||||
|
||||
# Steam Machine ISO and Steamify's install-time mode
|
||||
|
||||
Releasing the ISO (GitHub tag, built and attached on the Gitea mirror, naming, runner and server settings) is
|
||||
the `steamify-iso-release` skill; this one is for building and testing by hand.
|
||||
|
||||
A CachyOS live ISO for the Steam Machine that installs Steamify's default
|
||||
setup during the install. Two repos:
|
||||
|
||||
- `steamify-cachyos` (Steamify): `steamify.sh --defaults` applies what the
|
||||
menu would preselect, no prompts, passwordless sudo required. Without a
|
||||
session (installer) `user_systemctl` only changes unit files, the theme
|
||||
replaces an untouched `/etc/skel` Plasma layout so Plasma builds Vapor's at
|
||||
first login, and `lib/first-login.sh` leaves a one-time autostart
|
||||
(`--first-login`: single user's launcher on the new layout, then the app,
|
||||
next to CachyOS Hello). See AGENTS.md there.
|
||||
- `steamify-cachyos-live-iso` (fork of CachyOS-Live-ISO), branch work
|
||||
never on master:
|
||||
- `archiso/airootfs/usr/local/bin/calamares-online.sh` (the installer
|
||||
launcher) copies CachyOS's `settings_online.conf` over
|
||||
`/etc/calamares/settings.conf` on every start, so a shipped settings.conf
|
||||
is lost: it `sed`s `shellprocess@steamify` (+ its instance) in before
|
||||
`cleanup_calamares` right after that copy.
|
||||
- `modules/shellprocess_steamify.conf` runs
|
||||
`/usr/local/bin/steamify-install ${ROOT} ${USER}` outside the chroot; that
|
||||
gives the user a temporary NOPASSWD rule, runs the bundle with
|
||||
`arch-chroot ... runuser -u <user> -- env -i ... --defaults`, logs to
|
||||
`/var/log/steamify-install.log` in the target, never fails the install.
|
||||
- `steamify-prepare.sh [steamify checkout]` puts the bundle on the ISO
|
||||
(`archiso/airootfs/usr/local/share/steamify/steamify.sh`, git-ignored):
|
||||
from a checkout, else the newest release; refuses one without `--defaults`.
|
||||
|
||||
## Everything visible
|
||||
|
||||
The user watches the Steam Machine over Moonlight: run builds and tests in a
|
||||
Konsole on its desktop (or open one that follows the log), never hidden.
|
||||
Progress in the conversation: the `progress-report` skill (a table, only on a change).
|
||||
|
||||
Open that Konsole only when none follows the log yet: `tail -F` picks up
|
||||
each new build's log by itself, so one window serves every build (check with
|
||||
`pgrep -af "tail -n 50 -F"` on the Steam Machine; never `pkill -f` that
|
||||
pattern from a remote command, it kills its own shell).
|
||||
|
||||
```bash
|
||||
ssh steammachine bash -c "'pgrep -f \"tail -n 50 -F\" >/dev/null || { export XDG_RUNTIME_DIR=/run/user/1000 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus; systemd-run --user -q konsole --hold -e bash -c \"tail -n 50 -F ~/projects/iso-build.log\"; }'"
|
||||
```
|
||||
|
||||
## Steam Machine gotchas
|
||||
|
||||
- Login shells there and in the VM are **fish**: wrap loops in
|
||||
`bash -c '...'` or pipe a script to `bash -s`.
|
||||
- Never `pkill -f <pattern>` from a remote command containing that pattern:
|
||||
it kills its own shell (exit 255).
|
||||
- The VM's host key lives in `~/projects/steamify-cachyos-dev/known_hosts`.
|
||||
Scripts calling plain `ssh` (vmreset.sh) need a wrapper first in `PATH`:
|
||||
`/tmp/steamify-sshwrap/ssh` =
|
||||
`exec /usr/bin/ssh -o UserKnownHostsFile=$HOME/projects/steamify-cachyos-dev/known_hosts -o StrictHostKeyChecking=accept-new "$@"`
|
||||
(recreate after a reboot).
|
||||
- `vmreset.sh` shuts down *any* running QEMU (it checks `pgrep ^qemu-system`),
|
||||
including the ISO VM with someone's live session: ask before resetting the
|
||||
test VM while the ISO VM runs.
|
||||
- Run vmreset.sh as a systemd user unit with `-p KillMode=process`, a unique
|
||||
unit name, `--setenv=REPO=$HOME/projects/steamify-cachyos`, the wrapper
|
||||
`PATH` and `SSH_AUTH_SOCK` (connect with `ssh -A`, keep the session open with
|
||||
`--wait`). Without KillMode=process QEMU dies with the unit; without REPO
|
||||
QEMU fails on the old repo path and the script waits 5 minutes for SSH.
|
||||
|
||||
## Adding a Calamares page/module for Steamify: check it's real first
|
||||
|
||||
Before writing any QML/config for a Calamares page, or building an ISO to
|
||||
test one: confirm the module type is actually installed on this Calamares
|
||||
build. A `.conf` file existing under
|
||||
`archiso/airootfs/etc/calamares/modules/` does **not** mean the module
|
||||
exists — CachyOS's branding ships some vestigial config files with no
|
||||
matching `.so`. Check in a live session (boot the ISO,
|
||||
`scripts/vmisoboot.sh`, no install needed):
|
||||
|
||||
```bash
|
||||
ls /usr/lib/calamares/modules # real module types, e.g. packagechooser (not packagechooserq)
|
||||
pacman -Ql cachyos-calamares-next | grep viewmodule
|
||||
```
|
||||
|
||||
Found on this build (2026-09-28): `packagechooserq` (arbitrary custom QML,
|
||||
`qmlFilename`) does **not exist** — only `packagechooser` does. Its
|
||||
`mode: optionalmultiple` supports multi-select, but only with Shift/Ctrl-click (plain clicks
|
||||
replace the selection; unusable for a real user) despite the misleading static label text
|
||||
("Choose a product from the list. The selected product will be
|
||||
installed." shows in both single- and multi-select modes — don't trust
|
||||
it). Confirm real multi-select from the actual global-storage write in the
|
||||
foregrounded debug log (`pkexec-wrapper calamares -D6`), not the label
|
||||
text: `Config::updateGlobalStorage(const QStringList&)` logs
|
||||
`"<instance>" selected "id1,id2,id3"` (comma-separated) when it works.
|
||||
|
||||
Test a page live, without any ISO rebuild: in the booted live session,
|
||||
edit `/etc/calamares/modules/<module>_<instance>.conf` and the matching
|
||||
module reference in `/etc/calamares/settings.conf`, then relaunch
|
||||
(`pkexec-wrapper calamares -D6`). If Calamares is already running (e.g. an
|
||||
earlier failed launch left its window open), a second launch just prints
|
||||
"Calamares is already running." — close the first window
|
||||
(Afbreken/Cancel, confirm) before relaunching. Only spend the 13+ minute
|
||||
ISO build once the page's module and rendering are confirmed this way.
|
||||
|
||||
## Building the ISO in the test VM (preferred)
|
||||
|
||||
## Building the ISO in the test VM (preferred)
|
||||
|
||||
The build needs no real hardware: run it in the test VM (CachyOS; create it
|
||||
with the vm-install skill), not on the Steam Machine. Not tried yet in the
|
||||
VM: note what differs here. In the VM (`vm_ssh`, visibly in a Konsole there):
|
||||
|
||||
```bash
|
||||
sudo pacman -S --needed --noconfirm podman git
|
||||
git clone https://github.com/theupriser/steamify-cachyos-live-iso ~/projects/steamify-cachyos-live-iso
|
||||
sudo mount -t 9p -o trans=virtio,version=9p2000.L repo /mnt # the Steamify checkout (REPO)
|
||||
cd ~/projects/steamify-cachyos-live-iso && git checkout feat/steamify && ./steamify-prepare.sh /mnt
|
||||
```
|
||||
|
||||
then the same `podman run` as below (paths in the VM; `~/projects/iso-build.log`).
|
||||
The ISO (~3.2 GB) fits the 60G disk; copy it out with
|
||||
`vm_scp "$VM_USER@$VM_HOST:~/projects/steamify-cachyos-live-iso/out/desktop/*.iso" .`
|
||||
(`scripts/common.sh`), then install it with `scripts/vminstall.sh --iso`.
|
||||
Give the VM more room with a bigger disk if `out/`, `build/` and the package
|
||||
cache grow over several builds (`sudo rm -rf build out` between builds).
|
||||
|
||||
## Building the ISO (podman on the Steam Machine)
|
||||
|
||||
Only podman is installed on the Steam Machine itself; the build tools live in
|
||||
the container. Rootful (loop devices, mounts), so `sudo podman ps` shows it,
|
||||
not a rootless podman GUI. Clone: `~/projects/steamify-cachyos-live-iso`.
|
||||
|
||||
```bash
|
||||
cd ~/projects/steamify-cachyos-live-iso
|
||||
git checkout feat/steamify && ./steamify-prepare.sh ~/projects/steamify-cachyos
|
||||
sudo rm -rf build out
|
||||
systemd-run --user --collect -q -u isobuild-$(date +%s) --working-directory=$PWD bash -c \
|
||||
"sudo podman run --rm -t --pids-limit=-1 --ulimit nofile=65536:65536 --privileged --network=host \
|
||||
-v $HOME/projects/iso-cache:/var/cache/pacman/pkg \
|
||||
-v $PWD:/iso -w /iso docker.io/cachyos/cachyos:latest bash -c \
|
||||
'pacman-key --init && pacman-key --populate && pacman -Syu --noconfirm --needed archiso mkinitcpio-archiso git squashfs-tools grub sudo && ./build-live-modules.sh && ./build-calamares-modules.sh && { ./buildiso.sh -p desktop -w || ./buildiso.sh -p desktop -c -w; }' \
|
||||
> $HOME/projects/iso-build.log 2>&1"
|
||||
```
|
||||
|
||||
`-v ~/projects/iso-cache:/var/cache/pacman/pkg` keeps the downloaded packages
|
||||
(~3 GB) between builds (`mkdir -p ~/projects/iso-cache` once): the container's
|
||||
own tools and mkarchiso's `pacstrap -c` both use that cache, so only updated
|
||||
packages are downloaded. Clear it with `sudo rm -rf ~/projects/iso-cache/*`
|
||||
if it grows too large.
|
||||
|
||||
`build-calamares-modules.sh` builds Calamares' `packagechooserq` (the Steamify
|
||||
page's module; CachyOS's package leaves it out) from CachyOS's Calamares
|
||||
source against the installed `cachyos-calamares-next`, into airootfs, with
|
||||
the version it was built for in `built-for` (the prebuilt `.so` is no longer
|
||||
committed). `build-live-modules.sh` builds the power-off fix for the ISO's kernels
|
||||
(live session; needs the source steamify-prepare.sh puts on the ISO).
|
||||
Output: `out/desktop/steamify-cachyos-*.iso` (~3.2 GB, ~10 min). The build ends with
|
||||
`chown: missing operand` + `ERROR: An unknown error`: harmless, buildiso.sh
|
||||
chowns to `$SUDO_USER`, unset as root; the ISO is already written. Each flag fixes a failure seen before:
|
||||
|
||||
- `--network=host`: the default network has no DNS here (every mirror
|
||||
"Resolving timed out").
|
||||
- `--pids-limit=-1`: otherwise the last ~9 packages alphabetically fail with
|
||||
`GPGME error: Inappropriate ioctl for device` / "missing required signature".
|
||||
- `pacman-key --init/--populate`: the image's keyring isn't initialised.
|
||||
- The retry with `-c` keeps the work dir and cache for a flaky pass.
|
||||
|
||||
Never start a build while one runs (`sudo podman ps`): both use `build/`
|
||||
and `out/`, and `rm -rf build out` breaks the running one. Don't stop a running build to try something else without checking its log
|
||||
first (`grep -ac GPGME ~/projects/iso-build.log`). The repo's CI recipe
|
||||
(`archlinux:base-devel`, `.github/workflows/build.yml`) works too but pulls
|
||||
from one slow mirror.
|
||||
|
||||
## Testing Steamify's install-time mode in the VM
|
||||
|
||||
`scripts/vminstallsim.sh [--fresh]` (VM up via vmreset.sh `--fremont`, branch
|
||||
rsynced to `~/projects/steamify-cachyos`): creates `isotest` (never logged
|
||||
in), runs `/mnt/steamify.sh --defaults` for it without any session, visibly.
|
||||
Every component should print OK and `exit: 0`. Use `--fresh` after changes:
|
||||
an earlier run's single user mode edits the skel layout, and the theme then
|
||||
(rightly) refuses.
|
||||
|
||||
First desktop login as that user:
|
||||
|
||||
1. The first-login step (`~/.local/share/steamify/bin/first-login`, removes
|
||||
itself) starts the app through `run-app`, which always downloads the
|
||||
newest *release* (`releases/latest/download/steamify-app.sh`): before a
|
||||
release that shows the old version. For the branch's app, start it by
|
||||
hand afterwards as that user: copy `/mnt` to `/opt/steamify-test`
|
||||
(`chmod -R a+rX`), then `sudo -u isotest XDG_RUNTIME_DIR=/run/user/1001
|
||||
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1001/bus systemd-run --user
|
||||
/opt/steamify-test/ui/steamify-ui`.
|
||||
2. Gamescope doesn't run in the VM: `Session=plasma.desktop` in
|
||||
`/etc/sddm.conf.d/zz-steamos-autologin.conf`, remove
|
||||
`/etc/plasmalogin.conf.d/00-test-autologin.conf`, reboot.
|
||||
3. Expect: Steam Deck wallpaper (Vapor layout), `primaryActions=3` in
|
||||
isotest's `plasma-org.kde.plasma.desktop-appletsrc`, the autostart entry
|
||||
gone, `steamify-ui` running next to `cachyos-hello`.
|
||||
4. Screenshot: QMP `screendump` gives "no surface" with virgl; use
|
||||
`sudo -u isotest env XDG_RUNTIME_DIR=/run/user/1001 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1001/bus WAYLAND_DISPLAY=wayland-0 spectacle -b -n -f -o /tmp/shot.png`.
|
||||
|
||||
## Testing the ISO itself
|
||||
|
||||
Unattended: `VM_DIR=~/projects/iso-vm scripts/vminstall.sh --iso <built iso> --fremont`
|
||||
(vm-install skill) installs it with the ISO's headless `cachyos-installer`;
|
||||
Calamares and its Steamify step don't run then, so run `steamify-install`
|
||||
from the live script for that part. Through Calamares by hand, as below.
|
||||
|
||||
Install the built ISO in a **new** VM (`~/projects/iso-vm`: copy of run.sh,
|
||||
`share` symlink, `cachyos.iso` symlink to the build, own disk/vars; power the
|
||||
test VM off first, both use port 2222; `run.sh install --fremont` as a user
|
||||
unit), through Calamares, then check
|
||||
`/var/log/steamify-install.log` in the installed system. In the chroot
|
||||
`uname -r` is the live kernel: module loads and checks against the running
|
||||
kernel may fail even when DKMS built for the installed kernels. Real
|
||||
hardware (Steam Machine, USB stick) is the final test: gamescope, LEDs, CEC,
|
||||
power-off.
|
||||
|
||||
## Live-session quirks
|
||||
|
||||
- Copy/paste doesn't reach QEMU over Moonlight: type commands with
|
||||
`python3 scripts/qmptype.py <vm dir>/qmp.sock "<command>"` (US layout, adds
|
||||
Enter; the VM window's terminal must have focus). QMP `screendump` gives "no
|
||||
surface" with virgl: screenshot the Steam Machine's desktop instead
|
||||
(`spectacle -b -n -f -o /tmp/host.png` in its session).
|
||||
- CachyOS Hello refuses to start the installer on a self-built ISO ("testing
|
||||
ISO"): run `calamares-online.sh` in Konsole.
|
||||
- `cachyos-calamares-next 3.4.2-13` needs `libboost_*.so.1.91.0` while the
|
||||
repos ship Boost 1.92: steamify-prepare.sh puts 1.91's libraries on the ISO
|
||||
(from archive.archlinux.org) until CachyOS rebuilds it.
|
||||
- Driving the ISO VM's Calamares with `scripts/qmpkey.py`/`qmpclick.py`/
|
||||
`qmptype.py` (`scripts/vmisoboot.sh` for a VM without virgl, so
|
||||
`qmpshot.py` screendumps work): `qmptype.py`'s character map has no `>`
|
||||
or `&` — a typed command using either raises "no key for" and, worse,
|
||||
can leave an **unterminated quote** in the shell's input buffer (a
|
||||
stray `"` from a half-typed command), after which every later command
|
||||
you type gets silently absorbed into that one open string instead of
|
||||
running. Symptom: the prompt shows a bare `>` continuation instead of
|
||||
`[user@host ~]$`. Fix: send Ctrl+C first (`send-key` with `ctrl`+`c`),
|
||||
then retype. Prefer `| tee` over `> file`, and multiple `head`/`tail`
|
||||
calls over one long redirected command. Calamares page buttons
|
||||
(Volgende/Terug) move vertically as page content grows/shrinks: re-shot
|
||||
and re-locate the button after every page change, don't reuse a fixed
|
||||
y-coordinate. A second `pkexec-wrapper calamares -D6` while an earlier
|
||||
failed instance's window is still open just prints "Calamares is already
|
||||
running.": close that window first (its Cancel/Afbreken button, then
|
||||
confirm the "really cancel?" dialog).
|
||||
- Run `pkexec-wrapper calamares -D6` in its **own Konsole tab**: it stays
|
||||
in the foreground and blocks that tab's shell. Typing into the same
|
||||
tab afterwards just echoes the text inertly into Calamares' stdin
|
||||
(no error, nothing runs). Open "New Tab" in Konsole for commands.
|
||||
|
||||
## Keeping this cheap: don't poll screenshots
|
||||
|
||||
Repeated `qmpshot.py` + image analysis to detect "is it booted yet" burns a
|
||||
lot of tokens per image. Prefer cheap, text-only signals instead:
|
||||
|
||||
- **Boot readiness**: pass `VM_SERIAL=<dir>/serial` to `run.sh`/`vmisoboot.sh`
|
||||
(it logs `console=ttyS0` to a plain text file via a QMU chardev socket).
|
||||
Wait with a background `tail -F <dir>/serial | grep -E "Reached target
|
||||
Graphical Interface|sddm|plasmashell|login:|Failed|panic"` — one line of
|
||||
text per event, no image tokens at all.
|
||||
- **Driving the wizard/Calamares**: script a fixed sequence of
|
||||
`qmpkey.py`/`qmptype.py`/`qmpclick.py` calls from a known page layout
|
||||
instead of screenshotting after every step to "see where we are". Only
|
||||
take a screenshot at real decision points (to show the user, or to verify
|
||||
a page layout changed) — not as a polling mechanism.
|
||||
- **When the user is driving by hand** (as in a live install), just ask them
|
||||
what they see / what step they're on instead of screenshotting the VM
|
||||
yourself — they're already looking at it.
|
||||
- If you must poll for "is the screen non-black yet", a single low-res
|
||||
screenshot at the end of a serial-log-driven wait is enough; don't loop
|
||||
screenshots every few seconds.
|
||||
- Calamares' Welcome page applies its keyboard layout to the live session
|
||||
immediately (a live typing preview). If it's not US (e.g. Dutch after
|
||||
picking Nederlands), `qmptype.py`'s US-layout key map silently sends
|
||||
the wrong characters afterwards: `/` becomes `-`, `(`/`)` get garbled,
|
||||
with no "no key for" error. `setxkbmap us` does nothing on Wayland.
|
||||
Probe with a symbol-free command (`echo test123`) after any
|
||||
language/keyboard step, and fix the layout from the live session's own
|
||||
settings before typing paths or shell syntax again.
|
||||
|
||||
## Reading an installed VM's disk (no screenshots)
|
||||
|
||||
Also while the VM runs, read-only, without touching it:
|
||||
|
||||
```bash
|
||||
sudo modprobe nbd max_part=8
|
||||
sudo qemu-nbd -r -c /dev/nbd0 --image-opts driver=qcow2,force-share=on,file.driver=file,file.filename=$PWD/disk.qcow2
|
||||
sudo mount -o ro,rescue=nologreplay,subvol=@log /dev/nbd0p2 /mnt/x # logs; subvol=@ for the rest
|
||||
sudo cat /mnt/x/steamify-install.log
|
||||
sudo umount /mnt/x; sudo qemu-nbd -d /dev/nbd0
|
||||
```
|
||||
|
||||
(`qemu-nbd -U` doesn't exist here; `force-share=on` in `--image-opts` does
|
||||
the same.) For a running live session, start the VM with `VM_SERIAL=<file>`
|
||||
and end typed commands with `| sudo tee /dev/ttyS0`: the output lands in
|
||||
that file as text.
|
||||
@@ -0,0 +1,279 @@
|
||||
---
|
||||
name: steam-machine-testing
|
||||
description: Use when testing or developing Steamify CachyOS (repo steamify-cachyos, steamify.sh) directly on the real Steam Machine (Valve Fremont running CachyOS) - either in a Claude Code session on the machine itself or over SSH - running the wizard with scripted menu input, checking each component's state, gaming mode, HDMI-CEC, LEDs, kernel pin, BIOS and reboot checks.
|
||||
---
|
||||
|
||||
# Testing Steamify CachyOS on the real Steam Machine
|
||||
|
||||
This is the real-hardware twin of the `cachyos-vm-testing` skill. The big
|
||||
difference: **there are no snapshots**. Every change is on the actual
|
||||
machine, so undo what you turn on, never flash the BIOS without the user
|
||||
saying so, and ask before anything that could leave it unbootable (kernel
|
||||
pin, removing kernel headers, DKMS).
|
||||
|
||||
What the Steam Machine gives you that the VM can't: gaming mode that
|
||||
actually renders (Steam's settings, HDMI-CEC section, refresh rates), real
|
||||
LEDs, a real TV over HDMI-CEC, and the real BIOS version check.
|
||||
|
||||
## Keep everything in ~/projects
|
||||
|
||||
The machine is the user's own. Everything you put on it lives in
|
||||
`~/projects` and nowhere else in the home folder: the repo copy
|
||||
(`~/projects/steamify-cachyos`), the dev repo with the test VM
|
||||
(`~/projects/steamify-cachyos-dev`), module builds, scratch checkouts
|
||||
(`~/projects/<name>`). Temporary files go in `/tmp` and are removed again.
|
||||
Never create folders like `~/steamify-test` or `~/s4mod`. What Steamify
|
||||
itself installs (units, `~/.local/...`) is fine: that's the product. When
|
||||
you're done, remove what you left in `~/projects` that isn't a repo.
|
||||
|
||||
## Test VM on the Steam Machine
|
||||
|
||||
The Mac can't run the VM (no KVM); the Steam Machine can (31 GB RAM, 12
|
||||
threads, KVM). `qemu-desktop` and `edk2-ovmf` are installed; `run.sh` finds
|
||||
Arch's OVMF in `/usr/share/edk2/x64`. Run it from
|
||||
`~/projects/steamify-cachyos-dev` with
|
||||
`REPO=~/projects/steamify-cachyos` (the synced branch) and follow the
|
||||
`cachyos-vm-testing` skill, with `ssh steammachine` in front of its
|
||||
commands where they run on the host.
|
||||
|
||||
## Two ways to work
|
||||
|
||||
**A. Claude Code on the Steam Machine itself** (preferred for development):
|
||||
run commands directly, no SSH. Clone the repo there, e.g.
|
||||
`~/projects/steamify-cachyos`, and run the wizard from it. Where this skill
|
||||
says `sm bash -s << 'EOF'`, just run the body locally.
|
||||
|
||||
**B. From another machine over SSH.** Never guess the host: find the
|
||||
candidates, then ask the user (next section). Once chosen, set per shell:
|
||||
|
||||
```bash
|
||||
export SM_HOST=<chosen host> SM_USER=<chosen user>
|
||||
sm() { ssh -o BatchMode=yes "$SM_USER@$SM_HOST" "$@"; }
|
||||
```
|
||||
|
||||
### Finding the Steam Machine
|
||||
|
||||
Skip if the user already named a host, or `~/.ssh/config` has a
|
||||
`Host steammachine` entry (use that). Otherwise scan the LAN for SSH hosts,
|
||||
all read-only and quick:
|
||||
|
||||
```bash
|
||||
# mDNS: hosts announcing SSH (macOS: dns-sd; Linux: avahi-browse)
|
||||
if command -v dns-sd >/dev/null; then dns-sd -B _ssh._tcp local > /tmp/sm-mdns 2>&1 & p=$!; sleep 3; kill $p; awk 'NR>4{print $NF}' /tmp/sm-mdns | sort -u
|
||||
elif command -v avahi-browse >/dev/null; then avahi-browse -rtp _ssh._tcp 2>/dev/null | awk -F';' '/^=/{print $7" "$8}' | sort -u; fi
|
||||
# hosts with port 22 open on the local /24 (nmap if present, else nc; macOS has no `timeout`)
|
||||
net=$( (ipconfig getifaddr en0 || ip -4 route get 1 | awk '{print $7;exit}') 2>/dev/null | cut -d. -f1-3)
|
||||
if command -v nmap >/dev/null; then nmap -p22 --open -oG - "$net.0/24" | awk '/22\/open/{print $2" "$3}'
|
||||
else for i in $(seq 1 254); do (nc -z -w1 -G1 "$net.$i" 22 2>/dev/null || nc -z -w1 "$net.$i" 22 2>/dev/null) && echo "$net.$i" & done; wait; fi
|
||||
```
|
||||
|
||||
A CachyOS Steam Machine usually shows up by its hostname (e.g. `*.local`
|
||||
with "steam", "fremont" or "cachyos" in it). Put the best matches first
|
||||
and ask with AskUserQuestion: up to 3 candidates as options (label = host
|
||||
name or IP, description = what was found), "Other" lets the user type one.
|
||||
Ask the user name the same way (default: the local `$USER`).
|
||||
|
||||
Then check key login: `ssh -o BatchMode=yes -o ConnectTimeout=5 "$SM_USER@$SM_HOST" true`.
|
||||
If it fails, tell the user to run `! ssh-copy-id $SM_USER@$SM_HOST` (it needs
|
||||
their password; sshd must be on: `sudo systemctl enable --now sshd` on the
|
||||
machine). Offer to add a `Host steammachine` entry to `~/.ssh/config` so the
|
||||
next session skips the scan.
|
||||
|
||||
The dev repo's `scripts/*.sh` (vmstate, vmrun, cmp, vmshot, vmcec) all go
|
||||
through `vm_ssh`, so they also work against the Steam Machine with
|
||||
`VM_HOST=$SM_HOST VM_PORT=22 VM_USER=$SM_USER scripts/vmstate.sh`.
|
||||
Don't use `vmreset.sh`, `vmwake.sh` or `run.sh`: those are QEMU-only.
|
||||
`vmcec.sh` needs the `vivid` fake TV; on real hardware test CEC with the
|
||||
real TV (below) instead.
|
||||
|
||||
The user's shell may be fish: pass remote commands as bash heredocs, never
|
||||
as inline strings containing `$`.
|
||||
|
||||
Keep the repo in sync with the working copy on the machine (A: `git pull`;
|
||||
B: `rsync -a --exclude .git ./ "$SM_USER@$SM_HOST:~/projects/steamify-cachyos/"`).
|
||||
Below, `$REPO` is that checkout on the Steam Machine.
|
||||
|
||||
## Visible terminal (let the user watch)
|
||||
|
||||
When the user is watching the Steam Machine's screen (in person or through
|
||||
Moonlight/Sunshine), run commands in a Konsole window on its desktop
|
||||
instead of hidden over SSH: they see every command and its output. A tmux
|
||||
session makes that window scriptable.
|
||||
|
||||
Set it up (Plasma desktop running; `sudo pacman -S --needed tmux` once):
|
||||
|
||||
```bash
|
||||
sm bash -s << 'EOF'
|
||||
export XDG_RUNTIME_DIR=/run/user/$(id -u) WAYLAND_DISPLAY=wayland-0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus
|
||||
tmux kill-session -t claude 2>/dev/null
|
||||
# Plain bash, not the user's login shell: fish rejects bash syntax ($?) and
|
||||
# swallows Enter keys sent while it's still drawing its prompt.
|
||||
tmux new-session -d -s claude -x 200 -y 50 'bash --norc --noprofile -i'; sleep 1
|
||||
tmux send-keys -t claude "PS1='[claude] \\w\\$ '; clear" Enter
|
||||
systemd-run --user -q --setenv=XDG_CURRENT_DESKTOP=KDE konsole --separate -e tmux attach -t claude
|
||||
EOF
|
||||
```
|
||||
|
||||
Run a command and wait for it: end it with a marker, then poll the pane.
|
||||
|
||||
```bash
|
||||
sm bash -s << 'EOF'
|
||||
tmux send-keys -t claude 'clear; sudo pacman -S --needed --noconfirm foo; echo "== EXIT $?"' Enter
|
||||
for i in $(seq 200); do tmux capture-pane -p -t claude -S -400 | grep -q '^== EXIT' && break; sleep 3; done
|
||||
tmux capture-pane -p -t claude -S -400 | grep -v '^\s*$' | tail -30
|
||||
EOF
|
||||
```
|
||||
|
||||
- `clear` first, or an old marker still in the pane ends the wait at
|
||||
once; with several runs, count the markers (`grep -c`).
|
||||
- Longer scripts: write them to `/tmp/<name>.sh` in the heredoc, then
|
||||
`tmux send-keys -t claude 'clear; bash /tmp/<name>.sh' Enter`.
|
||||
- For runs of 10+ minutes, poll from a `run_in_background` Bash call and
|
||||
report when it finishes.
|
||||
- A reboot ends the session (and the window): after every reboot, set it
|
||||
up again before sending anything, or `send-keys` fails
|
||||
(`error connecting to /tmp/tmux-1000/default`) while a wait loop spins.
|
||||
- Watching remotely: Sunshine on the machine (`cachyos` repo, user unit
|
||||
`app-dev.lizardbyte.app.Sunshine.service`, `sunshine --creds` for the web
|
||||
UI, ufw: TCP 47984-47990 and 48010, UDP 47998-48010 from the LAN only) and
|
||||
Moonlight on the user's computer. Pairing: the user reads Moonlight's PIN,
|
||||
then `curl -k -u admin:<pw> https://<host>:47990/api/pin` (GET lists the
|
||||
pending `pairing_id`) and POST `{"pin","name","pairing_id"}` to it. The
|
||||
"Desktop" app streams any session; avoid "Low Res Desktop" (xrandr).
|
||||
|
||||
## Menu
|
||||
|
||||
1. SteamOS conversion (boot into gaming mode via autologin, Return to Gaming Mode shortcut, Steam desktop autostart)
|
||||
2. └ Boot into: gamescope / desktop (sub-option of 1)
|
||||
3. SteamOS theme (`cachyos-vapor` + Vapor global theme; needs a running Plasma session)
|
||||
4. Steam Deck/Machine icons (`STEAM_GAMEPADUI_ARGS -steamos3`)
|
||||
5. Single user mode (SDDM, no lock screen/user switching/log out, Valve's empty KDE wallet)
|
||||
6. Steamify shortcut
|
||||
7. HDMI-CEC (pre-ticked on a first run on Fremont)
|
||||
8. Steam Machine support (leds-valve-dkms-git, `ensure-kernel-headers.service`, udev rule, steamos-manager, powerdevilrc)
|
||||
9. └ Power-off fix (DKMS `steamify-fremont-poweroff`; ticked with 8, opt-out)
|
||||
10. └ Update BIOS (opt-in, never pre-ticked; tickable only when Valve has a newer BIOS)
|
||||
|
||||
Since 2.2.0 "Pin the kernel" and "HDMI refresh boost" are only shown while
|
||||
something of them is left, unticked, so a normal run removes them. The menu
|
||||
marks components a normal run will update with "(update)" (feature versions
|
||||
in `~/.local/state/cachyos-gamescope-boot/features.state`; set an entry to an
|
||||
older version, e.g. `machine=2.1.0`, to test the update flow).
|
||||
|
||||
Numbers shift when a parent is unticked (its sub-option is hidden), so read
|
||||
the ticks with `'q\n'` first. Undo journals: `~/.local/state/cachyos-gamescope-boot/`.
|
||||
|
||||
## Running the wizard
|
||||
|
||||
Visible, in the desktop session (best when the user is watching the screen):
|
||||
|
||||
```bash
|
||||
sm bash -s << 'EOF'
|
||||
export XDG_RUNTIME_DIR=/run/user/$(id -u) WAYLAND_DISPLAY=wayland-0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus
|
||||
printf 'q\n' | "$HOME/projects/steamify-cachyos/steamify.sh" 2>&1 | tee /tmp/wizard.log
|
||||
EOF
|
||||
```
|
||||
|
||||
Newest release instead of the checkout:
|
||||
`curl -fsSL https://github.com/<owner>/steamify-cachyos/releases/latest/download/steamify.sh -o /tmp/steamify.sh`
|
||||
then `printf '<input>' | WIZARD_KEEP_STDIN=1 bash /tmp/steamify.sh`.
|
||||
|
||||
Menu input: a number toggles, an empty line continues, `a` re-applies what is
|
||||
on, `q` quits; `y` answers "Go ahead?". After a run: Enter (menu) or
|
||||
`[m]`/`[r]` when a restart is needed. `q` with a restart pending asks
|
||||
"Restart now? [Y/n]" (default **yes** - on real hardware that really
|
||||
reboots). End scripted input right after the last `q`, or with `n`.
|
||||
|
||||
| Input | Meaning |
|
||||
|---|---|
|
||||
| `'q\n'` | just show the menu (always do this first) |
|
||||
| `'\ny\nm\nq\nn\n'` | first run, accept the pre-ticked items, don't reboot |
|
||||
| `'3\n\ny\n'` | toggle the theme (while 1 is ticked) |
|
||||
| `'7\n\ny\nm\nq\nn\n'` | toggle HDMI-CEC (while 1 is ticked) |
|
||||
| `'a\ny\nm\nq\nn\n'` | re-apply what is on |
|
||||
|
||||
When everything is off the menu treats it as a first run and pre-ticks.
|
||||
`a` doesn't retry a component that failed.
|
||||
|
||||
Long runs (packages, yay, DKMS) take 10+ minutes: use `run_in_background`
|
||||
and wait with an until-loop.
|
||||
|
||||
## Checking state
|
||||
|
||||
`scripts/vmstate.sh` (mode B with the env above), or in mode A run the body
|
||||
of its heredoc locally: display manager and autologin, session sync bridge,
|
||||
shortcut, Steam autostart, theme keys, lock restrictions, panel, kickoff,
|
||||
LED driver/DKMS/`/sys/class/leds/valve-leds*` owner, steamos-manager,
|
||||
journals and duplicate journal keys. `scripts/cmp.sh save` / `cmp.sh` diff
|
||||
the KDE configs.
|
||||
|
||||
## Real-hardware checks (what the VM can't do)
|
||||
|
||||
- **Gaming mode**: reboot with Boot into gamescope; Steam starts in Big
|
||||
Picture/gamepad UI, Steam Deck icons with item 4, "Switch to Desktop"
|
||||
lands in Plasma and "Return to Gaming Mode" goes back.
|
||||
- **HDMI-CEC with the real TV**: `cec-ctl -d /dev/cec0 --playback -S`
|
||||
(topology), TV remote keys reach Steam, Steam's settings > Display >
|
||||
HDMI-CEC toggles (remote, WakeTv, SuspendTv, SuspendDevice) change
|
||||
`com.steampowered.SteamOSManager1.HdmiCec2` properties, sleep turns the TV
|
||||
off, wake turns it on, TV standby suspends the machine when SuspendDevice is on.
|
||||
`steamosctl get/set-hdmi-cec-*` don't work (steamos-manager 26.4.1).
|
||||
- **LEDs**: `leds_valve` loaded, `/sys/class/leds/valve-leds*` owned by the
|
||||
user, writing `brightness` visibly changes them.
|
||||
- **Power-off fix**: `dmesg | grep 'GPIO 18'` shows the pin's register and
|
||||
whether the S4/S5 wake bit is set (recent kernels: set, "cleared at
|
||||
power-off"); `/sys/kernel/debug/gpio` has an S4/S5 column. The real test
|
||||
needs the user at the machine: shut down and check it stays off (30 s+),
|
||||
2-3 times; control run: `sudo rmmod steamify_fremont_poweroff`, shut down,
|
||||
it should boot again right away. Do this for every new major kernel.
|
||||
Install a test kernel next to the current one under another package name
|
||||
(e.g. `linux-cachyos-bore`) so the user can pick the old one in Limine.
|
||||
- **BIOS**: item 10 is tickable only if Valve has a newer BIOS than
|
||||
`cat /sys/class/dmi/id/bios_version`. Walk the flow with
|
||||
`WIZARD_BIOS_DRY_RUN=1` (never flashes). **Never run a real flash unless
|
||||
the user explicitly asks in this session**, and only on AC power.
|
||||
|
||||
## Reboot checks
|
||||
|
||||
The session reboots; in mode A the Claude session ends. Tell the user
|
||||
before rebooting and what to check afterwards.
|
||||
|
||||
- Single user mode on: SDDM autologin without greeter (`pgrep sddm-greeter` empty).
|
||||
- Everything off: plasmalogin greeter shown.
|
||||
- Stuck in a gamescope relogin loop: over SSH
|
||||
`sudo /usr/lib/steamos/steam-set-session plasma.desktop && sudo systemctl restart display-manager`.
|
||||
|
||||
## Restoring the machine
|
||||
|
||||
No snapshots, so before a test run note the starting state (`vmstate.sh`,
|
||||
`cmp.sh save` into `~/.cache/vm-baseline`) and afterwards turn back off
|
||||
what the test turned on through the menu (the undo journals do the rest).
|
||||
Check `cmp.sh` is clean except KDE's own default-valued keys.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- To see the plan, answer "n" at "Go ahead?" (`'\nn\nq\n'`). Piping a "y"
|
||||
applies it, on real hardware; never wrap a run that may call pacman in
|
||||
`timeout`: killing pacman after its transaction but before its hooks
|
||||
leaves a stale boot entry and `db.lck` (fix: remove the lock, reinstall the
|
||||
same packages so the hooks run).
|
||||
- The user's login shell is fish: inline `ssh host '...'` commands with `$`
|
||||
or `\$` break; always send bash heredocs (`sm bash -s << 'EOF'`).
|
||||
- pacman 404 on a `.sig`: the local sync database is stale (the mirror moved
|
||||
on to a newer version). A full `pacman -Syu` fixes it; don't fetch
|
||||
signatures by hand.
|
||||
- CachyOS's `linux-cachyos` is clang-built, `-bore` GCC-built: when building
|
||||
a module by hand, pass `LLVM=1` only for clang kernels (DKMS picks itself).
|
||||
|
||||
- `pkill -f '<pattern>'` also matches the shell running it; kill by PID.
|
||||
- Apps started straight from SSH lack the session's Qt theme or core-dump
|
||||
(spectacle): start them with `systemd-run --user`.
|
||||
- Screenshots: `systemd-run --user --wait -q spectacle -b -n -f -o /tmp/x.png`
|
||||
with the session env above; in gaming mode spectacle can't see gamescope,
|
||||
use Steam's own screenshot (Steam + R1) instead.
|
||||
- `makepkg -i` runs `sudo -k` and always asks for the password again; the
|
||||
real machine has no NOPASSWD rule, so scripted runs that need sudo stop
|
||||
at the prompt. Run those in a terminal the user can type into, or have the
|
||||
user add a temporary sudoers rule and remove it afterwards.
|
||||
- Check the bundle like CI: `.github/tools/bundle.sh`, then
|
||||
`shellcheck -S warning -e SC2034,SC2154 dist/steamify.sh`.
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: steamify-branch-cleanup
|
||||
description: Clean up git branches in steamify-cachyos (and sibling steamify repos) following the release-branch flow. Use when the user asks to clean up / prune branches.
|
||||
---
|
||||
|
||||
# Steamify branch cleanup
|
||||
|
||||
Rules:
|
||||
- ALWAYS keep `main` and every `release/X.Y.Z` branch (local and remote). Never delete them.
|
||||
- Delete `feature/*` and `bugfix/*` branches (local and remote) only when merged into `origin/main` or the current `origin/release/*` branch.
|
||||
- Delete local branches whose remote is gone (`[origin/...: gone]`) only if they are `feature/*` or `bugfix/*`; local stale `release/*` branches are kept unless the user says otherwise.
|
||||
- Never touch unmerged branches; list them and report instead.
|
||||
- Never commit or merge to main.
|
||||
|
||||
Merging a feature/bugfix PR yourself (into a release branch, or `feat/steamify` in the ISO repo): delete the
|
||||
branch at once, remote and local (`gh pr merge N --merge --delete-branch`, then `git branch -D`, `git fetch
|
||||
--prune`); the user asked for this on 2026-09-29. Release branches are never deleted (kept as history next to
|
||||
their tags). Unmerged old branches (`feat/vram-booster`, `refactor/qml-screens`, `backup/*`) are reported, not
|
||||
touched. The GitHub-side `release delete <tag> --cleanup-tag` is how test ISO releases go.
|
||||
|
||||
Steps:
|
||||
1. `git fetch --all --tags --prune`
|
||||
2. `git checkout main && git merge --ff-only origin/main`
|
||||
3. List candidates: `git branch -vv`, `git branch -r --merged origin/main`, and same for the current release branch.
|
||||
4. Delete local (`git branch -d`) and remote (`git push origin --delete <b>`) feature/bugfix branches per the rules above.
|
||||
5. Report what was deleted and what was kept.
|
||||
|
||||
Note: remote deletions may be blocked by the auto-mode classifier; if so, stop and let the user approve a permission rule or run it via `!`.
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
name: steamify-iso-release
|
||||
description: Use when releasing, debugging or changing how the Steamify ISO is built and published - the GitHub tag that names it, the Gitea mirror (git.upriser.nl) that builds and attaches the 3.2 GB ISO, the Steamify workflows that start it, and every trap met while setting it up (413, privileged runner, sudo dropping variables, mirror syncs deleting tags, upload-artifact@v3, GitHub 429). Also for the Gitea side of steamify-cachyos' own releases.
|
||||
---
|
||||
|
||||
# Steamify ISO release pipeline (set up 2026-09-29, first ISO out the same night)
|
||||
|
||||
Tested end to end: release `v2.9.3-dev.2026.09.29-2044` -> `steamify-cachyos-2.9.3-dev.2026.09.29-2044-x86_64.iso`
|
||||
on git.upriser.nl, GitHub's link to it answers 200.
|
||||
|
||||
## The flow (one tag names everything)
|
||||
|
||||
1. **GitHub, `iso-1-github-tag.yml`** (ISO repo `theupriser/steamify-cachyos-live-iso`, `workflow_dispatch` only):
|
||||
takes Steamify's newest release (`X.Y.Z`), the time **now in UTC** and makes an **annotated tag**
|
||||
`vX.Y.Z-[dev.]YYYY.MM.DD-HHMM` plus a GitHub release with the changelog notes and the *direct* download link
|
||||
on Gitea. `dev.` and a pre-release on `feat/steamify` (test build), none on `master` (a release).
|
||||
GitHub is the only place that reads a clock.
|
||||
2. **The mirror syncs** (git.upriser.nl is a pull mirror of GitHub, every 10 minutes): the tag arrives.
|
||||
3. **Gitea, `iso-2-gitea-build.yml`** (`on: push: tags: ['v*']`, skipped on GitHub by `github.server_url`): parses the
|
||||
tag, builds the ISO on the runner with exactly that Steamify (`STEAMIFY_VERSION`), the tag's time (label) and
|
||||
the tag without its `v` (`STEAMIFY_ISO_VERSION`: file name, boot menu, `/etc/steammachine-iso-build`), then
|
||||
attaches ISO + `.sha256` + `.sha1` + `.pkgs.txt` to the mirror's release for that tag
|
||||
(`akkuman/gitea-release-action`, the run's own token). Never overwrites a release that already has its ISO.
|
||||
4. **Trigger:** by hand (*Actions -> ISO 1/2 · Tag and release (GitHub) -> Run workflow*, or
|
||||
`gh workflow run iso-1-github-tag.yml -R theupriser/steamify-cachyos-live-iso --ref feat/steamify`), or by a
|
||||
new Steamify release: `steamify-cachyos`' `bundle.yml` step "Start the Steamify ISO's release" (secret
|
||||
`ISO_DISPATCH_TOKEN` there: fine-grained token, only the ISO repo, *Actions: read and write*; without it the
|
||||
step is skipped). A push does **not** release (a build is 20 minutes and 3.2 GB).
|
||||
5. Names, for tag `v2.9.3-dev.2026.09.29-2044`: file `steamify-cachyos-2.9.3-dev.2026.09.29-2044-x86_64.iso`,
|
||||
label `STEAMIFY_2_9_3_20260929_2044` (ISO 9660: 32 chars, `A-Z 0-9 _`). By hand, without a tag: `local`
|
||||
(`steamify-cachyos-local-x86_64.iso`, `STEAMIFY_<version>_LOCAL`). The build reads no clock.
|
||||
|
||||
Total from a Steamify release to the ISO on Gitea: about 30 minutes (release, tag, sync <=10 min, build 15-20).
|
||||
|
||||
## Why it is split like this (learned the hard way)
|
||||
|
||||
- **A release made only on the mirror disappears** at the next sync: the pull mirror removes tags GitHub doesn't
|
||||
have, and the release goes with it (one that has files may linger, tagless, and sorts oddly: delete it). A
|
||||
release on a tag that **comes from GitHub survives** every sync, files included (tested with a 0-byte asset).
|
||||
Hence: GitHub makes the tag, Gitea only attaches the file. You can't delete a tag on a mirror by hand either.
|
||||
- GitHub releases take **2 GB per file**; the ISO is 3.2 GB. So GitHub gets notes and a link, Gitea the file.
|
||||
- **Annotated tags**: a lightweight tag has only its commit's date, so tags on the same commit sort randomly
|
||||
on Gitea. UTC is fine (the release name says "UTC"; it is 2 h behind Dutch summer time, and nobody cared).
|
||||
- The GitHub release's link is built from the tag alone (`.../releases/download/<tag>/steamify-cachyos-<tag
|
||||
without v>-x86_64.iso`), so the Gitea file name must be that: check it with `curl -I -L` (the monitor in the
|
||||
session did: HTTP 200 = the whole chain is right).
|
||||
|
||||
## Gitea side: what the runner and the server need
|
||||
|
||||
- **Runner** (act_runner in Docker Compose, network `gitea_gitea`): `config.yaml` -> `container: privileged: true`
|
||||
(the workflow's `options: --privileged` is **ignored** otherwise; symptom: `mount: .../airootfs/proc: permission
|
||||
denied`, `failed to setup chroot`; the job log shows `Privileged:false`). ~20 GB free disk.
|
||||
- **Upload limit: `[repository.release] FILE_MAX_SIZE = 10240`** in `app.ini` (MB; default 2048), restart Gitea.
|
||||
`[attachment] MAX_SIZE` is for issues, **not** releases: symptom `status: 413` on `.../releases/<id>/assets`.
|
||||
The release is created *before* the upload, so a failed run leaves an empty release: delete it.
|
||||
- Logs of a public repo's run are readable without login: `.../actions/runs/<run>/jobs/<job>/logs` (job numbers
|
||||
count up; re-runs get a new job number). Good for a monitor; no `gh` for Gitea.
|
||||
- Mirror sync is set to 10 min by the user with their own token; "Synchronize Now" or the API
|
||||
(`POST /api/v1/repos/<repo>/mirror-sync`) for now. `GIT_UPRISER_TOKEN` (secret in the ISO repo) would make GitHub
|
||||
trigger it; not needed.
|
||||
- **Build traps in the container:** `sudo mkarchiso` in `util-iso.sh` **drops environment variables**:
|
||||
it needs `sudo --preserve-env=STEAMIFY_ISO_VERSION,STEAMIFY_BUILD_STAMP` (the first Gitea ISO came out as
|
||||
`-local-`). Step names are evaluated **before** earlier steps set env: don't put `${{ env.X }}` in a name.
|
||||
Lots of `error: command failed to execute correctly` (pacman hooks wanting systemd) are harmless.
|
||||
The bash test of the naming must go through `sudo` too, not only call `profiledef.sh` directly.
|
||||
- Kernel: the ISO is built from the generic `x86_64` repo (must boot on any PC), which lags the `v3/v4/znver4`
|
||||
repos by a day or so: 7.2.7 on the ISO while installed systems get 7.2.8. Normal, not a bug: the installer
|
||||
switches to the CPU's optimised repo (znver4 on the 9800X3D) and the first update brings the newer kernel.
|
||||
|
||||
## Steamify's own workflow (`steamify-cachyos/.github/workflows/bundle.yml`), one file for both servers
|
||||
|
||||
- Build + check run everywhere. shellcheck through `ludeeus/action-shellcheck@2.0.0` (brings its own binary:
|
||||
act_runner's image has none; GitHub's has). The artifact: `actions/upload-artifact@v4` on GitHub,
|
||||
`christopherhx/gitea-upload-artifact@v4` elsewhere. **Never write `upload-artifact@v3`**: GitHub fails the
|
||||
whole workflow for merely mentioning a deprecated version, even in a skipped step.
|
||||
- Publishing: GitHub release (`gh`, tag + `latest`) only on GitHub; on Gitea the release is `akkuman/gitea-release-action`
|
||||
with a Gitea-API version check (`RELEASE_TOKEN` secret optional, else the run's token). Steps are guarded by
|
||||
`github.server_url == 'https://github.com'` (or `!=`). YAML check without PyYAML on the Mac:
|
||||
`ruby -ryaml -e 'YAML.load_file(...)'`.
|
||||
- Steamify releases still follow the AGENTS.md flow (`release/X.Y.Z`, `bugfix/`/`feature/` branch, PR into the
|
||||
release branch, merged and **deleted** by the agent, the release PR into `main` is the user's). Even a CI-only
|
||||
change is a release (2.9.2, 2.9.3).
|
||||
|
||||
## GitHub rate limit that started this (HTTP 429)
|
||||
|
||||
`raw.githubusercontent.com` refused Valve's CEC driver source after a day of test installs (each CEC turn-on
|
||||
downloaded it again; the limit is per address, ~1 per 5 minutes seen). Fixed in Steamify 2.9.1: the driver is
|
||||
cached in `/var/cache/steamify/cros-ec-cec-<sha256:12>.c` (named after its checksum: a new pin downloads once and
|
||||
deletes the old file; a damaged file is fetched again). The test VMs share the host's `~/vms/pkg-cache/steamify`
|
||||
(bound over `/var/cache/steamify` by `vmsuite.sh`, `vmbootloadertest.sh`, `vminstall-live.sh`); hw block 50 tests
|
||||
the cache offline (`file://`). The source repo `evlaV/linux-integration` is an unofficial mirror without releases:
|
||||
if it ever vanishes, ship the file with Steamify.
|
||||
|
||||
## Checklist for a release
|
||||
|
||||
1. Steamify released? (`gh release list -R theupriser/steamify-cachyos`). The ISO gets the newest.
|
||||
2. `gh workflow run iso-1-github-tag.yml ... --ref feat/steamify` (test) or `--ref master` (release; master must have
|
||||
the workflows: it doesn't yet, `feat/steamify` is the ISO repo's working branch).
|
||||
3. Watch: tag on Gitea (`.../api/v1/repos/theupriser/steamify-cachyos-live-iso/tags`), the run's log (above),
|
||||
then `curl -I -L` the GitHub link. Old test releases: delete on GitHub (`gh release delete <tag>
|
||||
--cleanup-tag -y`); the sync removes the tag on Gitea; delete a leftover tagless Gitea release by hand.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,26 @@
|
||||
name: Sync git.upriser.nl mirror
|
||||
|
||||
# Trigger the Gitea pull-mirror immediately instead of waiting for its interval,
|
||||
# so Gitea builds/releases the ISO right after main or a tag lands on GitHub.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ['*']
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
# Gitea also reads .github/workflows on the mirror; only run on GitHub
|
||||
if: github.server_url == 'https://github.com'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger mirror-sync
|
||||
env:
|
||||
GITEA_URL: ${{ secrets.GIT_UPRISER_URL }}
|
||||
GIT_UPRISER_TOKEN: ${{ secrets.GIT_UPRISER_TOKEN }}
|
||||
# Gitea "owner/repo"; defaults to the same path as on GitHub
|
||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.repository }}
|
||||
run: |
|
||||
curl --fail-with-body -sS --retry 5 --retry-delay 10 --retry-all-errors -X POST \
|
||||
-H "Authorization: token ${GIT_UPRISER_TOKEN}" \
|
||||
"${GITEA_URL%/}/api/v1/repos/${GITEA_REPO}/mirror-sync"
|
||||
+9
-3
@@ -5,16 +5,21 @@
|
||||
!/README.md
|
||||
!/CHANGELOG.md
|
||||
!/run.sh
|
||||
!/run.ps1
|
||||
!/get-iso.sh
|
||||
!/get-iso.ps1
|
||||
!/add-ssh-key.ps1
|
||||
!/TESTPLAN.md
|
||||
!/TODO.md
|
||||
!/AGENTS.md
|
||||
!/.github/
|
||||
!/scripts/
|
||||
!/share/
|
||||
!/.claude/
|
||||
|
||||
share/*
|
||||
!share/guest-ssh-setup.sh
|
||||
!share/vminstall-live.sh
|
||||
!share/vminstall-post.sh
|
||||
!share/bootloader-test/
|
||||
!share/vmtest/
|
||||
|
||||
# Belt and braces, anywhere in the tree
|
||||
*.iso
|
||||
@@ -26,3 +31,4 @@ share/*
|
||||
*.log
|
||||
*.tmp
|
||||
host-keys.pub
|
||||
__pycache__/
|
||||
@@ -0,0 +1,63 @@
|
||||
# steamify-cachyos-dev: how to work here
|
||||
|
||||
Test tooling for Steamify (`../steamify-cachyos`) and the Steam Machine ISO (`../steamify-cachyos-live-iso`):
|
||||
VM scripts (`scripts/`, `run.sh`), the automated test (`scripts/vmtest.sh`, checks in `share/vmtest/` and
|
||||
`share/bootloader-test/`), `TESTPLAN.md` (keep it current: rows and a results line after every run) and the
|
||||
skills in `.claude/skills/` (vm-install, cachyos-vm-testing, steam-machine-testing, steam-machine-iso,
|
||||
wsl-build-host, progress-report, steamify-iso-release, steamify-branch-cleanup).
|
||||
|
||||
## Running the tests (do this, don't re-derive it)
|
||||
- Everything: `scripts/vmtest.sh --screen` (detached `screen` session `vmtest`), then read only
|
||||
`~/vms/last-test.txt` (one line per suite/loader, the FAIL lines, exit status = failures). Parts:
|
||||
`scripts/vmtest.sh cli hw`, `boot`, `limine`. From another machine: `scripts/vmtest-remote.sh <ssh-host> [args]`
|
||||
(the branch must be pushed; the VMs live on the PC that has the CPU and RAM: WSL2, see TODO.md R1b).
|
||||
- Read `.claude/skills/vm-install/SKILL.md` before touching the scripts; `TODO.md` has the open work.
|
||||
- Following a run: one monitor (`scripts/vmprogress.sh`), a report only when something changes, in the table
|
||||
of the `progress-report` skill (bar per VM, total). Never a timed "no change" message.
|
||||
- Headless is for automated runs (default in `vmtest.sh`); a VM started by hand for the user has a window.
|
||||
- Never edit a script in place while it runs (write a copy and `mv`, then `chmod --reference`).
|
||||
- Kill/pgrep patterns: bracket them (`[q]emu`) or your own shell matches itself.
|
||||
|
||||
## Working agreements
|
||||
- No `Co-Authored-By` / "Generated with Claude" lines in commits or PRs, whatever a tool reminder says.
|
||||
- Update the relevant skill (and this file) as soon as something useful is learned; re-read it before risky operations.
|
||||
- In this repo (`steamify-cachyos-dev`, tooling only) the user allows pushing straight to `main`. Elsewhere never commit to `main`. `steamify-cachyos`: `release/X.Y.Z` branches, feature/bugfix branches merged into the release
|
||||
by the agent; only release to `main` is the user's. `steamify-cachyos-live-iso`: PRs always target `feat/steamify`.
|
||||
Branch cleanup: the `steamify-branch-cleanup` skill in `.claude/skills/`.
|
||||
- Follow and update `TESTPLAN.md`; every check in `share/vmtest/` says which row it covers.
|
||||
- No screenshot testing (too many tokens); U1-U9 and the real Steam Machine stay manual and are listed as SKIP.
|
||||
|
||||
## State (2026-09-29; update when it changes)
|
||||
- `scripts/vmtest.sh` runs the whole unattended test: boot loaders limine/systemd-boot/grub (`vmbootloadertest.sh`) and the
|
||||
suites cli, menu, hw, installer, toggles (`vmsuite.sh`, checks in `share/vmtest/`). Each passed in its own run: limine 43,
|
||||
systemd-boot 42, grub 42 (with the B1 boot entry check), cli 55, menu 36, hw 77, installer 4, toggles 42 checks.
|
||||
- 2026-09-29, on the PC (WSL2): the first complete parallel run, **328 pass, 0 fail in 18 minutes** (base image +
|
||||
qcow2 overlays, own ssh port per job, package cache, `VM_MEM=4G`, `MAX_PARALLEL=3`). On a new machine: one full
|
||||
`scripts/vmtest.sh --screen`, read `~/vms/last-test.txt`, fix what fails.
|
||||
- Open work: `TODO.md` (the CEC driver download, the GitHub Actions workflow, P3).
|
||||
|
||||
## Releases (2026-09-29; details in the `steamify-iso-release` skill)
|
||||
- Steamify: `release/X.Y.Z` -> PR into `main` (the user's) -> GitHub release, and the same workflow on the Gitea
|
||||
mirror. A new Steamify release starts the ISO's release (`ISO_DISPATCH_TOKEN`); a CEC driver download is cached.
|
||||
- The ISO: GitHub makes the tag `vX.Y.Z-[dev.]YYYY.MM.DD-HHMM` (UTC) and a release with the notes; the Gitea
|
||||
mirror (git.upriser.nl, pull mirror, 10 min) builds on that tag and attaches the 3.2 GB ISO, named after the tag.
|
||||
A release that exists only on the mirror is deleted by its next sync: the tag must come from GitHub.
|
||||
- Merged feature/bugfix branches are deleted at once (`gh pr merge --delete-branch`). Push every commit.
|
||||
|
||||
## Where the tests run
|
||||
The tests run on the user's PC (Ryzen 9800X3D, 64 GB, **WSL2**), started from a laptop over ssh (the user's own skill, or
|
||||
`scripts/vmtest-remote.sh <ssh-host> [args]`: pulls the pushed branch there, starts `vmtest.sh --screen`, waits for
|
||||
`~/vms/last-test.txt`). WSL specifics (access `ssh wsl` = root@10.0.0.36, keep the Arch window open or WSL shuts
|
||||
down, syncing from the laptop, the podman ISO build, VM windows through WSLg, output in the user's WSL terminal, no GPU
|
||||
passthrough): `.claude/skills/wsl-build-host/SKILL.md`. `vminstall.sh` works there without udisks (losetup as root).
|
||||
|
||||
## Problems found in the Steamify ISO (fix in `steamify-cachyos-live-iso`, PRs into `feat/steamify`)
|
||||
- Fixed (PR #2): cachyos-installer's `bootctl install` runs in a chroot and writes no EFI boot entry, so systemd-boot
|
||||
only booted through the disk's fallback path (in OVMF after 4-5 minutes of network boot, every boot).
|
||||
`steamify-install` now registers it with efibootmgr from the live system.
|
||||
- Fixed (PR #3): `/var/log` (`@log`) wasn't mounted when `steamify-install` ran, so its logs were hidden after boot.
|
||||
- cachyos-installer leaves systemd-boot with `#timeout 3` and no default entry: a real machine waits in the menu for ever
|
||||
(the test setup papers over it in `share/vminstall-post.sh`).
|
||||
- It enables ufw, which drops ssh from the host (the test setup runs `ufw allow 22/tcp`).
|
||||
- An installed system's package database can be older than what the installer got (mirror skew): a kernel "update" may
|
||||
downgrade. Not a Steamify bug; noted in the vm-install skill.
|
||||
@@ -4,6 +4,38 @@ All notable changes, per version and per commit. Versions follow
|
||||
[Semantic Versioning](https://semver.org/) and were numbered from the start
|
||||
of the history.
|
||||
|
||||
## 0.4.0 - 2026-09-28
|
||||
|
||||
An unattended VM install, a test plan, and the lessons from testing
|
||||
Steamify 2.7.0.
|
||||
|
||||
- **feat: scripts/vminstall.sh installs an ssh-ready test VM unattended; TESTPLAN.md; vm-install skill**
|
||||
- `scripts/vminstall.sh [--force] [--iso <file>] [--fremont]`: CachyOS from
|
||||
the ISO's headless `cachyos-installer` (kernel booted directly with
|
||||
`systemd.run=` + `systemd.wants=`, files and log over
|
||||
`scripts/vminstall-server.py`), then `share/vminstall-post.sh`: sshd,
|
||||
passwordless sudo, test autologin, Limine `default_entry`. Snapshots
|
||||
`clean` and `ssh-ready`. User: the host's username, password `steamify`;
|
||||
key `~/.ssh/steamify-vm_ed25519` (made once, never overwritten).
|
||||
- `scripts/common.sh`: `VM_DIR`, and the VM's user, key and known_hosts from
|
||||
it; `run.sh`: `VM_ISO`, `VM_KERNEL`/`VM_INITRD`/`VM_APPEND`, `VM_SERIAL`,
|
||||
`repo-path`.
|
||||
- `scripts/steammachine-addkey.sh`: the VM key on the Steam Machine too.
|
||||
- `TESTPLAN.md`: regression, the app, features per release, Steam Machine
|
||||
hardware (faked and real), results log. `scripts/qmpkey.py` presses keys
|
||||
in the VM (drives the app).
|
||||
- Fixes: `vmreset.sh` doesn't restart plasmalogin when the snapshot already
|
||||
logged in (HELPER_TTY_ERROR); `vmstate.sh` reads `~/.local/state/steamify`;
|
||||
`vminstallsim.sh` passes `LANG=C.UTF-8` like the ISO.
|
||||
- Skills: update `ssh-ready` at the start of every session; the ISO is built
|
||||
in the test VM.
|
||||
|
||||
## 0.3.0 - 2026-09-27
|
||||
|
||||
The Steam Machine ISO and Steamify's install-time mode.
|
||||
|
||||
- `f8e2473` **feat: steam-machine-iso skill (ISO build in podman on the Steam Machine, installer simulation, first-login test) and scripts/vminstallsim.sh**
|
||||
|
||||
## 0.2.0 - 2026-09-24
|
||||
|
||||
Watchable test runs and the lessons from testing cachyos-gamescope-boot 0.7.0
|
||||
|
||||
@@ -11,116 +11,116 @@ tracked file to it explicitly.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Linux host with KVM, `qemu-system-x86_64` (GTK/OpenGL display, virtfs/9p) and `qemu-img`
|
||||
- An x86_64 Linux host with KVM (not Windows or macOS: `run.sh` relies on KVM,
|
||||
memfd and GTK/virgl, and CachyOS only exists for x86_64),
|
||||
`qemu-system-x86_64` (GTK/OpenGL display, virtfs/9p) and `qemu-img`
|
||||
(Ubuntu: `sudo apt install qemu-system-x86 qemu-utils ovmf`).
|
||||
- OVMF firmware at `/usr/share/OVMF/OVMF_CODE_4M.fd` and `OVMF_VARS_4M.fd`.
|
||||
- `curl` and `sha256sum` for `get-iso.sh`.
|
||||
- Optional: an NVIDIA dGPU for `--nvidia` (PRIME offload of the virgl renderer).
|
||||
|
||||
### Windows
|
||||
|
||||
`run.ps1` and `get-iso.ps1` are Windows versions. They need
|
||||
[QEMU for Windows](https://qemu.weilnetz.de/w64/) (or set `QEMU_DIR`) and the
|
||||
"Windows Hypervisor Platform" feature (`-accel whpx`). Windows QEMU has no 9p
|
||||
shares: copy the repo into the guest with `scp -P 2222 -r ...`, and authorize
|
||||
your key with `.\add-ssh-key.ps1 -User <vm-user>`.
|
||||
|
||||
## Quick start
|
||||
## Quick start: unattended install
|
||||
|
||||
```bash
|
||||
./get-iso.sh # download and verify the latest CachyOS desktop ISO
|
||||
./run.sh install # first run creates disk.qcow2 (60G) and vars.fd, boots the ISO
|
||||
./get-iso.sh # latest CachyOS desktop ISO (verified)
|
||||
VM_DIR=~/vms/steamify-vm scripts/vminstall.sh --fremont # 15-30 min, watch the VM's window
|
||||
```
|
||||
|
||||
Install CachyOS with the **KDE Plasma** desktop and **plasma-login-manager**
|
||||
as login manager. Power off and snapshot (only while the VM is off):
|
||||
Installs CachyOS from the ISO without any manual step, with CachyOS's own
|
||||
headless installer (KDE Plasma, plasma-login-manager, btrfs, Limine,
|
||||
`linux-cachyos` + `-lts`), and leaves the snapshots `clean` and `ssh-ready`:
|
||||
sshd with your key, passwordless sudo, autologin into Plasma, English, the
|
||||
host's timezone. Details: [`.claude/skills/vm-install`](.claude/skills/vm-install/SKILL.md).
|
||||
|
||||
- **Login by hand**: your host username, password **`steamify`** (root too).
|
||||
- **SSH**: `~/.ssh/steamify-vm_ed25519`, made on the first install (it asks:
|
||||
that key, or one of your own `~/.ssh/*.pub`), then always used. Existing
|
||||
keys are never overwritten; the VM's host key lives in `$VM_DIR/known_hosts`,
|
||||
not in `~/.ssh/known_hosts`.
|
||||
- `$VM_DIR` also records the user, key and Steamify checkout (`vm-user`,
|
||||
`ssh-key`, `repo-path`); pass the same `VM_DIR` to the other scripts.
|
||||
- `--iso <file>` installs another archiso-based ISO the same way (the Steam
|
||||
Machine ISO, later); `--force` replaces an existing VM (asks first).
|
||||
|
||||
Snapshots (only while the VM is off; keep vars.fd with the disk):
|
||||
|
||||
```bash
|
||||
qemu-img snapshot -c clean disk.qcow2 && cp vars.fd vars.clean.fd
|
||||
qemu-img snapshot -a ssh-ready disk.qcow2 && cp vars.ssh-ready.fd vars.fd # restore
|
||||
```
|
||||
|
||||
Boot the installed system (`./run.sh`) and, in the guest, enable SSH with key
|
||||
auth and passwordless sudo through the read-only `vmtools` share:
|
||||
### Manual install (fallback)
|
||||
|
||||
`./run.sh install` boots the ISO (a first run creates `disk.qcow2`, 60G, and
|
||||
`vars.fd`). Install CachyOS with **KDE Plasma** and **plasma-login-manager**
|
||||
through Calamares, power off, `qemu-img snapshot -c clean disk.qcow2 && cp
|
||||
vars.fd vars.clean.fd`. Boot it (`./run.sh`) and, in the guest:
|
||||
|
||||
```bash
|
||||
sudo mount -t 9p -o trans=virtio,version=9p2000.L vmtools /media && /media/guest-ssh-setup.sh
|
||||
```
|
||||
|
||||
### Adding your SSH key
|
||||
(sshd, the host's `~/.ssh/*.pub` from `share/host-keys.pub`, passwordless
|
||||
sudo). Power off, `qemu-img snapshot -c ssh-ready disk.qcow2 && cp vars.fd
|
||||
vars.ssh-ready.fd`.
|
||||
|
||||
`guest-ssh-setup.sh` authorizes the host's public keys. `run.sh` copies them
|
||||
at every start from `~/.ssh/*.pub` into `share/host-keys.pub` (git-ignored),
|
||||
which the guest sees as `/media/host-keys.pub`. So:
|
||||
|
||||
1. Make sure you have a key on the host; create one if `ls ~/.ssh/*.pub`
|
||||
shows nothing:
|
||||
|
||||
```bash
|
||||
ssh-keygen -t ed25519
|
||||
```
|
||||
|
||||
2. Start (or restart) the VM with `./run.sh`, so the key is copied into the
|
||||
share.
|
||||
3. In the guest, run the setup script (above). It installs and starts
|
||||
`sshd`, opens port 22 if a firewall is active, adds every host key to
|
||||
`~/.ssh/authorized_keys` (without duplicates) and gives the guest user
|
||||
passwordless sudo.
|
||||
4. Test from the host: `ssh -p 2222 <vm-user>@localhost true` should return
|
||||
without asking for a password.
|
||||
|
||||
If you ran the setup script before the key existed (it prints "No host keys
|
||||
were shared"), or you want to add another key later, either restart the VM
|
||||
and run the setup script again, or copy the key over SSH with the guest
|
||||
user's password:
|
||||
### The same key on the Steam Machine
|
||||
|
||||
```bash
|
||||
ssh-copy-id -p 2222 <vm-user>@localhost
|
||||
scripts/steammachine-addkey.sh [user@host] # default: steammachine (~/.ssh/config)
|
||||
```
|
||||
|
||||
On Windows, use `.\add-ssh-key.ps1 -User <vm-user>` instead (it creates a key
|
||||
if needed). If SSH hangs at "banner exchange", `sshd` isn't running in the
|
||||
guest or a firewall blocks port 22; check with `systemctl is-active sshd` in
|
||||
the guest.
|
||||
|
||||
Power off and snapshot again:
|
||||
|
||||
```bash
|
||||
qemu-img snapshot -c ssh-ready disk.qcow2 && cp vars.fd vars.ssh-ready.fd
|
||||
```
|
||||
|
||||
Connect with `ssh -p 2222 <user>@localhost`. The project repo is shared
|
||||
read-write as 9p tag `repo`; in the guest:
|
||||
`sudo mount -t 9p -o trans=virtio,version=9p2000.L repo /mnt`.
|
||||
authorizes `~/.ssh/steamify-vm_ed25519` on the Steam Machine too (asks its
|
||||
password once), so one key reaches the VM and the real machine.
|
||||
|
||||
### run.sh options
|
||||
|
||||
```
|
||||
[REPO=/path/to/cachyos-gamescope-boot] ./run.sh [install] [--nvidia] [--vulkan] [--fremont]
|
||||
[REPO=/path/to/steamify-cachyos] ./run.sh [install] [--nvidia] [--vulkan [--amd]] [--fremont]
|
||||
```
|
||||
|
||||
- `install`: boot the installer ISO
|
||||
- `install`: boot the installer ISO (`VM_ISO`, default `cachyos.iso`)
|
||||
- `--nvidia`: render the guest's virtio-gpu (virgl) on the host NVIDIA dGPU
|
||||
- `--vulkan`: expose Vulkan to the guest (venus; unstable)
|
||||
- `--fremont`: fake the Valve Steam Machine (Fremont) DMI data via `-smbios`
|
||||
- `REPO` defaults to `$HOME/projects/cachyos-gamescope-boot` (the local clone of
|
||||
[steamify-cachyos](https://github.com/theupriser/steamify-cachyos))
|
||||
- `REPO`: the Steamify checkout shared as 9p `repo` (in the guest: `sudo
|
||||
mount -t 9p -o trans=virtio,version=9p2000.L repo /mnt`); default
|
||||
`repo-path` next to the disk, else `$HOME/projects/cachyos-gamescope-boot`
|
||||
- `VM_KERNEL`/`VM_INITRD`/`VM_APPEND` (boot a kernel directly) and
|
||||
`VM_SERIAL` (serial console log + socket): used by `vminstall.sh`
|
||||
|
||||
## Helper scripts
|
||||
|
||||
- `scripts/vminstall.sh [--force] [--iso <file>] [--fremont]`: unattended install (above)
|
||||
- `scripts/vmreset.sh [--fremont]`: restore `ssh-ready`, boot, mount the repo, autologin into Plasma
|
||||
(and skip the broken krfoss mirror, install shellcheck)
|
||||
- `scripts/vmrun.sh '<menu input>'`: run the wizard in the guest's Plasma session with scripted input
|
||||
- `scripts/vmwatch.sh [--release] '<menu input>' [label]`: same, but in a visible Konsole window
|
||||
in the VM; `--release` runs the newest GitHub release instead of the mounted repo
|
||||
- `scripts/vminstallsim.sh [--fresh] [--defaults options]`: Steamify's install-time mode for a
|
||||
user who never logged in (the Steam Machine ISO's installer step)
|
||||
- `scripts/vmshot.sh [--clean] <out.png>`: screenshot the guest's desktop (`--clean` closes
|
||||
Steam, CachyOS Hello and Konsole first)
|
||||
- `scripts/vmstate.sh`: print the state of every wizard component
|
||||
- `scripts/vmcec.sh [--no-sleep]`: HDMI-CEC against a fake TV (vivid)
|
||||
- `scripts/qmptype.py <qmp.sock> "<text>"` / `scripts/qmpkey.py <qmp.sock> <keys>`: type text / press
|
||||
keys in the VM (e.g. drive the Steamify app: `down right ctrl-ret ret`)
|
||||
- `scripts/cmp.sh [save]`: save / diff the guest's KDE configs against a baseline
|
||||
- `scripts/steammachine-addkey.sh [user@host]`: the VM key on the Steam Machine (above)
|
||||
|
||||
They use `VM_USER` (default `theupriser`), `VM_PORT` (`2222`) and `VM_HOST` (`localhost`).
|
||||
They use `VM_DIR` (default: this repo if it has `disk.qcow2`, else
|
||||
`~/vms/cachyos-test`), and from it `VM_USER` (`vm-user`, else `theupriser`)
|
||||
and `VM_SSH_KEY` (`ssh-key`); `VM_PORT` (`2222`), `VM_HOST` (`localhost`).
|
||||
|
||||
## Claude Code skill
|
||||
## Test plan
|
||||
|
||||
The full test workflow is documented in
|
||||
[`.claude/skills/cachyos-vm-testing/SKILL.md`](.claude/skills/cachyos-vm-testing/SKILL.md);
|
||||
Claude Code picks it up when run from this repo.
|
||||
[`TESTPLAN.md`](TESTPLAN.md): what to test per release (regression, the app,
|
||||
new features, Steam Machine hardware faked and real) and the results log.
|
||||
|
||||
## Claude Code skills
|
||||
|
||||
- [`cachyos-vm-testing`](.claude/skills/cachyos-vm-testing/SKILL.md): the test workflow in the VM
|
||||
- [`vm-install`](.claude/skills/vm-install/SKILL.md): creating the VM unattended
|
||||
- [`steam-machine-testing`](.claude/skills/steam-machine-testing/SKILL.md): on the real Steam Machine
|
||||
- [`steam-machine-iso`](.claude/skills/steam-machine-iso/SKILL.md): the Steam Machine ISO
|
||||
|
||||
Claude Code picks them up when run from this repo.
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
# Steamify test plan
|
||||
|
||||
What to test before a Steamify release, in the VM (`--fremont`) and on the
|
||||
real Steam Machine. How to run each step: `.claude/skills/cachyos-vm-testing`.
|
||||
**Keep this file up to date**: add a section when a feature is added, and a
|
||||
line to the results log after every test run.
|
||||
|
||||
Every VM run starts with the session start of the skill (`ssh-ready` brought
|
||||
up to date), and each numbered block below starts from a fresh `ssh-ready`
|
||||
(`scripts/vmreset.sh --fremont`, `REPO=` the branch checkout). Runs are
|
||||
visible in a Konsole on the VM's desktop.
|
||||
|
||||
## R. Regression (every release)
|
||||
|
||||
### R1. Menu on an existing desktop (user `theupriser`)
|
||||
|
||||
| # | Step | Expect |
|
||||
|---|---|---|
|
||||
| R1.1 | `'q\n'` | first-run ticks: everything but Boot into desktop, BIOS |
|
||||
| R1.2 | `'\ny\nm\nq\nn\n'` | every item OK, back in the menu with all on |
|
||||
| R1.3 | `scripts/vmstate.sh` | SDDM + autologin, Vapor, 17 LEDs owned by the user, DKMS `installed` for every kernel, steamos-manager active |
|
||||
| R1.4 | rerun `'\nq\n'` | "Everything is already the way you want it" |
|
||||
| R1.5 | re-apply `'a\ny\nm\nq\nn\n'`, `vmstate.sh` | no errors, no `DUPLICATES` |
|
||||
| R1.6 | `steamify.sh --backend status` | valid JSON, current `VERSION`, states match R1.3 |
|
||||
| R1.7 | close the app first, then `sudo modprobe vivid; scripts/vmcec.sh --no-sleep` | all PASS (2 SKIP in the VM: wake from TV, sleep). The fake remote's Select arrives as Enter: an open app on "All done" restarts the PC |
|
||||
| R1.8 | toggles (theme, single user, CEC, Steam Machine support off/on) | full matrix in the skill; `cmp.sh` clean after theme off |
|
||||
| R1.10 | set an item older in `~/.local/state/steamify/features.state` (e.g. `cec=2.1.0`), `'q\n'`, then `'\ny\nm\nq\nn\n'` | menu shows "HDMI-CEC (update)" ticked; the plan lists it as "update"; after the run the file has the current version |
|
||||
| R1.9 | reboot checks | single user: SDDM logs straight in; everything off: greeter |
|
||||
|
||||
### U. The app (`/mnt/ui/steamify-ui`, on the VM's desktop)
|
||||
|
||||
Drive it with `scripts/qmpkey.py <vm dir>/qmp.sock <keys>` (QEMU qcodes:
|
||||
`up down left right ret esc ctrl-ret ctrl-q r`; wait ~1 s between keys and
|
||||
~12 s after starting the app) and check every screen with a screenshot
|
||||
(`spectacle -b -n -a` for the active window; the progress screen of a quick
|
||||
change only shows right after Enter).
|
||||
|
||||
| # | Step | Expect |
|
||||
|---|---|---|
|
||||
| U1 | start after R1.5 | header `v<VERSION>`, Steam Machine chip, every row with "now on", Kernel / LED bar (17) / HDMI-CEC / Version panel |
|
||||
| U2 | Up/Down over the rows | detail panel follows: body text and "What it changes" per row |
|
||||
| U3 | an updated feature (bump `FEATURE_VERSION`, or `features.state` older) | `UPDATE` badge, "1 change" |
|
||||
| U4 | Boot into: Right / Left | Gaming / Desktop, detail panel `DESKTOP`, change count +1 |
|
||||
| U5 | Enter on a toggle, Enter again | off, back on, count back |
|
||||
| U6 | Ctrl+Enter | review lists exactly the changes, summary counts right; Esc goes back |
|
||||
| U7 | Enter (apply) | progress per step ("Boot into desktop", update badge), details log, "All done, N changes applied", Restart now / Back to the menu |
|
||||
| U8 | after U7 | the changes are on the system (e.g. boot-desktop unit, `features.state`) |
|
||||
| U9 | `r` (re-apply), Ctrl+Q | re-apply review; the app quits |
|
||||
|
||||
### R2. Installer mode (`scripts/vminstallsim.sh`, user `isotest`, no session)
|
||||
|
||||
| # | Step | Expect |
|
||||
|---|---|---|
|
||||
| R2.1 | plain `--defaults` | every item OK, `exit: 0`, first-login autostart present |
|
||||
| R2.2 | first desktop login (steam-machine-iso skill) | Vapor layout, `primaryActions=3`, autostart gone, app opens |
|
||||
|
||||
### F2.8.0 `--defaults --list`
|
||||
|
||||
| # | Step | Expect |
|
||||
|---|---|---|
|
||||
| G1 | `steamify.sh --defaults --list` | exit 0, valid JSON array, no stderr, no header text mixed in |
|
||||
| G2 | items | actions (bios), retired (kpin, hdmi) excluded; unavailable here (vram, steamgame) excluded; poweroff has parent machine, boot has parent gaming and kind choice |
|
||||
| G3 | menu / plain `--defaults` still work | unaffected (regression) |
|
||||
|
||||
## F. Features per release
|
||||
|
||||
### F2.7.0 `--defaults --options` / `--boot`
|
||||
|
||||
| # | Step | Expect |
|
||||
|---|---|---|
|
||||
| F1 | `--options` (no value), `bogus`, `boot`, `bios`, `--boot` (no value), `--boot sideways`, `--skip x` | each `exit: 1` with a clear error, nothing changed |
|
||||
| F2 | `--options theme --boot desktop` (either order) | `exit: 1`: desktop needs the conversion |
|
||||
| F3 | `--options gaming,theme,glyphs,single,launcher,notify,vram,cec,machine,poweroff --boot desktop` | all on, boot-desktop unit enabled; VRAM left out with a warning in the VM |
|
||||
| F4 | `--options theme,single,machine --boot desktop` | single brings the conversion; poweroff, CEC, glyphs, launcher, notify recorded `off` |
|
||||
| F5 | `--options launcher,vram,poweroff` | poweroff brings Steam Machine support; conversion off, plasmalogin unchanged |
|
||||
| F6 | `steamify.sh --boot desktop` without the conversion | `exit: 1` |
|
||||
| F7 | on an install: `--boot desktop`, again, `--boot gamescope` | only Boot into changes; "Already starting in desktop"; menu ticks unchanged |
|
||||
|
||||
## H. Steam Machine hardware
|
||||
|
||||
### H-VM. With the faked Steam Machine (`--fremont`)
|
||||
|
||||
| # | Step | Expect |
|
||||
|---|---|---|
|
||||
| H1 | LED driver | `leds_valve` loaded, 17 `valve-leds*` nodes owned by the user (no light to see) |
|
||||
| H2 | HDMI-CEC | R1.7 (vivid: fake TV and remote) |
|
||||
| H3 | BIOS update | `BIOS_VERSION=F7F0107 scripts/vmreset.sh --fremont`, `WIZARD_BIOS_DRY_RUN=1`: whole flow, never flashes |
|
||||
| H4 | Boot into desktop, reboot | Plasma straight after boot; Boot into gaming: SDDM autologin into gamescope (black in the VM, see the skill; check the session file) |
|
||||
| H5 | power-off module | DKMS built for every kernel, module loads, "No such device" (no GPIO wake bit in the VM) |
|
||||
|
||||
### H-Real. Only on the real Steam Machine
|
||||
|
||||
- Power-off fix: stays off after shutdown, for every new major kernel.
|
||||
- VRAM booster (the VM has no `dmem` VRAM region).
|
||||
- Gamescope itself: gaming mode, Switch to Desktop / Return to Gaming Mode.
|
||||
- LED bar light, real TV over HDMI-CEC (remote, TV on/off, sleep/wake).
|
||||
|
||||
## B. Boot loaders (`scripts/vmtest.sh`, unattended)
|
||||
|
||||
Every loader the ISO advertises (Limine, systemd-boot, GRUB), each on a VM installed from the Steamify ISO
|
||||
and started with `--fremont`. Automated: `scripts/vmtest.sh` (see the vm-install skill).
|
||||
|
||||
| # | Step | Expect |
|
||||
|---|---|---|
|
||||
| B1 | first boot after the unattended install, and every reboot | boots by itself through the loader's own EFI boot entry (`BootCurrent`, a real partition; checked after every reboot), no network boot first (systemd-boot: the entry comes from the ISO's `steamify-install` since PR #2; `timeout`/`default` set by the test setup), SSH answers |
|
||||
| B2 | Steamify's state, as its own `*_status` functions | gaming, theme, glyphs, single, launcher, notify, poweroff, cec all on |
|
||||
| B3 | OS name | `os-release` NAME/PRETTY_NAME are CachyOS's, `lsb-release` has no "with Steamify"; Limine has `TARGET_OS_NAME` |
|
||||
| B4 | loader identity, firmware setup | the expected loader; `Boot into FW: supported` (BIOS item) |
|
||||
| B5 | DKMS modules (`leds-valve`, `cros-ec-cec`, `steamify-fremont-poweroff`) | installed for every kernel, files present, `modules-load.d` entry |
|
||||
| B6 | legacy `drm.edid_firmware` removal (`hdmi_remove_boot_param`) | detected, removed, initramfs and entries rebuilt, no edid left |
|
||||
| B7 | kernel update (both kernels + headers) | DKMS rebuilds 3 modules per kernel, initramfs rebuilt, loader config regenerated |
|
||||
| B8 | reboot into the default and the other kernel | both boot, no failed units, power-off fix and LED modules loaded on each |
|
||||
|
||||
## Known gaps
|
||||
|
||||
- "Add as non-Steam game" can't be set up at install time (no Steam account
|
||||
yet); not covered by any installer test.
|
||||
- ISO: `steamify-install` and the packagechooser pages still pass `--skip`
|
||||
(removed in 2.7.0); to be replaced by the Steamify page.
|
||||
|
||||
- `/etc/plasmalogin.conf` missing before Steamify (a VM from vminstall.sh,
|
||||
not Calamares): turning the conversion off leaves an empty file instead of
|
||||
none (harmless; Calamares installs have one, restored from the backup).
|
||||
- R2.2: the first login's app is the newest *release* (`run-app`), so before
|
||||
a release it shows the old version; start the branch's app by hand
|
||||
(steam-machine-iso skill).
|
||||
|
||||
- CEC: the driver source comes from an unofficial mirror of Valve's kernel on GitHub; since 2.9.1 it is
|
||||
downloaded once and cached (/var/cache/steamify; the VMs share the host's), so only a first download
|
||||
can hit GitHub's rate limit (HTTP 429).
|
||||
|
||||
## Results log
|
||||
|
||||
| Date | Branch / version | Blocks | Result |
|
||||
|---|---|---|---|
|
||||
| 2026-09-28 | `feat/defaults-options` 2.7.0 (uncommitted `--options`, `--boot`) | R1.1-R1.6, R2.1, F1-F7 | all pass (VM `--fremont`) |
|
||||
| 2026-09-28 | same + CEC socket fix, UI label fix | R1.7, R1.10, U1-U8, H1, H4 (desktop), H5 | R1.7 first failed: `cec-audio-control.socket` never enabled (old bug, fixed: `cec_enable` enables it, `FEATURE_VERSION[cec]=2.7.0`); after: 53 PASS / 0 FAIL / 2 SKIP. UI: progress row said "Boot into Boot into" (fixed). Rest pass. Not run: R1.8, R1.9, R2.2, U9, H3 |
|
||||
| 2026-09-28 | `release/2.7.0` (VM from vminstall.sh, `~/vms/steamify-vm`) | R1.2 (fresh, CEC socket), R1.8, R1.9, R1.5 via U9, U9, R2.1, R2.2, H3 | all pass except one old bug: theme on (single on) → single off → theme off left single's launcher keys; fixed (`58e43d4`, PR #34) and retested. BIOS dry run F7F0107 → F7F0108 passed; greeter and SDDM reboot checks passed |
|
||||
| 2026-09-28 | `release/2.7.0` | H-Real | counted as passed: power-off fix, VRAM booster, LEDs, gamescope and Steam Machine support are unchanged since 2.6.0, which passed on the real Steam Machine. HDMI-CEC's change (enabling `cec-audio-control.socket`) passed with the fake TV in the VM; check the TV remote's volume keys on the real TV after updating (HDMI-CEC shows as update) |
|
||||
| 2026-09-29 | `feature/vminstall-bootloader` (Steamify ISO 2026.09.28, `--fremont`) | B1-B8 | Limine 41 / systemd-boot 40 / GRUB 40 checks pass. Found on the way (test setup, not Steamify): ufw blocks ssh, systemd-boot has no timeout/default after cachyos-installer |
|
||||
| 2026-09-28 | `release/2.8.0` (`feature/defaults-list`) | G1-G3, R1.1, R1.2 | all pass (VM `--fremont`, fake Steam Machine). Bundle shellcheck clean |
|
||||
| 2026-09-29 | Steamify 2.9.0 (`steamify-cachyos` main), ISO `feat/steamify` with PR #2 (2026.09.29), dev `main` 5b867f8; on the PC (WSL2) | everything automated: B1-B8 (3 loaders), cli, menu, hw, installer, toggles (`scripts/vmtest.sh`, parallel, `MAX_PARALLEL=3`) | **328 pass, 0 fail** (toggles 42 +1 expected skip, hw 70, cli 55, menu 36, installer 4, Limine 41, systemd-boot 40, GRUB 40) in 18 minutes: the first complete parallel run. Found on the way: systemd-boot had no EFI boot entry (installer's `bootctl` in a chroot), fixed in the ISO (PR #2); `vminstall.sh` fixes for parallel installs and a root host user |
|
||||
| 2026-09-29 | ISO `feat/steamify` + PR #3 (`/var/log`), dev `main` with the B1 boot entry check | B1-B8 | Limine 43 / GRUB 42 pass (boot entry check passes: `Limine`, `cachyos`). systemd-boot (reinstalled): 36 pass, 8 fail, all CEC: the driver download got HTTP 429 from GitHub (rate limit); boot entry check passes (`Linux Boot Manager`); `/var/log/steamify-install.log` and `steamify-bootentry.log` readable after boot |
|
||||
| 2026-09-29 | Steamify `release/2.9.1` (CEC driver cache), ISO `feat/steamify` (PRs #2, #3), dev `main` | everything automated incl. hw block 50 (CEC cache) and the B1 boot entry check | **341 pass, 0 fail** in 19 minutes (toggles 42 +1 expected skip, hw 77, cli 55, menu 36, installer 4, Limine 43, systemd-boot 42, GRUB 42) |
|
||||
@@ -0,0 +1,95 @@
|
||||
# TODO
|
||||
|
||||
## GitHub Actions: run the boot loader test in CI
|
||||
|
||||
Goal: every push/PR (and on demand) runs the whole automated test (`scripts/vmtest.sh`, see the next
|
||||
section: boot loaders plus every suite) on runners, one job per suite or loader, and fails on any `FAIL` line.
|
||||
Public repos, so the free Linux runners with KVM apply. Work on `feature/ci-bootloader-test`,
|
||||
PR into the release branch. Tick items off as they're done.
|
||||
|
||||
- [x] 1. `run.sh`: headless mode (`VM_HEADLESS=1` -> `-display none`, plain virtio-vga); still to do: make `vmtest.sh` and `vmbootloadertest.sh` headless by default (`--window` opts out, a log Konsole opens on a desktop, CI opens none) and copy the repo's run.sh into `$VM_DIR` before starting
|
||||
- [ ] 2. `vminstall.sh`: read the ISO's kernel/initramfs without `udisksctl` (bsdtar/7z when there is no session)
|
||||
- [ ] 3. `vminstall.sh` / `vmbootloadertest.sh`: no `/dev/tty` prompts and no `pgrep` on the whole host in CI (`CI=1`)
|
||||
- [ ] 4. Where the ISO comes from: the newest ISO release on git.upriser.nl (the Gitea mirror of
|
||||
`theupriser/steamify-cachyos-live-iso` builds it and attaches it to the tag's release, see the
|
||||
`steamify-iso-release` skill; GitHub's release links to it), or a `workflow_dispatch` input (release tag / URL)
|
||||
- [ ] 5. `.github/workflows/bootloader-test.yml`: matrix over `limine, systemd-boot, grub`; steps: install
|
||||
qemu + ovmf, enable /dev/kvm, fetch the ISO, run the script, upload `~/vms/bl-<loader>.test.log` as an artifact
|
||||
- [ ] 6. Runner limits: disk (sparse 60 GB qcow2, free space check), RAM (8 GB VM on a 16 GB runner), timeout (60 min)
|
||||
- [ ] 7. Cache the pacman packages between runs (`actions/cache` on `~/vms/pkg-cache`, `VM_CACHE`)
|
||||
- [ ] 8. Trigger it once on the branch, fix what the runner shows; note the run time in the skill
|
||||
- [ ] 8b. Second job, `--fremont` hardware tests from TESTPLAN.md section H (LED driver, CEC with vivid,
|
||||
BIOS dry-run, boot into desktop/gaming and reboot, power-off module): guest-side, so they run on a runner too;
|
||||
needs a way to check without screenshots (gaming mode doesn't render)
|
||||
- [ ] 9. `vm-install` skill + `vmtest.sh` header: mention the workflow, and that the advertised-loader check fails CI when a loader has no test
|
||||
- [x] 10. TESTPLAN.md: section B for the boot loader test (B1-B8), pointing at `scripts/vmtest.sh`
|
||||
|
||||
## Full automated test: everything in TESTPLAN.md that needs no person
|
||||
|
||||
One command, `scripts/vmtest.sh [suite...]` (suites: `boot`, `cli`, `menu`, `hw`, `installer`; default all),
|
||||
each check prints `PASS`/`FAIL`, the summary counts them, exit status = failures. Each block starts from a
|
||||
fresh `ssh-ready` (`scripts/vmreset.sh --fremont`), as TESTPLAN.md says. GitHub runs the same command.
|
||||
Guest-side checks live in `share/vmtest/<suite>/`, one file per TESTPLAN block. Adding a row to
|
||||
TESTPLAN.md means adding its check here (say which row each check covers in a comment: `# F4`).
|
||||
|
||||
Automatable (encode as checks): R1.1-R1.6, R1.7 (`vmcec.sh` already prints PASS/FAIL), R1.8 (toggle matrix),
|
||||
R1.9, R1.10, R2.1, G1-G3, F1-F7, H1, H3, H4 (session file), H5, and the boot loader matrix (done).
|
||||
Not automatable without a person (stay manual, listed in the summary as SKIP; no screenshot testing): U1-U9,
|
||||
R2.2 (first desktop login look), H-Real (real Steam Machine).
|
||||
|
||||
- [x] S1. Framework (vmtest.sh runs boot + every suite, --screen, summary in ~/vms/last-test.txt): `scripts/vmtest.sh` takes suites, resets the VM per block, tallies PASS/FAIL/SKIP,
|
||||
prints the skipped manual rows; move the loader matrix under suite `boot`
|
||||
- [x] S2. Suite `cli` (55 checks pass): G1-G3, F1-F7 (exit codes, `features.state`, boot-desktop unit)
|
||||
- [x] S3. Suite `menu` (36 checks pass): R1.1-R1.6, R1.10 with scripted menu input (`vmrun.sh`) and `vmstate.sh` asserts
|
||||
- [x] S4. Suite `hw` (70 checks pass incl. H3 BIOS dry-run): H1, H3 (`WIZARD_BIOS_DRY_RUN=1`), H4, H5, R1.7 (`vmcec.sh --no-sleep`)
|
||||
- [x] S5. Suite `installer` (4 pass): R2.1 (`vminstallsim.sh`)
|
||||
- [x] S6. R1.8 toggle matrix as suite `toggles` (42 pass, 1 expected skip); R1.9 reboot checks still to add
|
||||
- [ ] S7. TESTPLAN.md: mark which rows are automated (a column or a tag), keep the results log automatic:
|
||||
the script appends a line to it (date, branch, suites, counts)
|
||||
- [ ] S8. Update the `steam-machine-testing` and `cachyos-vm-testing` skills: "run `scripts/vmtest.sh`" first
|
||||
|
||||
## Speed: the full run takes about an hour (target 20-25 min)
|
||||
|
||||
Where the time goes: boot loader VMs ~3 min each, every suite block restores a snapshot and reboots
|
||||
(~1 min), and every block that applies Steamify downloads Steam and the rest again from the mirrors.
|
||||
|
||||
- [x] P1 (proven 2026-09-29: full run in 18 min on the PC). Package cache for the suite VM: `VM_CACHE` shared as 9p and bound over `/var/cache/pacman/pkg`
|
||||
in `vmreset.sh`/the prelude (as vminstall-live.sh does); downloads only once
|
||||
- [x] P2 (base image + overlays, own port per suite, MAX_PARALLEL=3, VM_MEM=4G; first complete parallel run 2026-09-29: 328 pass, 18 min). Parallel: each suite (and each boot loader) on its own copy of the VM (`cp --reflink` of the disk and vars),
|
||||
own `VM_PORT` and `VM_DIR`; `vmtest.sh` starts them together and waits; watch host CPU (16 cores, 6 vCPUs each)
|
||||
- [ ] P3. Fewer resets: merge blocks that only read state (cli 10 + the menu R1.1)
|
||||
|
||||
## Run the tests on the PC (9800X3D, 64 GB) from a laptop
|
||||
|
||||
- [ ] R1. `scripts/vmtest-remote.sh <ssh-host>` written; try it once: the PC needs the three repos, qemu + OVMF + screen,
|
||||
KVM (on Windows: WSL2 with nested virtualization), the VMs in `~/vms` (`vmtest.sh --install` builds the boot loader
|
||||
VMs, `vminstall.sh --iso <CachyOS ISO>` with `VM_STEAMIFY=skip` the plain one), and the Steamify ISO
|
||||
- [x] R1b (done 2026-09-29: full vmtest.sh run there, 328 pass in 18 min; setup in the wsl-build-host skill). The PC is WSL2 (Windows). To check there: `/dev/kvm` exists (Windows 11, virtualization on in the BIOS,
|
||||
nested virtualization on); `%UserProfile%\.wslconfig` `memory=` is raised (WSL2 defaults to half the RAM = 32 GB;
|
||||
3-4 VMs at 4-8 GB want ~48 GB) and `processors=16`; keep `~/vms` and the repos on WSL's own ext4, not under `/mnt/c`;
|
||||
the distro is Arch (root): `pacman -S qemu-full edk2-ovmf screen`; automated runs are headless with no Konsole
|
||||
(`view_start` returns early; watch with `screen -r vmtest` or `tail -f ~/vms/test.log`), a VM for the user gets a
|
||||
window through WSLg (see the wsl-build-host skill). Still to do: a first full `vmtest.sh` run there
|
||||
- [x] R1c. `vminstall.sh` without udisks (WSL): `losetup -P` + `mount` when `udisksctl` is missing and it runs as root
|
||||
- [ ] R2. With 64 GB and 8c/16t: try `MAX_PARALLEL=4` and 8 GB VMs (`VM_MEM=8G`)
|
||||
|
||||
## Found 2026-09-29 (first full run on the PC)
|
||||
|
||||
- [x] B1 check (share/bootloader-test/boot-check.sh, after every reboot; passes on all three): the loader's own EFI boot entry exists, points to a real partition (not `HD(0,GPT,0000...)`)
|
||||
and was used (`efibootmgr` `BootCurrent`); systemd-boot booted through the fallback path unnoticed
|
||||
- [x] ISO: mount `@log` at `$ROOT/var/log` before `steamify-install` writes its logs (PR #3 merged)
|
||||
- [x] Steamify 2.9.1 (PR #53 into release/2.9.1; hw block 50 tests it): the CEC driver comes from one download (raw.githubusercontent.com, no retry, no fallback);
|
||||
a GitHub rate limit (HTTP 429, seen 2026-09-29 after a day of test installs) leaves CEC off. Ship the
|
||||
pinned source with Steamify instead (steamify-cachyos, via a release branch). Done as a cache in
|
||||
/var/cache/steamify (named after its checksum); the VMs share the host's (~/vms/pkg-cache/steamify).
|
||||
Later maybe: ship the file itself (the source repo is an unofficial mirror)
|
||||
- [x] ISO: systemd-boot's EFI boot entry (efibootmgr from the live system), PR #2 merged
|
||||
- [x] vminstall.sh: a poweroff that drops ssh is no failure, wait only for its own VM, ISO loop device under
|
||||
a lock, never root as the guest user
|
||||
|
||||
## Boot loader test (local)
|
||||
|
||||
- [x] `scripts/vmtest.sh [--install] [loader...]` runs all advertised loaders, `vmbootloadertest.sh` one
|
||||
- [x] guest checks in `share/bootloader-test/` (state, modules, legacy param, kernel update, boot check)
|
||||
- [x] first full run of all three through `vmtest.sh`: Limine 41 / systemd-boot 40 / GRUB 40, 0 failed
|
||||
- [ ] test the `VM_CACHE` speed-up with a second install
|
||||
@@ -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
@@ -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" }
|
||||
@@ -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
|
||||
@@ -1,11 +1,19 @@
|
||||
#!/bin/bash
|
||||
# CachyOS test VM for steamify.sh.
|
||||
# [REPO=/path/to/cachyos-gamescope-boot] ./run.sh [install] [--nvidia] [--vulkan] [--fremont]
|
||||
# [REPO=/path/to/cachyos-gamescope-boot] ./run.sh [install] [--nvidia] [--vulkan] [--amd] [--fremont] [--headless]
|
||||
# install boot the installer ISO
|
||||
# --nvidia render the guest's virtio-gpu (virgl) on the host NVIDIA dGPU
|
||||
# --vulkan expose Vulkan to the guest (venus; needed by gamescope, can be unstable)
|
||||
# --amd with --vulkan: venus on the host AMD iGPU (RADV) instead of NVIDIA
|
||||
# --fremont report the Valve Steam Machine's DMI data (for testing the wizard)
|
||||
# --headless no window (unattended tests, CI); a VM you start by hand has one. VM_HEADLESS=1 does the same
|
||||
# REPO defaults to $HOME/projects/cachyos-gamescope-boot.
|
||||
# VM_ISO=<path> boots that ISO for `install` instead of cachyos.iso; with
|
||||
# VM_KERNEL/VM_INITRD/VM_APPEND its kernel is booted directly with those
|
||||
# parameters (scripts/vminstall.sh, the unattended install).
|
||||
# VM_MEM=<size>: guest RAM (8G; the automated tests use 4G so three VMs fit next to a desktop).
|
||||
# VM_CPUS=<n>: guest CPUs (6); ISO builds use ~75% of host cores (nproc * 3 / 4), so the host stays usable.
|
||||
# VM_PORT=<n>: the host port for the guest's SSH (2222), e.g. a second VM.
|
||||
# BIOS_VERSION=F7F0107 makes the guest report that BIOS version (DMI), e.g.
|
||||
# to test the wizard's BIOS update item; the firmware itself doesn't change.
|
||||
# The repo is shared into the guest; mount it there with:
|
||||
@@ -14,39 +22,66 @@
|
||||
# sudo mount -t 9p -o trans=virtio,version=9p2000.L vmtools /media && /media/guest-ssh-setup.sh
|
||||
# SSH from the host: ssh -p 2222 <user>@localhost
|
||||
cd "$(dirname "$0")"
|
||||
# REPO: from the environment, else what scripts/vminstall.sh recorded next
|
||||
# to the disk (repo-path), else the old default.
|
||||
[[ -z "${REPO:-}" && -f repo-path ]] && REPO="$(cat repo-path)"
|
||||
repo="${REPO:-$HOME/projects/cachyos-gamescope-boot}"
|
||||
|
||||
cdrom=()
|
||||
direct=()
|
||||
[[ -n "${VM_KERNEL:-}" ]] && direct=(-kernel "$VM_KERNEL" -initrd "$VM_INITRD" -append "$VM_APPEND")
|
||||
# VM_CACHE=<host dir>: shared read-write as 9p tag `cache` (the ISO build's
|
||||
# package cache, scripts/vmisobuild.sh; one per VM, e.g. $VM_DIR/iso-cache),
|
||||
# so snapshot resets don't lose it.
|
||||
[[ -n "${VM_CACHE:-}" ]] && { mkdir -p "$VM_CACHE"; direct+=(-virtfs "local,path=$VM_CACHE,mount_tag=cache,security_model=mapped-xattr"); }
|
||||
# VM_SERIAL=<file>: the guest's serial console (console=ttyS0) logged into
|
||||
# that file, and reachable as a socket next to it (<file>.sock) to type into.
|
||||
[[ -n "${VM_SERIAL:-}" ]] && direct+=(-chardev "socket,id=ser0,path=$VM_SERIAL.sock,server=on,wait=off,logfile=$VM_SERIAL" -serial chardev:ser0)
|
||||
smbios=()
|
||||
gpu=virtio-vga-gl,xres=1920,yres=1080
|
||||
gpu=virtio-vga-gl,xres=1920,yres=1080 gl=on
|
||||
# VM_NOGL=1: no virgl (software rendering in the guest), so QMP screendump
|
||||
# works (scripts/qmpshot.py); for the live ISO and Calamares.
|
||||
[[ -n "${VM_NOGL:-}" ]] && { gpu=virtio-vga,xres=1920,yres=1080; gl=off; }
|
||||
[[ -n "${BIOS_VERSION:-}" ]] && smbios+=(-smbios "type=0,version=$BIOS_VERSION")
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
install) cdrom=(-cdrom cachyos.iso -boot d) ;;
|
||||
install) cdrom=(-cdrom "${VM_ISO:-cachyos.iso}" -boot d) ;;
|
||||
--nvidia) export __NV_PRIME_RENDER_OFFLOAD=1 __GLX_VENDOR_LIBRARY_NAME=nvidia \
|
||||
__EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/10_nvidia.json ;;
|
||||
--vulkan) gpu+=,hostmem=4G,blob=true,venus=true ;;
|
||||
--amd) export VK_DRIVER_FILES=/usr/share/vulkan/icd.d/radeon_icd.json ;;
|
||||
--headless) VM_HEADLESS=1 ;;
|
||||
--fremont) smbios+=(-smbios type=1,manufacturer=Valve,product=Fremont -smbios type=2,manufacturer=Valve,product=Fremont) ;;
|
||||
*) echo "unknown argument: $arg" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# VM_HEADLESS=1: no window (unattended tests, CI): plain virtio-vga and -display none;
|
||||
# QMP screendump works then too. The guest and ssh don't notice.
|
||||
display=(-display "gtk,gl=$gl,zoom-to-fit=off")
|
||||
[[ -n "${VM_HEADLESS:-}" ]] && { gpu=virtio-vga,xres=1920,yres=1080; display=(-display none); }
|
||||
|
||||
# UEFI firmware: Ubuntu's path, or Arch's (edk2-ovmf).
|
||||
ovmf=/usr/share/OVMF; ovmf_code=OVMF_CODE_4M.fd; ovmf_vars=OVMF_VARS_4M.fd
|
||||
[[ -f "$ovmf/$ovmf_code" ]] || { ovmf=/usr/share/edk2/x64; ovmf_code=OVMF_CODE.4m.fd; ovmf_vars=OVMF_VARS.4m.fd; }
|
||||
|
||||
# First run: create the disk and the writable UEFI variable store.
|
||||
[[ -f disk.qcow2 ]] || qemu-img create -f qcow2 disk.qcow2 60G
|
||||
[[ -f vars.fd ]] || cp /usr/share/OVMF/OVMF_VARS_4M.fd vars.fd
|
||||
[[ -f vars.fd ]] || cp "$ovmf/$ovmf_vars" vars.fd
|
||||
|
||||
# Hand the host's public keys to the guest setup script.
|
||||
cat ~/.ssh/*.pub > share/host-keys.pub
|
||||
|
||||
exec qemu-system-x86_64 \
|
||||
-enable-kvm -machine q35,memory-backend=mem -cpu host -smp 6 -m 8G \
|
||||
-object memory-backend-memfd,id=mem,size=8G,share=on \
|
||||
-drive if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE_4M.fd \
|
||||
-enable-kvm -machine q35,memory-backend=mem -cpu host -smp "${VM_CPUS:-6}" -m "${VM_MEM:-8G}" \
|
||||
-object memory-backend-memfd,id=mem,size=${VM_MEM:-8G},share=on \
|
||||
-drive if=pflash,format=raw,readonly=on,file="$ovmf/$ovmf_code" \
|
||||
-drive if=pflash,format=raw,file=vars.fd \
|
||||
-drive file=disk.qcow2,if=virtio \
|
||||
-device "$gpu" -display gtk,gl=on,zoom-to-fit=off \
|
||||
-device "$gpu" "${display[@]}" \
|
||||
-device qemu-xhci -device usb-tablet \
|
||||
-nic user,model=virtio-net-pci,hostfwd=tcp::2222-:22 \
|
||||
-nic user,model=virtio-net-pci,hostfwd=tcp::${VM_PORT:-2222}-:22 \
|
||||
-virtfs local,path="$repo",mount_tag=repo,security_model=mapped-xattr \
|
||||
-virtfs local,path="$PWD/share",mount_tag=vmtools,security_model=mapped-xattr,readonly=on \
|
||||
"${smbios[@]}" "${cdrom[@]}"
|
||||
-qmp unix:"$PWD/qmp.sock",server=on,wait=off \
|
||||
"${smbios[@]}" "${cdrom[@]}" "${direct[@]}"
|
||||
+28
-2
@@ -1,5 +1,31 @@
|
||||
# Sourced by the other scripts: SSH target of the test VM.
|
||||
VM_USER="${VM_USER:-theupriser}"
|
||||
VM_PORT="${VM_PORT:-2222}"
|
||||
VM_HOST="${VM_HOST:-localhost}"
|
||||
vm_ssh() { ssh -p "$VM_PORT" -o BatchMode=yes "$VM_USER@$VM_HOST" "$@"; }
|
||||
# Where disk.qcow2, vars*.fd and run.sh live: this repo if it has a disk,
|
||||
# else ~/vms/cachyos-test.
|
||||
_repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
if [[ -z "${VM_DIR:-}" ]]; then
|
||||
if [[ -f "$_repo/disk.qcow2" ]]; then VM_DIR="$_repo"; else VM_DIR="$HOME/vms/cachyos-test"; fi
|
||||
fi
|
||||
# The guest user: what scripts/vminstall.sh created ($VM_DIR/vm-user), else
|
||||
# theupriser (VMs installed by hand).
|
||||
if [[ -z "${VM_USER:-}" && -f "$VM_DIR/vm-user" ]]; then VM_USER="$(cat "$VM_DIR/vm-user")"; fi
|
||||
VM_USER="${VM_USER:-theupriser}"
|
||||
# scripts/vminstall.sh records the key it authorized in $VM_DIR/ssh-key.
|
||||
# The VM's host key lives next to its disk, never in ~/.ssh/known_hosts: a
|
||||
# reinstalled VM gets a new one on the same localhost port.
|
||||
VM_SSH_OPTS=(-o BatchMode=yes -o "UserKnownHostsFile=$VM_DIR/known_hosts" -o StrictHostKeyChecking=accept-new)
|
||||
if [[ -z "${VM_SSH_KEY:-}" && -f "$VM_DIR/ssh-key" ]]; then VM_SSH_KEY="$(cat "$VM_DIR/ssh-key")"; fi
|
||||
[[ -n "${VM_SSH_KEY:-}" ]] && VM_SSH_OPTS+=(-i "$VM_SSH_KEY" -o IdentitiesOnly=yes)
|
||||
vm_ssh() { ssh -p "$VM_PORT" "${VM_SSH_OPTS[@]}" "$VM_USER@$VM_HOST" "$@"; }
|
||||
vm_scp() { scp -q -P "$VM_PORT" "${VM_SSH_OPTS[@]}" "$@"; }
|
||||
|
||||
# One Konsole for every test run: it follows $TEST_LOG, which the test scripts append to.
|
||||
# Started only when there is a desktop, the run is headless and no such window exists.
|
||||
TEST_LOG="${TEST_LOG:-$HOME/vms/test.log}"
|
||||
view_start() {
|
||||
[[ -n "${VM_HEADLESS:-}" && -z "${CI:-}" && -n "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ]] && command -v konsole >/dev/null || return 0
|
||||
systemctl --user is-active --quiet vmtest-view 2>/dev/null && return 0
|
||||
mkdir -p "$(dirname "$TEST_LOG")"; touch "$TEST_LOG"
|
||||
systemd-run --user -q --collect --unit=vmtest-view konsole --hold -e tail -n 60 -F "$TEST_LOG" >/dev/null 2>&1 || true
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Executable
+13
@@ -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."
|
||||
Executable
+121
@@ -0,0 +1,121 @@
|
||||
#!/bin/bash
|
||||
# Does a boot loader work with Steamify's kernel side (the Steam Machine's DKMS
|
||||
# modules)? On a VM that reports Fremont hardware: applies Steamify's options,
|
||||
# checks the modules for every kernel, Steamify's boot-loader code (removal of
|
||||
# the old drm.edid_firmware parameter), a kernel update, and a reboot into
|
||||
# each of the two kernels. Guest checks: share/bootloader-test/*.sh.
|
||||
# All loaders in one go: scripts/vmtest.sh.
|
||||
# scripts/vmbootloadertest.sh <limine|systemd-boot|grub> [--install] [--window]
|
||||
# --install first install the VM (scripts/vminstall.sh, from the Steamify ISO)
|
||||
# --window show the VM's window (default: headless, with a Konsole on the log when there is a desktop)
|
||||
# Env: VM_DIR (default ~/vms/bl-<loader>), VM_ISO (default: the newest ISO in
|
||||
# ../steamify-cachyos-live-iso/out/desktop), REPO (steamify-cachyos, 9p `repo`).
|
||||
# Exit status: the number of failed checks. Only one VM runs at a time (port 2222).
|
||||
set -uo pipefail
|
||||
here="$(cd "$(dirname "$0")" && pwd)"
|
||||
repo="$(cd "$here/.." && pwd)"
|
||||
loader="${1:?usage: $0 <limine|systemd-boot|grub> [--install]}"
|
||||
install=false; window=false
|
||||
for a in "${@:2}"; do
|
||||
case "$a" in --install) install=true ;; --window) window=true ;; *) echo "Unknown argument: $a" >&2; exit 2 ;; esac
|
||||
done
|
||||
# No human is needed: headless (no VM window) unless --window. On a desktop a Konsole follows the log.
|
||||
$window || export VM_HEADLESS=1
|
||||
case "$loader" in limine|systemd-boot|grub) ;; *) echo "Unknown boot loader: $loader" >&2; exit 2 ;; esac
|
||||
export VM_DIR="${VM_DIR:-$HOME/vms/bl-$loader}"
|
||||
# Own ssh port per loader, so the loaders can run side by side (vmtest.sh).
|
||||
case "$loader" in limine) export VM_PORT="${VM_PORT:-2401}" ;; systemd-boot) export VM_PORT="${VM_PORT:-2402}" ;; grub) export VM_PORT="${VM_PORT:-2403}" ;; esac
|
||||
. "$here/common.sh"
|
||||
guest="$repo/share/bootloader-test"
|
||||
log="$VM_DIR.test.log"
|
||||
|
||||
step() { printf '\n##### %s %s\n' "$(date +%T)" "$*"; }
|
||||
gssh() { vm_ssh -o ConnectTimeout=6 -o LogLevel=ERROR "$@"; }
|
||||
gscript() { gssh env "LOADER=$loader" bash -s < "$guest/$1"; } # the guest's login shell is fish
|
||||
waitssh() { local _; for _ in $(seq 60); do gssh 'uptime -p' 2>/dev/null | grep -q '^up' && return 0; sleep 5; done; echo "FAIL no SSH"; return 1; }
|
||||
running() { pgrep -f "[h]ostfwd=tcp::$VM_PORT-" >/dev/null; }
|
||||
stop_vm() {
|
||||
running || return 0
|
||||
# Whichever VM runs: its key and host key are not this VM's.
|
||||
ssh -p "$VM_PORT" -i "$VM_SSH_KEY" -o BatchMode=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o LogLevel=ERROR \
|
||||
"$VM_USER@$VM_HOST" 'sudo systemctl poweroff' >/dev/null 2>&1
|
||||
local _; for _ in $(seq 30); do running || return 0; sleep 2; done
|
||||
pkill -f "[h]ostfwd=tcp::$VM_PORT-"; sleep 2
|
||||
}
|
||||
export VM_CACHE="${VM_CACHE-$HOME/vms/pkg-cache}" # 9p tag `cache`: Steamify's downloads once for every VM
|
||||
start_vm() { stop_vm; cp "$repo/run.sh" "$VM_DIR/run.sh"; (cd "$VM_DIR" && VM_MEM="${VM_MEM:-4G}" REPO="${REPO:-$repo/../steamify-cachyos}" setsid nohup ./run.sh --fremont > vm.log 2>&1 &); }
|
||||
reboot_vm() { gssh 'sudo systemctl reboot' >/dev/null 2>&1; sleep 25; waitssh; }
|
||||
view_start # one shared Konsole (common.sh)
|
||||
# The guest checks print PASS/FAIL lines; the summary at the end counts them in the log.
|
||||
check() { cat; }
|
||||
|
||||
{
|
||||
printf '\n===== vmbootloadertest.sh %s (%s) =====\n' "$loader" "$(date +%T)"
|
||||
if $install; then
|
||||
step "install $loader VM in $VM_DIR"
|
||||
stop_vm
|
||||
iso="${VM_ISO:-$(ls -t "$repo"/../steamify-cachyos-live-iso/out/desktop/*.iso 2>/dev/null | head -n 1)}"
|
||||
[[ -f "$iso" ]] || { echo "No ISO: set VM_ISO"; exit 2; }
|
||||
if [[ -f "$VM_DIR/disk.qcow2" ]]; then
|
||||
[[ -t 0 ]] || { echo "$VM_DIR has a disk; run this from a terminal to replace it"; exit 2; }
|
||||
read -r -p "Replace $VM_DIR/disk.qcow2? Type YES: " a
|
||||
[[ "$a" == YES ]] || exit 1
|
||||
rm -f "$VM_DIR/disk.qcow2"
|
||||
fi
|
||||
VM_BOOTLOADER="$loader" "$here/vminstall.sh" --iso "$iso" < /dev/null || exit 1
|
||||
fi
|
||||
|
||||
step "start $loader as a Steam Machine"
|
||||
start_vm; waitssh || exit 1
|
||||
gssh 'sudo cat /sys/class/dmi/id/product_name' 2>/dev/null
|
||||
|
||||
step "Steamify's Steam Machine options"
|
||||
gssh 'sudo mkdir -p /var/cache/steamify-pkg /var/cache/steamify; sudo mountpoint -q /var/cache/steamify-pkg || sudo mount -t 9p -o trans=virtio,version=9p2000.L cache /var/cache/steamify-pkg && { sudo mkdir -p /var/cache/steamify-pkg/steamify; sudo mount --bind /var/cache/steamify-pkg/steamify /var/cache/steamify; }' 2>/dev/null
|
||||
gssh 'sudo mountpoint -q /mnt || sudo mount -t 9p -o trans=virtio,version=9p2000.L repo /mnt; cd /mnt && nohup bash steamify.sh --defaults --options gaming,theme,glyphs,single,launcher,notify,vram,cec,machine,poweroff --boot gamescope > /tmp/steamify.log 2>&1 < /dev/null &'
|
||||
sleep 20
|
||||
# [s]: the pattern must not match this ssh command itself.
|
||||
for _ in $(seq 90); do gssh 'pgrep -f "[s]teamify.sh --defaults" >/dev/null && echo R || echo D' 2>/dev/null | grep -q D && break; sleep 10; done
|
||||
gssh 'sed "s/\x1b\[[0-9;]*m//g" /tmp/steamify.log | grep -E "^\[(WARN|ERROR)\]" | cut -c1-140'
|
||||
gssh 'tail -n 1 /tmp/steamify.log | sed "s/\x1b\[[0-9;]*m//g"' | grep -q '^\[OK\] Done' && echo "PASS Steamify finished" || { echo "FAIL Steamify did not finish"; }
|
||||
|
||||
step "Steamify's state, OS name, boot loader"; gscript steamify-state.sh | check
|
||||
step "Steamify's modules"; gscript modules.sh | check
|
||||
step "boot loader code (legacy kernel parameter removal)"; gscript legacy-param.sh | check
|
||||
step "kernel update"; gscript kernel-update.sh | check
|
||||
|
||||
step "reboot into the default kernel"
|
||||
if [[ "$loader" == limine ]]; then
|
||||
# remember_last_entry: yes boots whatever ran last, not default_entry: pin the default for this test, restore it at the end.
|
||||
cur="$(gssh "sudo sed -n 's/^default_entry: *//p' /boot/limine.conf")"
|
||||
rem="$(gssh "sudo sed -n 's/^remember_last_entry: *//p' /boot/limine.conf")"
|
||||
gssh "sudo sed -i -e 's/^default_entry: .*/default_entry: $cur/' -e 's/^remember_last_entry: .*/remember_last_entry: no/' /boot/limine.conf"
|
||||
fi
|
||||
reboot_vm && gscript boot-check.sh | check
|
||||
a="$(gssh uname -r)"
|
||||
|
||||
step "reboot into the other kernel"
|
||||
case "$loader" in
|
||||
systemd-boot) # the default entry is linux-cachyos.conf (vminstall-post.sh)
|
||||
gssh 'sudo bootctl set-oneshot linux-cachyos-lts.conf'; gssh 'sudo systemctl reboot' >/dev/null 2>&1; sleep 25; waitssh ;;
|
||||
grub) # GRUB_DEFAULT = the other kernel in the "Advanced options" submenu (0 = lts, 1 = linux-cachyos), no key presses
|
||||
sub=$([[ "$a" == *lts* ]] && echo 1 || echo 0)
|
||||
gg="$(gssh "sudo sed -n 's/^GRUB_DEFAULT=//p' /etc/default/grub")"
|
||||
gssh "sudo sed -i 's/^GRUB_DEFAULT=.*/GRUB_DEFAULT=\"1>$sub\"/' /etc/default/grub && sudo grub-mkconfig -o /boot/grub/grub.cfg >/dev/null 2>&1"
|
||||
reboot_vm ;;
|
||||
limine) # the next entry in limine.conf; remember_last_entry would override default_entry
|
||||
gssh "sudo sed -i -e 's/^default_entry: .*/default_entry: $((cur + 1))/' /boot/limine.conf"
|
||||
reboot_vm ;;
|
||||
esac
|
||||
gscript boot-check.sh | check
|
||||
b="$(gssh uname -r)"
|
||||
[[ -n "$a" && -n "$b" && "$a" != "$b" ]] && echo "PASS booted both kernels ($a, $b)" || echo "FAIL both boots ran the same kernel ($a, $b)"
|
||||
[[ "$loader" == grub ]] && gssh "sudo sed -i 's/^GRUB_DEFAULT=.*/GRUB_DEFAULT=$gg/' /etc/default/grub && sudo grub-mkconfig -o /boot/grub/grub.cfg >/dev/null 2>&1"
|
||||
[[ "$loader" == limine ]] && gssh "sudo sed -i -e 's/^default_entry: .*/default_entry: $cur/' -e 's/^remember_last_entry: .*/remember_last_entry: ${rem:-yes}/' /boot/limine.conf"
|
||||
|
||||
step "DONE"
|
||||
stop_vm
|
||||
} 2>&1 | tee "$log" | tee >(sed -u "s/^/[$loader] /" >> "$TEST_LOG") | sed 's/\x1b\[[0-9;]*m//g'
|
||||
fails="$(grep -c '^FAIL' "$log")"
|
||||
echo; echo "== $loader: $(grep -c '^PASS' "$log") passed, $fails failed (log: $log)"
|
||||
grep '^FAIL' "$log"
|
||||
exit "$fails"
|
||||
Executable
+230
@@ -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
|
||||
@@ -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()
|
||||
Executable
+208
@@ -0,0 +1,208 @@
|
||||
#!/bin/bash
|
||||
# Install a fresh, ssh-ready test VM unattended: CachyOS (KDE Plasma,
|
||||
# plasma-login-manager, btrfs, Limine, linux-cachyos + -lts) from the ISO's
|
||||
# headless installer, then sshd with your key, passwordless sudo and the test
|
||||
# autologin into Plasma. Ends with the snapshots `clean` and `ssh-ready`.
|
||||
# scripts/vminstall.sh [--force] [--iso <file>] [run.sh flags...] e.g. --fremont
|
||||
# --force replace an existing disk.qcow2 in $VM_DIR (asks first)
|
||||
# --iso <file> the live ISO (default $VM_DIR/cachyos.iso, e.g. a
|
||||
# Steamify CachyOS build later; it must be archiso-based and
|
||||
# have cachyos-installer)
|
||||
# Env: VM_DIR (see common.sh), VM_USER (default: your host username; theupriser when that is root),
|
||||
# VM_PASSWORD (steamify), VM_SSH_KEY (private key to authorize; default
|
||||
# ~/.ssh/steamify-vm_ed25519, asked once when that doesn't exist), REPO (shared as 9p `repo`, default ../steamify-cachyos),
|
||||
# VM_CACHE (host dir for the packages, default ~/vms/pkg-cache; empty = none),
|
||||
# VM_STEAMIFY (a Steamify ISO also runs steamify-install: the Steamify page's ids,
|
||||
# empty = defaults, skip = plain CachyOS), VM_BOOTLOADER (limine; or systemd-boot, grub), VM_TIMEZONE (the host's), VM_HOST_IP (the host as the guest sees it,
|
||||
# 10.0.2.2 with QEMU's user networking).
|
||||
# The VM's user and key are recorded in $VM_DIR (vm-user, ssh-key) for the
|
||||
# other scripts. Never touches your existing SSH keys or ~/.ssh/known_hosts.
|
||||
set -euo pipefail
|
||||
here="$(cd "$(dirname "$0")" && pwd)"
|
||||
repo="$(cd "$here/.." && pwd)"
|
||||
[[ -n "${VM_USER:-}" ]] || VM_USER="$(id -un)"
|
||||
[[ "$VM_USER" != root ]] || VM_USER=theupriser # root can't be the guest user (WSL runs as root)
|
||||
export VM_USER
|
||||
. "$here/common.sh"
|
||||
# The pacman package cache, on the host and shared by every VM you install
|
||||
# (9p tag `cache`, see run.sh): a second install downloads nothing. VM_CACHE= (empty) turns it off.
|
||||
export VM_CACHE="${VM_CACHE-$HOME/vms/pkg-cache}"
|
||||
|
||||
force=false iso="" run_flags=()
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--force) force=true; shift ;;
|
||||
--iso) iso="${2:?--iso needs a file}"; shift 2 ;;
|
||||
*) run_flags+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
password="${VM_PASSWORD:-steamify}"
|
||||
timezone="${VM_TIMEZONE:-$(timedatectl show -p Timezone --value 2>/dev/null || cat /etc/timezone 2>/dev/null || echo UTC)}"
|
||||
hostname=steamify-vm
|
||||
REPO="${REPO:-$(dirname "$repo")/steamify-cachyos}"
|
||||
[[ -d "$REPO" ]] || { echo "REPO $REPO doesn't exist (the Steamify checkout shared into the VM); set REPO=." >&2; exit 1; }
|
||||
export REPO
|
||||
|
||||
mkdir -p "$VM_DIR"
|
||||
iso="${iso:-$VM_DIR/cachyos.iso}"
|
||||
[[ -f "$iso" ]] || { echo "No ISO at $iso: run get-iso.sh (in $VM_DIR) or pass --iso <file>." >&2; exit 1; }
|
||||
iso="$(cd "$(dirname "$iso")" && pwd)/$(basename "$iso")"
|
||||
pgrep -f "[h]ostfwd=tcp::$VM_PORT-" >/dev/null && { echo "A VM is running on port $VM_PORT; power it off first." >&2; exit 1; }
|
||||
if [[ -f "$VM_DIR/disk.qcow2" ]]; then
|
||||
[[ "$force" == true ]] || { echo "$VM_DIR already has a disk.qcow2; use --force to replace it, or another VM_DIR." >&2; exit 1; }
|
||||
read -r -p "Replace $VM_DIR/disk.qcow2 and all its snapshots? Type YES: " answer < /dev/tty
|
||||
[[ "$answer" == YES ]] || { echo "Nothing changed."; exit 1; }
|
||||
fi
|
||||
|
||||
# --- The SSH key to authorize: a new one just for the VM, or one of yours.
|
||||
# The VM's own key is made once and then always used: asked only when it
|
||||
# doesn't exist yet.
|
||||
dedicated="$HOME/.ssh/steamify-vm_ed25519"
|
||||
[[ -z "${VM_SSH_KEY:-}" && -f "$dedicated" ]] && VM_SSH_KEY="$dedicated"
|
||||
if [[ -z "${VM_SSH_KEY:-}" ]]; then
|
||||
mapfile -t pubs < <(ls "$HOME"/.ssh/*.pub 2>/dev/null | grep -vxF "$dedicated.pub")
|
||||
echo "Which SSH key should log in to the VM?"
|
||||
echo " 1) a new key just for the VM, used from now on: $dedicated (no passphrase)"
|
||||
for i in "${!pubs[@]}"; do echo " $((i + 2))) your key ${pubs[$i]%.pub}"; done
|
||||
read -r -p "Choice [1]: " choice < /dev/tty
|
||||
choice="${choice:-1}"
|
||||
if [[ "$choice" == 1 ]]; then
|
||||
VM_SSH_KEY="$dedicated"
|
||||
# Never overwrite a key: it doesn't exist here.
|
||||
ssh-keygen -q -t ed25519 -N "" -C "steamify-vm" -f "$dedicated"
|
||||
elif [[ "$choice" =~ ^[0-9]+$ && -n "${pubs[$((choice - 2))]:-}" ]]; then
|
||||
VM_SSH_KEY="${pubs[$((choice - 2))]%.pub}"
|
||||
else
|
||||
echo "Unknown choice: $choice" >&2; exit 1
|
||||
fi
|
||||
fi
|
||||
[[ -f "$VM_SSH_KEY.pub" ]] || { echo "No public key $VM_SSH_KEY.pub." >&2; exit 1; }
|
||||
pubkey="$(cat "$VM_SSH_KEY.pub")"
|
||||
|
||||
echo "VM: $VM_DIR user: $VM_USER password: $password hostname: $hostname timezone: $timezone"
|
||||
echo "ISO: $iso key: $VM_SSH_KEY"
|
||||
|
||||
# --- The ISO's kernel and initramfs, to boot it with our parameters.
|
||||
boot="$VM_DIR/iso-boot"
|
||||
mkdir -p "$boot"
|
||||
# One loop setup at a time: parallel installs (vmtest.sh --install) otherwise get "Device or resource busy".
|
||||
exec 9> /tmp/vminstall-iso.lock; flock 9
|
||||
if ! command -v udisksctl >/dev/null && [[ $EUID -eq 0 ]]; then
|
||||
# No udisks (the WSL box, as root): a plain loop device and mount.
|
||||
loop="$(losetup -f -r -P --show "$iso")"
|
||||
mnt="$(mktemp -d)"
|
||||
cleanup_loop() { umount "$mnt" 2>/dev/null || true; rmdir "$mnt" 2>/dev/null || true; losetup -d "$loop" 2>/dev/null || true; }
|
||||
trap cleanup_loop EXIT
|
||||
mount -r "${loop}p1" "$mnt" 2>/dev/null || mnt=""
|
||||
else
|
||||
loop="$(udisksctl loop-setup --no-user-interaction -r -f "$iso" | grep -o '/dev/loop[0-9]*')"
|
||||
cleanup_loop() { udisksctl unmount --no-user-interaction -b "${loop}p1" >/dev/null 2>&1 || true; udisksctl loop-delete --no-user-interaction -b "$loop" >/dev/null 2>&1 || true; }
|
||||
trap cleanup_loop EXIT
|
||||
mnt=""
|
||||
for _ in $(seq 10); do
|
||||
mnt="$(findmnt -nro TARGET "${loop}p1" 2>/dev/null || true)"
|
||||
[[ -n "$mnt" ]] && break
|
||||
udisksctl mount --no-user-interaction -b "${loop}p1" >/dev/null 2>&1 || true; sleep 1
|
||||
done
|
||||
fi
|
||||
[[ -n "$mnt" ]] || { echo "Couldn't mount the ISO." >&2; exit 1; }
|
||||
rm -f "$boot"/* # copied read-only from the ISO
|
||||
install -m 644 "$mnt/arch/boot/x86_64/vmlinuz-linux-cachyos" "$mnt/arch/boot/x86_64/initramfs-linux-cachyos.img" "$boot/"
|
||||
search="$(grep -rhoE 'archisosearchuuid=[^ ]+' "$mnt/boot" "$mnt/EFI" 2>/dev/null | head -1)"
|
||||
[[ -n "$search" ]] || { echo "No archisosearchuuid in the ISO's boot menu." >&2; exit 1; }
|
||||
cleanup_loop; trap - EXIT
|
||||
flock -u 9; exec 9>&-
|
||||
|
||||
# --- What the live system fetches from the host.
|
||||
www="$VM_DIR/install-www"
|
||||
rm -rf "$www"; mkdir -p "$www"
|
||||
# QEMU's user networking (-nic user in run.sh) always shows the host to the
|
||||
# guest at 10.0.2.2, whatever network the host is on; only a bridged/tap
|
||||
# setup would need another address.
|
||||
host_ip="${VM_HOST_IP:-10.0.2.2}"
|
||||
port="$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1])')"
|
||||
sed -e "s/@PORT@/$port/" -e "s/@HOST_IP@/$host_ip/" "$repo/share/vminstall-live.sh" > "$www/vminstall-live.sh"
|
||||
cp "$repo/share/vminstall-post.sh" "$www/"
|
||||
{
|
||||
printf 'VM_USER=%q\nVM_HOSTNAME=%q\nVM_PUBKEY=%q\nDEVICE=/dev/vda\nVM_STEAMIFY=%q\n' "$VM_USER" "$hostname" "$pubkey" "${VM_STEAMIFY:-}"
|
||||
} > "$www/vminstall.env"
|
||||
python3 - "$www/settings.json" "$VM_USER" "$password" "$timezone" "$hostname" "${VM_BOOTLOADER:-limine}" << 'PY'
|
||||
import json, sys
|
||||
out, user, pw, tz, host, bootloader = sys.argv[1:]
|
||||
json.dump({
|
||||
"install_type": "simple", "headless_mode": True,
|
||||
"device": "/dev/vda", "fs_name": "btrfs", "subvolumes": "default",
|
||||
"partitions": [
|
||||
{"name": "/dev/vda1", "mountpoint": "/boot", "size": "2G", "fs_name": "vfat", "type": "boot"},
|
||||
{"name": "/dev/vda2", "mountpoint": "/", "size": "100%", "type": "root"},
|
||||
],
|
||||
"hostname": host, "locale": "en_US.UTF-8", "xkbmap": "us", "timezone": tz,
|
||||
"user_name": user, "user_pass": pw, "user_shell": "/bin/fish", "root_pass": pw,
|
||||
"kernel": "linux-cachyos linux-cachyos-lts", "desktop": "kde", "bootloader": bootloader,
|
||||
}, open(out, "w"), indent=2)
|
||||
PY
|
||||
python3 "$here/vminstall-server.py" "$www" "$port" &
|
||||
server=$!
|
||||
trap 'kill $server 2>/dev/null || true' EXIT
|
||||
|
||||
# --- Install.
|
||||
cd "$VM_DIR"
|
||||
cp "$repo/run.sh" run.sh # always this repo's: the install needs its kernel/serial options
|
||||
[[ -e share ]] || ln -s "$repo/share" share
|
||||
rm -f disk.qcow2 vars.fd vars.clean.fd vars.ssh-ready.fd known_hosts
|
||||
qemu-img create -q -f qcow2 disk.qcow2 60G
|
||||
echo "$VM_USER" > vm-user
|
||||
echo "$REPO" > repo-path
|
||||
echo "$VM_SSH_KEY" > ssh-key
|
||||
# systemd.run= only generates kernel-command-line.service; systemd.wants=
|
||||
# starts it next to the normal boot (desktop, network). It fetches and runs
|
||||
# the live script. (Root logs in on the serial console without a password:
|
||||
# VM_SERIAL.sock, for debugging.)
|
||||
append="$search archisobasedir=arch cow_spacesize=10G module_blacklist=pcspkr console=ttyS0,115200 console=tty0"
|
||||
append+=" systemd.run=\"/usr/bin/bash -c 'curl -fsS --retry 150 --retry-all-errors --retry-delay 2 http://$host_ip:$port/vminstall-live.sh | bash'\""
|
||||
append+=" systemd.run_success_action=none systemd.run_failure_action=none systemd.wants=kernel-command-line.service"
|
||||
echo "Installing: watch the VM's window, or follow the console with tail -f $VM_DIR/serial.log (install log: $www/install.log)..."
|
||||
VM_SERIAL="$VM_DIR/serial.log" VM_ISO="$iso" VM_KERNEL="$boot/vmlinuz-linux-cachyos" VM_INITRD="$boot/initramfs-linux-cachyos.img" VM_APPEND="$append" \
|
||||
./run.sh install "${run_flags[@]}" > vm-install.log 2>&1 &
|
||||
qemu=$!
|
||||
# 90 minutes; a failure reported by the live system ends it at once (it stays up after one)
|
||||
for _ in $(seq 1080); do
|
||||
kill -0 $qemu 2>/dev/null || break
|
||||
[[ -s "$www/status" && "$(cat "$www/status")" != ok ]] && { sleep 5; kill $qemu 2>/dev/null; wait $qemu 2>/dev/null; break; }
|
||||
sleep 5
|
||||
done
|
||||
kill -0 $qemu 2>/dev/null && { echo "The install didn't finish in 90 minutes; see $www/install.log." >&2; kill $qemu; exit 1; }
|
||||
status="$(cat "$www/status" 2>/dev/null || echo "no status (the live system never reported back)")"
|
||||
cp "$www/install.log" install.log 2>/dev/null || true
|
||||
[[ "$status" == ok ]] || { echo "Install failed: $status. Log: $VM_DIR/install.log" >&2; exit 1; }
|
||||
qemu-img snapshot -c clean disk.qcow2 && cp vars.fd vars.clean.fd
|
||||
echo "Installed; snapshot 'clean' taken."
|
||||
|
||||
# --- First boot: check it's ssh-ready, then snapshot.
|
||||
nohup ./run.sh "${run_flags[@]}" > vm.log 2>&1 &
|
||||
vm=$!
|
||||
for _ in $(seq 100); do vm_ssh -o ConnectTimeout=3 true 2>/dev/null && break; sleep 3; done
|
||||
vm_ssh true || { echo "No SSH after the first boot; see $VM_DIR/vm.log." >&2; exit 1; }
|
||||
# The poweroff at the end may drop the connection (ssh exits 255): judge by the ssh-ready line instead.
|
||||
out="$(vm_ssh bash -s 2>&1 << 'REMOTE'
|
||||
set -e
|
||||
if [[ -f /etc/sddm.conf.d/10-gamescope-autologin.conf ]]; then
|
||||
# Steamify's gaming mode (gamescope, no Plasma, and it doesn't render in the VM): the login manager has to be up.
|
||||
for _ in $(seq 60); do systemctl is-active --quiet display-manager && break; sleep 3; done
|
||||
systemctl is-active --quiet display-manager || { echo "The login manager didn't start." >&2; exit 1; }
|
||||
else
|
||||
for _ in $(seq 60); do pgrep -u "$USER" -x plasmashell >/dev/null && break; sleep 3; done
|
||||
pgrep -u "$USER" -x plasmashell >/dev/null || { echo "Plasma didn't start." >&2; exit 1; }
|
||||
fi
|
||||
sudo -n true
|
||||
echo "ssh-ready: $(id -un)@$(hostname), $(uname -r), $(localectl status | sed -n 's/.*LANG=//p'), $(timedatectl show -p Timezone --value)"
|
||||
sudo systemctl poweroff
|
||||
REMOTE
|
||||
)" || true
|
||||
printf '%s\n' "$out" | grep -v '^Connection to .* closed'
|
||||
grep -q '^ssh-ready: ' <<< "$out" || { echo "The first boot check failed; see above and $VM_DIR/vm.log." >&2; kill $vm 2>/dev/null; exit 1; }
|
||||
while kill -0 $vm 2>/dev/null; do sleep 2; done # this VM only: others may run in parallel
|
||||
qemu-img snapshot -c ssh-ready disk.qcow2 && cp vars.fd vars.ssh-ready.fd
|
||||
rm -rf "$www"
|
||||
echo "Done: snapshots 'clean' and 'ssh-ready' in $VM_DIR."
|
||||
echo "Log in by hand as $VM_USER / $password (root: $password)."
|
||||
Executable
+32
@@ -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
|
||||
Executable
+44
@@ -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\""
|
||||
Executable
+67
@@ -0,0 +1,67 @@
|
||||
#!/bin/bash
|
||||
# Build the Steam Machine ISO (steamify-cachyos-live-iso) in the test VM
|
||||
# (directly, no container: build-live-modules.sh and build-calamares-modules.sh with sudo, buildiso.sh as
|
||||
# the user, it sudos itself),
|
||||
# visibly (a Konsole on the VM's desktop follows the log), with the package
|
||||
# downloads cached on the host, and copy the ISO back.
|
||||
# scripts/vmisobuild.sh [--steamify <guest path>]
|
||||
# --steamify a Steamify checkout in the guest (e.g. /mnt, the mounted
|
||||
# REPO) instead of the newest release
|
||||
# The VM must run with its cache shared (one per VM, next to its disk), and
|
||||
# gets all host cores but 2 for the build: start it with
|
||||
# VM_CACHE=$VM_DIR/iso-cache VM_CPUS=$(( $(nproc) * 3 / 4 )) scripts/vmreset.sh --fremont
|
||||
# Env: VM_DIR (see common.sh), ISO_BRANCH (feat/steamify), ISO_OUT (the host's
|
||||
# steamify-cachyos-live-iso/out/desktop).
|
||||
set -euo pipefail
|
||||
here="$(cd "$(dirname "$0")" && pwd)"
|
||||
. "$here/common.sh"
|
||||
steamify=""
|
||||
[[ "${1:-}" == --steamify ]] && steamify="${2:?--steamify needs a guest path}"
|
||||
branch="${ISO_BRANCH:-feat/steamify}"
|
||||
# Where the ISO lands: out/desktop of the host's live-ISO checkout (next to
|
||||
# this repo), where a local build would put it.
|
||||
out="${ISO_OUT:-$(dirname "$(cd "$here/.." && pwd)")/steamify-cachyos-live-iso/out/desktop}"
|
||||
mkdir -p "$out"
|
||||
|
||||
{ printf 'branch=%q steamify=%q\n' "$branch" "$steamify"; cat << 'REMOTE'
|
||||
set -e
|
||||
export XDG_RUNTIME_DIR=/run/user/$(id -u) DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus
|
||||
# The package cache on the host (9p tag `cache`), kept across snapshot resets,
|
||||
# is the VM's pacman cache: the build tools and mkarchiso (which installs the
|
||||
# ISO's packages with the build system's cache) both use it. No container:
|
||||
# the VM is CachyOS already, and a snapshot reset removes the build tools.
|
||||
sudo mkdir -p /var/cache/steamify-iso
|
||||
mountpoint -q /var/cache/steamify-iso || sudo mount -t 9p -o trans=virtio,version=9p2000.L cache /var/cache/steamify-iso ||
|
||||
{ echo "No 9p share 'cache': start the VM with VM_CACHE=<host dir> (run.sh)." >&2; exit 1; }
|
||||
sudo mkdir -p /var/cache/steamify-iso/pkg
|
||||
mountpoint -q /var/cache/pacman/pkg || sudo mount --bind /var/cache/steamify-iso/pkg /var/cache/pacman/pkg
|
||||
[ -z "$steamify" ] || mountpoint -q /mnt || sudo mount -t 9p -o trans=virtio,version=9p2000.L repo /mnt
|
||||
# archlinux.cachyos.org serves packages without their .sig (404), and pacman
|
||||
# doesn't try the next mirror for the signature of a cached package: the
|
||||
# build fails. mkarchiso uses this mirrorlist, so skip it here.
|
||||
sudo sed -i '/archlinux\.cachyos\.org/s/^Server/#Server/' /etc/pacman.d/mirrorlist
|
||||
sudo pacman -Sy --needed --noconfirm archiso mkinitcpio-archiso git squashfs-tools grub >/dev/null
|
||||
iso=~/projects/steamify-cachyos-live-iso
|
||||
if [ -d "$iso/.git" ]; then git -C "$iso" fetch -q && git -C "$iso" checkout -q "$branch" && git -C "$iso" reset -q --hard "origin/$branch"
|
||||
else mkdir -p ~/projects && git clone -q -b "$branch" https://github.com/theupriser/steamify-cachyos-live-iso "$iso"; fi
|
||||
cd "$iso" && echo "ISO repo: $(git log --oneline -1)"
|
||||
./steamify-prepare.sh $steamify | tail -1
|
||||
sudo rm -rf build out
|
||||
cat > ~/projects/iso-build.sh << S
|
||||
#!/bin/bash
|
||||
cd $iso
|
||||
sudo ./build-live-modules.sh && sudo ./build-calamares-modules.sh && { ./buildiso.sh -p desktop -w || ./buildiso.sh -p desktop -c -w; }
|
||||
echo "== BUILD EXIT \$?"
|
||||
ls -la out/desktop/*.iso 2>/dev/null
|
||||
S
|
||||
chmod +x ~/projects/iso-build.sh; : > ~/projects/iso-build.log
|
||||
systemd-run --user --collect -q -u "isobuild-$(date +%s)" bash -c "~/projects/iso-build.sh > ~/projects/iso-build.log 2>&1"
|
||||
systemd-run --user -q --setenv=XDG_CURRENT_DESKTOP=KDE konsole --separate --hold -e bash -c "tail -n 50 -F ~/projects/iso-build.log"
|
||||
REMOTE
|
||||
} | vm_ssh bash -s
|
||||
echo "Building in the VM (its Konsole shows the log)..."
|
||||
until vm_ssh 'grep -q "^== BUILD EXIT" ~/projects/iso-build.log'; do sleep 30; done
|
||||
iso="$(vm_ssh "bash -c 'ls -t ~/projects/steamify-cachyos-live-iso/out/desktop/*.iso 2>/dev/null | head -1'")"
|
||||
[[ -n "$iso" ]] || { echo "No ISO was built; see ~/projects/iso-build.log in the VM." >&2; exit 1; }
|
||||
vm_scp "$VM_USER@$VM_HOST:$iso" "$out/"
|
||||
echo "ISO: $out/$(basename "$iso") (cache: $VM_DIR/iso-cache)"
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
# Progress of test jobs, one line per change (for a monitor; see the progress-report skill).
|
||||
# scripts/vmprogress.sh [--once] [job...] jobs: limine systemd-boot grub cli menu hw installer toggles (default all)
|
||||
# Per job (only a new step, a finished job or a FAIL counts as a change): state (install NN% <stage> / <test step> / done / -), checks passed/expected, failed.
|
||||
# Line: "HH:MM total P/E F failed ;; job | state | P/E | F failed ;; ...". Prints nothing while nothing
|
||||
# changes (checked every 30 s), "ALLDONE" and exits once every started job has its summary line.
|
||||
# Expected counts: the last complete run of each (AGENTS.md "State"); update them when checks are added.
|
||||
set -uo pipefail
|
||||
declare -A expect=([limine]=43 [systemd-boot]=42 [grub]=42 [cli]=55 [menu]=36 [hw]=77 [installer]=4 [toggles]=42)
|
||||
once=false; jobs=()
|
||||
for a in "$@"; do [[ "$a" == --once ]] && once=true || jobs+=("$a"); done
|
||||
[[ ${#jobs[@]} -gt 0 ]] || jobs=(limine systemd-boot grub cli menu hw installer toggles)
|
||||
vms="$HOME/vms"
|
||||
|
||||
status() { # status <job>: "state|pass|fail|finished(0/1)|started(0/1)"
|
||||
local j="$1" log dir
|
||||
case "$j" in limine|systemd-boot|grub) log="$vms/bl-$j.test.log" dir="$vms/bl-$j" ;; *) log="$vms/run-$j.log" dir="$vms/run-$j" ;; esac
|
||||
[[ -f "$log" ]] || { echo "-|0|0|0|0"; return; }
|
||||
local p f st run
|
||||
# The logs are appended to: only the latest run counts (from its "===== vm..." header on).
|
||||
run="$(tac "$log" | sed '/^===== vm/q' | tac)"
|
||||
p=$(grep -ac '^PASS' <<< "$run"); f=$(grep -ac '^FAIL' <<< "$run")
|
||||
if grep -aq "^== $j: [0-9]* passed" <<< "$run"; then echo "done|$p|$f|1|1"; return; fi
|
||||
st=$(grep -aE '^##### ' <<< "$run" | tail -1 | sed 's/^##### [0-9:]* //' | cut -c1-40)
|
||||
if [[ "$st" == install* && -f "$dir/install-www/install.log" ]]; then
|
||||
st="install $(grep -aoE '^\[ *[0-9.]+%\] [^.(]*' "$dir/install-www/install.log" | tail -1 | sed 's/^\[ *\([0-9]*\)[0-9.]*%\] /\1% /; s/ *$//')"
|
||||
# The live installer's own failure (vminstall-live.sh: "== failed: ..."), or the host side giving up.
|
||||
local why; why=$(grep -ahE '^== failed: ' "$dir/install-www/install.log" "$log" 2>/dev/null | tail -1 | cut -c12-70)
|
||||
[[ -n "$why" ]] && { st="INSTALL FAILED: $why"; f=$((f + 1)); }
|
||||
fi
|
||||
echo "${st:-starting}|$p|$f|0|1"
|
||||
}
|
||||
|
||||
prev=""
|
||||
while true; do
|
||||
line="" tp=0 te=0 tf=0 running=0
|
||||
for j in "${jobs[@]}"; do
|
||||
IFS='|' read -r st p f fin started <<< "$(status "$j")"
|
||||
[[ "$started" == 1 && "$fin" == 0 ]] && running=$((running + 1))
|
||||
tp=$((tp + p)) tf=$((tf + f)) te=$((te + ${expect[$j]:-0}))
|
||||
line+=" ;; $j | $st | $p/${expect[$j]:-?} | $f failed"
|
||||
done
|
||||
cur="total $tp/$te $tf failed$line"
|
||||
# A change is a new stage/step, a job finishing or a FAIL: not a new install percentage or PASS count.
|
||||
key="$(sed -E 's/install [0-9]+% /install /g; s/\| [0-9]+\/[0-9?]+ \|/|/g; s/^total [0-9]+\/[0-9]+ //' <<< "$cur")"
|
||||
[[ "$key" != "$prev" ]] && { echo "$(date +%H:%M) $cur"; prev="$key"; }
|
||||
$once && exit 0
|
||||
[[ $running -eq 0 ]] && { echo ALLDONE; exit 0; }
|
||||
sleep 30
|
||||
done
|
||||
+7
-5
@@ -9,10 +9,7 @@
|
||||
# VM_LOG (QEMU output, default $VM_DIR/vm.log).
|
||||
set -euo pipefail
|
||||
. "$(dirname "$0")/common.sh"
|
||||
repo="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
if [[ -z "${VM_DIR:-}" ]]; then
|
||||
if [[ -f "$repo/disk.qcow2" ]]; then VM_DIR="$repo"; else VM_DIR="$HOME/vms/cachyos-test"; fi
|
||||
fi
|
||||
repo_dir="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
SNAPSHOT="${SNAPSHOT:-ssh-ready}"
|
||||
VM_LOG="${VM_LOG:-$VM_DIR/vm.log}"
|
||||
|
||||
@@ -25,6 +22,8 @@ fi
|
||||
|
||||
echo "Restoring snapshot '$SNAPSHOT' in $VM_DIR..."
|
||||
cd "$VM_DIR"
|
||||
# Always this repo's run.sh: a copy in $VM_DIR falls behind (VM_CACHE, ...).
|
||||
[[ "$repo_dir/run.sh" -ef run.sh ]] || cp "$repo_dir/run.sh" run.sh
|
||||
qemu-img snapshot -a "$SNAPSHOT" disk.qcow2
|
||||
cp "vars.$SNAPSHOT.fd" vars.fd
|
||||
|
||||
@@ -37,7 +36,10 @@ vm_ssh bash -s << 'REMOTE'
|
||||
sudo mount -t 9p -o trans=virtio,version=9p2000.L repo /mnt
|
||||
sudo mkdir -p /etc/plasmalogin.conf.d
|
||||
printf '[Autologin]\nUser=%s\nSession=plasma.desktop\n' "$USER" | sudo tee /etc/plasmalogin.conf.d/00-test-autologin.conf >/dev/null
|
||||
sudo systemctl restart plasmalogin
|
||||
# Snapshots with the autologin file log in at boot; restarting plasmalogin
|
||||
# then fails (HELPER_TTY_ERROR on tty1) and leaves the greeter.
|
||||
for _ in $(seq 10); do pgrep -u "$USER" -x plasmashell >/dev/null && break; sleep 3; done
|
||||
pgrep -u "$USER" -x plasmashell >/dev/null || { sudo systemctl reset-failed plasmalogin; sudo systemctl restart plasmalogin; }
|
||||
for _ in $(seq 40); do pgrep -u "$USER" -x plasmashell >/dev/null && break; sleep 3; done
|
||||
sleep 10
|
||||
# Windows the snapshot opens at login would cover the screenshots.
|
||||
|
||||
+1
-1
@@ -25,5 +25,5 @@ sleep 2
|
||||
systemd-run --user --wait -q spectacle -b -n -f -o /tmp/vmshot.png
|
||||
REMOTE
|
||||
} | vm_ssh bash -s
|
||||
scp -q -P "$VM_PORT" "$VM_USER@$VM_HOST:/tmp/vmshot.png" "$out"
|
||||
vm_scp "$VM_USER@$VM_HOST:/tmp/vmshot.png" "$out"
|
||||
echo "$out"
|
||||
+2
-2
@@ -9,7 +9,7 @@ echo "DM=$(systemctl show -p Id --value display-manager) sddm-autologin=$(yn /et
|
||||
echo "plasmalogin [Autologin]: $(sed -n '/^\[Autologin\]/,/^\[/p' /etc/plasmalogin.conf | grep -v '^\[' | tr '\n' ' ')"
|
||||
echo "shortcut=$(grep -h Exec= ~/Desktop/*Gaming*.desktop 2>/dev/null || echo none)"
|
||||
echo "steam-unit=$([ -f ~/.config/systemd/user/steam-desktop-autostart.service ] && systemctl --user is-enabled steam-desktop-autostart.service || echo none) glyphs=$(yn ~/.config/environment.d/99-gamescope-steam-glyphs.conf)"
|
||||
echo "lnf=$(kreadconfig6 --file kdeglobals --group KDE --key LookAndFeelPackage) dark-lnf=$(kreadconfig6 --file kdeglobals --group KDE --key DefaultDarkLookAndFeel) scheme=$(kreadconfig6 --file kdeglobals --group General --key ColorScheme) cursor=$(kreadconfig6 --file kcminputrc --group Mouse --key cursorTheme) vapor-pkg=$(pacman -Q cachyos-vapor 2>/dev/null || echo none) layout-backup=$(yn ~/.local/state/cachyos-gamescope-boot/theme-layout)"
|
||||
echo "lnf=$(kreadconfig6 --file kdeglobals --group KDE --key LookAndFeelPackage) dark-lnf=$(kreadconfig6 --file kdeglobals --group KDE --key DefaultDarkLookAndFeel) scheme=$(kreadconfig6 --file kdeglobals --group General --key ColorScheme) cursor=$(kreadconfig6 --file kcminputrc --group Mouse --key cursorTheme) vapor-pkg=$(pacman -Q cachyos-vapor 2>/dev/null || echo none) layout-backup=$(yn ~/.local/state/steamify/theme-layout)"
|
||||
echo "wallpaper: $(grep -h '^Image=' ~/.config/plasma-org.kde.plasma.desktop-appletsrc | sort -u | tr '\n' ' ')"
|
||||
echo "lock_screen=$(kreadconfig6 --file kdeglobals --group 'KDE Action Restrictions' --key action/lock_screen) autolock=$(kreadconfig6 --file kscreenlockerrc --group Daemon --key Autolock) metaL=$(kreadconfig6 --file kglobalshortcutsrc --group ksmserver --key 'Lock Session' | cut -c1-12)"
|
||||
echo "panel: $(grep -A1 '^\[PlasmaViews\]\[Panel [0-9]*\]$' ~/.config/plasmashellrc | grep floating | tr '\n' ' ') $(grep -h 'thickness' ~/.config/plasmashellrc | tr '\n' ' ')"
|
||||
@@ -18,7 +18,7 @@ echo "leds-pkg=$(pacman -Q leds-valve-dkms-git 2>/dev/null || echo none) steamos
|
||||
echo "dkms: $(dkms status leds-valve-dkms 2>/dev/null | cut -d, -f2- | sed 's/, x86_64//' | tr '\n' ';') kernels: $(ls /usr/lib/modules | tr '\n' ' ')"
|
||||
echo "dkms-override=$(yn /etc/dkms/leds-valve-dkms.conf) headers-check=$(systemctl is-enabled ensure-kernel-headers.service 2>/dev/null || echo none)"
|
||||
echo "leds-owner=$(stat -c %U /sys/class/leds/valve-leds*/brightness 2>/dev/null | sort -u | tr '\n' ' ')"
|
||||
j=~/.local/state/cachyos-gamescope-boot
|
||||
j=~/.local/state/steamify
|
||||
echo "journals: $(ls "$j" 2>/dev/null | tr '\n' ' ')"
|
||||
for f in "$j"/*; do [ -f "$f" ] || continue; d=$(cut -f1-3 "$f" | sort | uniq -d); [ -n "$d" ] && echo "DUPLICATES in $(basename "$f"): $d"; done
|
||||
true
|
||||
|
||||
Executable
+102
@@ -0,0 +1,102 @@
|
||||
#!/bin/bash
|
||||
# One suite of the automated test (TESTPLAN.md rows that need no person) on the plain CachyOS VM
|
||||
# (Plasma, no Steamify: ~/vms/steamify-vm, from vminstall.sh --iso <CachyOS ISO> with VM_STEAMIFY=skip).
|
||||
# scripts/vmsuite.sh <suite> [--window] suites: the folders in share/vmtest/ (cli, ...)
|
||||
# scripts/vmsuite.sh --prepare only make the base image (below)
|
||||
# Every block share/vmtest/<suite>/NN-name.sh starts from a fresh copy of the VM's `ssh-ready` state,
|
||||
# runs in the guest as the VM user (repo on /mnt; NN-name.host.sh blocks run on the host instead: reboots,
|
||||
# host scripts), and prints PASS/FAIL/SKIP lines; the first comment lines say which TESTPLAN rows it
|
||||
# covers; `# env: VAR=value` there is set for the VM start (e.g. BIOS_VERSION=F7F0107).
|
||||
# Fast and parallel: the `ssh-ready` snapshot is converted once into $SRC/base-ssh-ready.qcow2; each suite
|
||||
# has its own directory ~/vms/run-<suite> and ssh port, where every block gets a new qcow2 overlay of the
|
||||
# base (instant, no snapshot restore), and the host's package cache (VM_CACHE, ~/vms/pkg-cache) is bound
|
||||
# over pacman's cache, so packages are downloaded once for all. Log: $VM_DIR.log. Exit status = FAILs.
|
||||
# Env: ONLY=<block name prefix> runs just that block; SRC (default ~/vms/steamify-vm); REPO (default ../steamify-cachyos).
|
||||
set -uo pipefail
|
||||
here="$(cd "$(dirname "$0")" && pwd)"
|
||||
repo="$(cd "$here/.." && pwd)"
|
||||
src="${SRC:-$HOME/vms/steamify-vm}"
|
||||
base="$src/base-ssh-ready.qcow2"
|
||||
|
||||
prepare_base() { # once, under a lock (parallel suites): the ssh-ready snapshot as a stand-alone image
|
||||
( flock 9
|
||||
if [[ ! -f "$base" || "$src/vars.ssh-ready.fd" -nt "$base" ]]; then
|
||||
echo "Making $base from the ssh-ready snapshot (once, a few minutes)..." >&2
|
||||
qemu-img convert -f qcow2 -O qcow2 -l snapshot.name=ssh-ready "$src/disk.qcow2" "$base.tmp" && mv "$base.tmp" "$base"
|
||||
fi ) 9>"$src/.base.lock"
|
||||
[[ -f "$base" ]]
|
||||
}
|
||||
if [[ "${1:-}" == --prepare ]]; then prepare_base; exit $?; fi
|
||||
|
||||
suite="${1:?usage: $0 <suite> [--window] | --prepare}"
|
||||
[[ "${2:-}" == --window ]] || export VM_HEADLESS=1
|
||||
dir="$repo/share/vmtest/$suite"
|
||||
[[ -d "$dir" ]] || { echo "No suite $suite (folders in share/vmtest/)" >&2; exit 2; }
|
||||
case "$suite" in cli) port=2301 ;; menu) port=2302 ;; hw) port=2303 ;; installer) port=2304 ;; toggles) port=2305 ;; *) port=2390 ;; esac
|
||||
export VM_PORT="${VM_PORT:-$port}"
|
||||
export VM_DIR="$HOME/vms/run-$suite"
|
||||
export REPO="${REPO:-$repo/../steamify-cachyos}"
|
||||
export VM_CACHE="${VM_CACHE-$HOME/vms/pkg-cache}"
|
||||
export VM_MEM="${VM_MEM:-4G}" # three VMs at once next to a desktop: 8G each pushed the host into swap
|
||||
prepare_base || { echo "FAIL no base image" >&2; exit 1; }
|
||||
mkdir -p "$VM_DIR"
|
||||
for f in vm-user ssh-key; do cp "$src/$f" "$VM_DIR/$f"; done
|
||||
echo "$REPO" > "$VM_DIR/repo-path"; [[ -e "$VM_DIR/share" ]] || ln -s "$repo/share" "$VM_DIR/share"
|
||||
. "$here/common.sh"
|
||||
log="$VM_DIR.log"
|
||||
view_start # one shared Konsole (common.sh), headless on a desktop
|
||||
|
||||
vm_running() { pgrep -f "[h]ostfwd=tcp::$VM_PORT-" >/dev/null; }
|
||||
vm_stop() {
|
||||
vm_running || return 0
|
||||
vm_ssh -o ConnectTimeout=4 'sudo systemctl poweroff' >/dev/null 2>&1
|
||||
local _; for _ in $(seq 20); do vm_running || return 0; sleep 1; done
|
||||
pkill -f "[h]ostfwd=tcp::$VM_PORT-"; sleep 2
|
||||
}
|
||||
# A fresh VM for a block: new overlay of the base, the snapshot's UEFI vars, Plasma up, repo mounted, package cache bound.
|
||||
vm_fresh() { # vm_fresh [VAR=value...]: extra environment for the VM start (BIOS_VERSION=...)
|
||||
vm_stop
|
||||
cp "$repo/run.sh" "$VM_DIR/run.sh"; cp "$src/vars.ssh-ready.fd" "$VM_DIR/vars.fd"
|
||||
rm -f "$VM_DIR/disk.qcow2" "$VM_DIR/known_hosts"
|
||||
qemu-img create -q -f qcow2 -b "$base" -F qcow2 "$VM_DIR/disk.qcow2" || return 1
|
||||
(cd "$VM_DIR" && env "$@" nohup ./run.sh --fremont > vm.log 2>&1 &)
|
||||
local _; for _ in $(seq 100); do vm_ssh -o ConnectTimeout=3 true 2>/dev/null && break; sleep 3; done
|
||||
vm_ssh -o LogLevel=ERROR bash -s <<'REMOTE'
|
||||
sudo mount -t 9p -o trans=virtio,version=9p2000.L repo /mnt
|
||||
sudo mkdir -p /etc/plasmalogin.conf.d
|
||||
printf '[Autologin]\nUser=%s\nSession=plasma.desktop\n' "$USER" | sudo tee /etc/plasmalogin.conf.d/00-test-autologin.conf >/dev/null
|
||||
for _ in $(seq 10); do pgrep -u "$USER" -x plasmashell >/dev/null && break; sleep 3; done
|
||||
pgrep -u "$USER" -x plasmashell >/dev/null || { sudo systemctl reset-failed plasmalogin; sudo systemctl restart plasmalogin; }
|
||||
for _ in $(seq 40); do pgrep -u "$USER" -x plasmashell >/dev/null && break; sleep 3; done
|
||||
pkill -u "$USER" systemsettings; pkill -u "$USER" cachyos-hello
|
||||
for f in /etc/pacman.d/*mirrorlist*; do sudo sed -i '/krfoss/s/^Server/#Server/' "$f"; done
|
||||
# the host's package cache over pacman's
|
||||
sudo mkdir -p /var/cache/steamify-pkg
|
||||
if sudo mount -t 9p -o trans=virtio,version=9p2000.L cache /var/cache/steamify-pkg 2>/dev/null; then
|
||||
sudo mkdir -p /var/cache/steamify-pkg/pkg; sudo mount --bind /var/cache/steamify-pkg/pkg /var/cache/pacman/pkg
|
||||
# Steamify's own downloads (the CEC driver: GitHub rate-limits it) once for every VM
|
||||
sudo mkdir -p /var/cache/steamify-pkg/steamify /var/cache/steamify; sudo mount --bind /var/cache/steamify-pkg/steamify /var/cache/steamify
|
||||
fi
|
||||
pgrep -u "$USER" -x plasmashell >/dev/null && echo "Logged in to Plasma." || { echo "Plasma did not start." >&2; exit 1; }
|
||||
REMOTE
|
||||
}
|
||||
|
||||
{
|
||||
printf '\n===== vmsuite.sh %s (%s) =====\n' "$suite" "$(date +%T)"
|
||||
for block in "$dir"/*.sh; do
|
||||
[[ -z "${ONLY:-}" || "$(basename "$block")" == "$ONLY"* ]] || continue # ONLY=50: just that block
|
||||
printf '\n##### %s %s\n' "$(date +%T)" "$(basename "$block" .sh)"
|
||||
vm_fresh $(sed -n 's/^# env: //p' "$block" | head -n 1) > /tmp/vmsuite-$suite-reset.out 2>&1 \
|
||||
|| { echo "FAIL the VM did not come up: $(tail -n 1 /tmp/vmsuite-$suite-reset.out)"; continue; }
|
||||
if [[ "$block" == *.host.sh ]]; then # runs on the host (reboots, host scripts); prelude-host.sh has the helpers
|
||||
( . "$repo/share/vmtest/prelude-host.sh"; . "$block" ) 2>&1 | sed -u 's/\x1b\[[0-9;]*m//g'
|
||||
continue
|
||||
fi
|
||||
cat "$repo/share/vmtest/prelude.sh" "$block" | vm_ssh -o ConnectTimeout=6 -o LogLevel=ERROR bash -s 2>&1 | sed -u 's/\x1b\[[0-9;]*m//g'
|
||||
done
|
||||
vm_stop
|
||||
} 2>&1 | tee "$log" | sed -u "s/^/[$suite] /" >> "$TEST_LOG"
|
||||
fails="$(grep -c '^FAIL' "$log")"
|
||||
echo "== $suite: $(grep -c '^PASS' "$log") passed, $fails failed, $(grep -c '^SKIP' "$log") skipped (log: $log)" | tee -a "$log"
|
||||
grep '^FAIL' "$log"
|
||||
exit "$fails"
|
||||
Executable
+21
@@ -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'
|
||||
Executable
+84
@@ -0,0 +1,84 @@
|
||||
#!/bin/bash
|
||||
# The whole automated test, one command: the boot loader test for every loader the Steamify ISO
|
||||
# advertises, plus every suite in share/vmtest/ (TESTPLAN.md rows that need no person).
|
||||
# scripts/vmtest.sh [--install] [--window] [--screen] [boot | <loader>... | <suite>...]
|
||||
# no argument everything: boot (all advertised loaders) and all suites
|
||||
# boot the boot loader test (limine, systemd-boot, grub), or name loaders
|
||||
# <suite> cli, menu, hw, installer, toggles (folders in share/vmtest/)
|
||||
# --install install the boot loader VMs first from the newest Steamify ISO (asks before replacing a disk)
|
||||
# --window show the VMs' windows (default: headless; one shared Konsole follows ~/vms/test.log)
|
||||
# MAX_PARALLEL=3 (env) VMs at once; the slowest suites start first
|
||||
# --screen run in a detached `screen` session named vmtest: `screen -r vmtest` to watch, Ctrl-a d to leave
|
||||
# The suites run on the plain CachyOS VM ~/vms/steamify-vm (Plasma, from vminstall.sh --iso <CachyOS ISO>
|
||||
# with VM_STEAMIFY=skip); the boot loader test on ~/vms/bl-<loader> (Steamify ISO).
|
||||
# Output: one line per loader/suite and the failed checks; the same in ~/vms/last-test.txt. Exit status = failures.
|
||||
# Rows that need a person (screenshots, the real Steam Machine) are listed at the end, never silently dropped.
|
||||
set -uo pipefail
|
||||
here="$(cd "$(dirname "$0")" && pwd)"
|
||||
repo="$(cd "$here/.." && pwd)"
|
||||
iso_repo="$repo/../steamify-cachyos-live-iso"
|
||||
args=("$@")
|
||||
install=""; window=""; screen=false; loaders=(); suites=(); want_boot=false
|
||||
for a in "$@"; do
|
||||
case "$a" in
|
||||
--install) install=--install ;;
|
||||
--window) window=--window ;;
|
||||
--screen) screen=true ;;
|
||||
boot) want_boot=true ;;
|
||||
limine|systemd-boot|grub) loaders+=("$a"); want_boot=true ;;
|
||||
*) [[ -d "$repo/share/vmtest/$a" ]] && suites+=("$a") || { echo "Unknown argument: $a" >&2; exit 2; } ;;
|
||||
esac
|
||||
done
|
||||
if $screen; then
|
||||
command -v screen >/dev/null || { echo "screen is not installed" >&2; exit 2; }
|
||||
screen -ls | grep -q '\.vmtest\s' && { echo "A vmtest screen session is already running: screen -r vmtest" >&2; exit 1; }
|
||||
rest=(); for a in "${args[@]}"; do [[ "$a" == --screen ]] || rest+=("$a"); done
|
||||
screen -dmS vmtest bash -c "$(printf '%q ' "$0" "${rest[@]}"); echo; echo 'Done (summary: ~/vms/last-test.txt). Press Enter to close.'; read -r _"
|
||||
echo "Running in the screen session vmtest: screen -r vmtest (Ctrl-a d to leave it running); the summary lands in ~/vms/last-test.txt"
|
||||
exit 0
|
||||
fi
|
||||
if [[ ${#loaders[@]} -eq 0 && ${#suites[@]} -eq 0 ]] && ! $want_boot; then
|
||||
want_boot=true
|
||||
for d in cli menu hw installer toggles; do [[ -d "$repo/share/vmtest/$d" ]] && suites+=("$d"); done
|
||||
fi
|
||||
# What the ISO advertises: the boot loaders calamares-online.sh keeps.
|
||||
advertised="$(sed -n 's/.*"bootloader:\([^"]*\)".*/\1/p' "$iso_repo/archiso/airootfs/usr/local/bin/calamares-online.sh" 2>/dev/null | head -n 1)"
|
||||
[[ -n "$advertised" ]] || { echo "Can't read the advertised boot loaders from $iso_repo (calamares-online.sh)" >&2; exit 2; }
|
||||
$want_boot && [[ ${#loaders[@]} -eq 0 ]] && read -r -a loaders <<< "$advertised"
|
||||
tested="limine systemd-boot grub" # the loaders vmbootloadertest.sh has a way to boot the other kernel for
|
||||
summary="$HOME/vms/last-test.txt"
|
||||
maxp="${MAX_PARALLEL:-3}" # VMs at once: 8 GB RAM each, 6 vCPUs each
|
||||
# Jobs, the slowest first so the last ones are short: suites, then the boot loaders.
|
||||
jobs_list=()
|
||||
for s in toggles hw cli menu installer; do [[ " ${suites[*]} " == *" $s "* ]] && jobs_list+=("suite:$s"); done
|
||||
for s in "${suites[@]}"; do [[ " toggles hw cli menu installer " == *" $s "* ]] || jobs_list+=("suite:$s"); done
|
||||
for l in "${loaders[@]}"; do jobs_list+=("loader:$l"); done
|
||||
run_job() { # run_job <kind:name>: output in /tmp/vmtest-<name>.out, exit status in /tmp/vmtest-<name>.rc
|
||||
local kind="${1%%:*}" name="${1#*:}"
|
||||
if [[ "$kind" == loader ]]; then bash "$here/vmbootloadertest.sh" "$name" $install $window > "/tmp/vmtest-$name.out" 2>&1
|
||||
else bash "$here/vmsuite.sh" "$name" $window > "/tmp/vmtest-$name.out" 2>&1; fi
|
||||
echo $? > "/tmp/vmtest-$name.rc"
|
||||
}
|
||||
{
|
||||
echo "vmtest.sh $(date '+%F %T') branch $(git -C "$repo" branch --show-current) $(git -C "$repo" rev-parse --short HEAD)"
|
||||
total=0
|
||||
for l in $advertised; do
|
||||
[[ " $tested " == *" $l "* ]] || { echo "FAIL the ISO advertises $l, but there is no test for it (scripts/vmbootloadertest.sh)"; total=$((total + 1)); }
|
||||
done
|
||||
[[ ${#suites[@]} -gt 0 ]] && { bash "$here/vmsuite.sh" --prepare || { echo "FAIL no base image for the suites"; total=$((total + 1)); }; }
|
||||
rm -f /tmp/vmtest-*.rc
|
||||
for job in "${jobs_list[@]}"; do
|
||||
run_job "$job" &
|
||||
while [[ $(jobs -rp | wc -l) -ge $maxp ]]; do wait -n; done
|
||||
done
|
||||
wait
|
||||
for job in "${jobs_list[@]}"; do
|
||||
name="${job#*:}"
|
||||
sed -n '/^== /,$p' "/tmp/vmtest-$name.out"
|
||||
total=$((total + $(cat "/tmp/vmtest-$name.rc" 2>/dev/null || echo 1)))
|
||||
done
|
||||
echo
|
||||
echo "SKIP (needs a person, TESTPLAN.md): U1-U9 the app's screens, R2.2 the first desktop login, H-Real the real Steam Machine"
|
||||
echo "== total: $total failed check(s); boot loaders: ${loaders[*]:-none}; suites: ${suites[*]:-none}"
|
||||
} 2>&1 | tee "$summary"
|
||||
exit "$(grep -c '^FAIL' "$summary")"
|
||||
@@ -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()
|
||||
Executable
+11
@@ -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."
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
# Guest side: after a reboot, the running kernel has Steamify's modules.
|
||||
pass() { echo "PASS $*"; }; fail() { echo "FAIL $*"; }
|
||||
k=$(uname -r)
|
||||
echo "kernel: $k"; echo "cmdline: $(cut -c1-140 /proc/cmdline)"
|
||||
compgen -G "/usr/lib/modules/$k/updates/dkms/steamify-fremont-poweroff.ko*" >/dev/null && pass "power-off fix built for the running kernel" || fail "no power-off fix for $k"
|
||||
# Loaded (a real Steam Machine), or tried at boot and refused with "No such device" (the VM has no AMD GPIO
|
||||
# controller, so the module returns -ENODEV silently). The refusal is in the journal; it only sometimes
|
||||
# reaches dmesg, which is what this used to grep.
|
||||
if lsmod | grep -q '^steamify_fremont_poweroff'; then pass "power-off fix loaded at boot"
|
||||
elif sudo journalctl -b -q --no-pager -u systemd-modules-load | grep -q "steamify_fremont_poweroff.*No such device"; then
|
||||
pass "power-off fix tried at boot (No such device: no AMD GPIO controller in the VM)"
|
||||
else fail "power-off fix not loaded, and not tried at boot"; fi
|
||||
lsmod | grep -q '^leds_valve' && pass "leds_valve loaded" || fail "leds_valve not loaded"
|
||||
[[ -z "$(systemctl --failed --no-legend)" ]] && pass "no failed units" || { fail "failed units:"; systemctl --failed --no-legend; }
|
||||
# B1: booted through the loader's own EFI boot entry, not the firmware's automatic disk entry (the
|
||||
# fallback path \EFI\BOOT\BOOTX64.EFI) and not an entry without a partition (HD(0,GPT,0000...)).
|
||||
cur=$(sudo efibootmgr | sed -n 's/^BootCurrent: //p')
|
||||
entry=$(sudo efibootmgr -v | grep -a "^Boot$cur")
|
||||
if [[ -n "$cur" && "$entry" != *auto_created_boot_option* && "$entry" =~ HD\(([0-9]+),GPT,([0-9a-fA-F-]+) \
|
||||
&& "${BASH_REMATCH[1]}" != 0 && "${BASH_REMATCH[2]}" != 00000000-0000-* ]]; then
|
||||
pass "booted through its own EFI entry: $(cut -f1 <<< "$entry" | cut -c1-60)"
|
||||
else
|
||||
fail "not booted through its own EFI entry (BootCurrent $cur: $(cut -c1-120 <<< "$entry"))"
|
||||
fi
|
||||
Executable
+28
@@ -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
|
||||
Executable
+28
@@ -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
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
# Guest side: Steamify's DKMS modules exist for every installed kernel.
|
||||
# Prints PASS/FAIL lines (scripts/vmbootloadertest.sh counts the FAILs).
|
||||
pass() { echo "PASS $*"; }; fail() { echo "FAIL $*"; }
|
||||
kernels=$(ls /usr/lib/modules)
|
||||
n=$(wc -w <<< "$kernels")
|
||||
echo "kernels: $(tr '\n' ' ' <<< "$kernels")"
|
||||
dkms status
|
||||
[[ $(dkms status | grep -c ': installed') -eq $((3 * n)) ]] && pass "3 DKMS modules x $n kernels installed" || fail "dkms status is not 3 modules x $n kernels"
|
||||
for k in $kernels; do
|
||||
for m in cros-ec-cec leds-valve steamify-fremont-poweroff; do
|
||||
compgen -G "/usr/lib/modules/$k/updates/dkms/$m.ko*" >/dev/null && pass "$m for $k" || fail "$m missing for $k"
|
||||
done
|
||||
done
|
||||
[[ -f /etc/modules-load.d/steamify-fremont-poweroff.conf ]] && pass "modules-load.d has the power-off fix" || fail "no modules-load.d entry"
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
# Guest side ($LOADER = limine|systemd-boot|grub): Steamify's state as its own
|
||||
# menu detects it (the same *_status functions), the OS name untouched, and
|
||||
# the boot loader as expected.
|
||||
pass() { echo "PASS $*"; }; fail() { echo "FAIL $*"; }
|
||||
sudo mountpoint -q /mnt || sudo mount -t 9p -o trans=virtio,version=9p2000.L repo /mnt
|
||||
cd /mnt || exit 1
|
||||
for lib in common state packages login-manager single-user steam-desktop steam-machine fremont-poweroff vram-booster hdmi-refresh cec boot-session vapor-theme steamos-extras bios desktop-shortcut wizard-shortcut steam-game update-notifier; do
|
||||
# shellcheck source=/dev/null
|
||||
. "lib/$lib.sh" 2>/dev/null
|
||||
done
|
||||
# Items this Steam Machine VM has on after `--options gaming,theme,glyphs,single,launcher,notify,cec,machine,poweroff`.
|
||||
for item in gaming theme glyphs single launcher notify poweroff cec; do
|
||||
if "${item}_status" 2>/dev/null; then pass "$item is on (the menu shows it ticked)"; else fail "$item is off"; fi
|
||||
done
|
||||
# Steamify never renames the OS (limine-snapper-sync / grub-btrfs read it).
|
||||
grep -qx 'NAME="CachyOS Linux"' /etc/os-release && grep -qx 'PRETTY_NAME="CachyOS"' /etc/os-release && pass "os-release NAME and PRETTY_NAME are CachyOS's" || fail "os-release NAME/PRETTY_NAME changed"
|
||||
grep -q 'with Steamify' /etc/lsb-release && fail "lsb-release still says 'with Steamify'" || pass "lsb-release name is CachyOS's"
|
||||
# The boot loader is the one this VM was installed with.
|
||||
case "$LOADER" in
|
||||
limine) command -v limine-update >/dev/null && [[ -f /etc/default/limine ]] && pass "Limine is installed" || fail "no Limine"
|
||||
grep -q '^TARGET_OS_NAME="CachyOS"' /etc/default/limine && pass "limine TARGET_OS_NAME is set" || fail "TARGET_OS_NAME missing in /etc/default/limine" ;;
|
||||
systemd-boot) sudo bootctl status --no-pager 2>/dev/null | grep -q 'Product: systemd-boot' && pass "systemd-boot is the boot loader" || fail "not systemd-boot" ;;
|
||||
grub) sudo bootctl status --no-pager 2>/dev/null | grep -q 'Product: GRUB' && pass "GRUB is the boot loader" || fail "not GRUB" ;;
|
||||
esac
|
||||
# "BIOS" menu item: rebooting into the firmware setup must be possible (never trigger it here).
|
||||
sudo bootctl status --no-pager 2>/dev/null | grep -q 'Boot into FW: supported' && pass "reboot into firmware setup is supported" || fail "firmware setup reboot not supported"
|
||||
Executable
+93
@@ -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
|
||||
Executable
+64
@@ -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"
|
||||
Executable
+41
@@ -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"
|
||||
Executable
+8
@@ -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
@@ -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
@@ -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)"
|
||||
Executable
+13
@@ -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"
|
||||
Executable
+16
@@ -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
@@ -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"
|
||||
Executable
+4
@@ -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
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
# H3: BIOS update, the whole flow with WIZARD_BIOS_DRY_RUN=1 (never flashes). The VM reports an older BIOS.
|
||||
# env: BIOS_VERSION=F7F0107
|
||||
out=$(printf '11\n\ny\ny\nUPDATE\nm\nq\nn\n' | WIZARD_BIOS_DRY_RUN=1 bash /mnt/steamify.sh 2>&1 | sed 's/\x1b\[[0-9;]*m//g')
|
||||
grep -qi "dry run" <<< "$out" && pass "H3 the dry run went through" || fail "H3 no dry-run message: $(tail -n 3 <<< "$out" | tr '\n' ' ' | cut -c1-160)"
|
||||
grep -q '^\[ERROR\]' <<< "$out" && fail "H3 errors: $(grep -B4 -m1 '^\[ERROR\]' <<< "$out" | tr '\n' '|' | cut -c1-400)" || pass "H3 no errors"
|
||||
@@ -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
@@ -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"
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
# R1.1-R1.6, R1.10: the menu on an existing desktop, driven with scripted input (each row builds on the last).
|
||||
menu() { printf "$1" | bash /mnt/steamify.sh 2>&1 | sed 's/\x1b\[[0-9;]*m//g'; }
|
||||
# R1.1: first-run ticks: everything but Boot into desktop and BIOS
|
||||
out=$(menu 'q\n')
|
||||
for n in 1 3 4 5 6 7 8 9 10; do grep -qE "^ +$n +[a-z-]+ +\[x\]" <<< "$out" && pass "R1.1 row $n ticked" || fail "R1.1 row $n not ticked"; done
|
||||
grep -qE "^ +11 .*\[ \]" <<< "$out" && pass "R1.1 Update BIOS not ticked" || fail "R1.1 Update BIOS ticked"
|
||||
grep -q "Boot into: \[gamescope\]" <<< "$out" && pass "R1.1 boots into gamescope by default" || fail "R1.1 boot default is not gamescope"
|
||||
# R1.2: everything OK, back in the menu with all on
|
||||
info "applying Steamify (installs packages, builds kernel modules: a few minutes)..."
|
||||
out=$(menu '\ny\nm\nq\nn\n')
|
||||
grep -q '^\[ERROR\]' <<< "$out" && fail "R1.2 errors: $(grep -m1 '^\[ERROR\]' <<< "$out")" || pass "R1.2 no errors"
|
||||
grep -q "^\[OK\]" <<< "$out" && pass "R1.2 applied (the run printed [OK] lines)" || fail "R1.2 printed no [OK] line"
|
||||
expect_on gaming theme glyphs single launcher notify cec machine poweroff
|
||||
# R1.3: the state (what vmstate.sh prints)
|
||||
[[ "$(current_display_manager)" == sddm ]] && [[ -f /etc/sddm.conf.d/10-gamescope-autologin.conf ]] && pass "R1.3 SDDM with autologin" || fail "R1.3 no SDDM autologin"
|
||||
n=$(compgen -G "/sys/class/leds/valve-leds*" | wc -l); [[ $n -eq 17 ]] && pass "R1.3 17 LED nodes" || fail "R1.3 $n LED nodes"
|
||||
own=$(stat -c %U /sys/class/leds/valve-leds*/brightness 2>/dev/null | sort -u | tr '\n' ' '); [[ "$own" == "$(id -un) " ]] && pass "R1.3 LEDs owned by the user" || fail "R1.3 LED owner: $own"
|
||||
[[ $(dkms status | grep -c ': installed') -ge $((3 * $(ls /usr/lib/modules | wc -w))) ]] && pass "R1.3 DKMS installed for every kernel" || fail "R1.3 DKMS: $(dkms status | tr '\n' ';')"
|
||||
systemctl is-active --quiet steamos-manager && pass "R1.3 steamos-manager active" || systemctl --user is-active --quiet steamos-manager && pass "R1.3 steamos-manager active (user)" || fail "R1.3 steamos-manager not active"
|
||||
# R1.4: nothing to do
|
||||
out=$(menu '\nq\n'); grep -qi "already the way you want it" <<< "$out" && pass "R1.4 already the way you want it" || fail "R1.4 message missing"
|
||||
# R1.5: re-apply: no errors, no duplicates in the journals
|
||||
out=$(menu 'a\ny\nm\nq\nn\n')
|
||||
grep -q '^\[ERROR\]' <<< "$out" && fail "R1.5 errors: $(grep -m1 '^\[ERROR\]' <<< "$out")" || pass "R1.5 re-apply without errors"
|
||||
dup=""; for f in ~/.local/state/steamify/*; do [ -f "$f" ] && d=$(cut -f1-3 "$f" | sort | uniq -d) && [ -n "$d" ] && dup+="$(basename "$f"): $d; "; done
|
||||
[[ -z "$dup" ]] && pass "R1.5 no duplicates in the journals" || fail "R1.5 DUPLICATES $dup"
|
||||
# R1.6: --backend status
|
||||
ver=$(sed -n 's/^VERSION=//p' /mnt/steamify.sh | head -n 1)
|
||||
js=$(bash /mnt/steamify.sh --backend status 2>/dev/null)
|
||||
python3 - "$js" "$ver" <<'PY'
|
||||
import json, sys
|
||||
def r(ok, m): print(("PASS " if ok else "FAIL ") + m)
|
||||
try:
|
||||
d = json.loads(sys.argv[1]); r(True, "R1.6 --backend status is valid JSON")
|
||||
except Exception as e:
|
||||
r(False, f"R1.6 --backend status is not JSON: {e}"); sys.exit()
|
||||
r(sys.argv[2] in json.dumps(d), f"R1.6 the JSON has the current version {sys.argv[2]}")
|
||||
PY
|
||||
# R1.10: an item older in features.state shows as an update, and the run brings it up to date
|
||||
f=~/.local/state/steamify/features.state
|
||||
[[ -f "$f" ]] && sed -i 's/^cec=.*/cec=2.1.0/' "$f" && grep -q '^cec=2.1.0' "$f" && pass "R1.10 cec set to 2.1.0" || fail "R1.10 cannot set cec older in $f"
|
||||
out=$(menu 'q\n'); grep -q "HDMI-CEC.*(update)" <<< "$out" && pass "R1.10 the menu shows HDMI-CEC (update)" || fail "R1.10 no (update) on HDMI-CEC"
|
||||
out=$(menu '\ny\nm\nq\nn\n'); grep -q '^cec=2.1.0' "$f" && fail "R1.10 cec still 2.1.0 after the run" || pass "R1.10 cec brought up to date"
|
||||
@@ -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; }
|
||||
@@ -0,0 +1,18 @@
|
||||
# Sent to the guest in front of every block (scripts/vmsuite.sh): helpers and Steamify's libs.
|
||||
# ssh has no Plasma session: the theme item (Vapor layout) needs one (vmrun.sh does the same).
|
||||
export XDG_RUNTIME_DIR=/run/user/$(id -u) WAYLAND_DISPLAY=wayland-0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus
|
||||
info() { echo "INFO $*"; }
|
||||
pass() { echo "PASS $*"; }; fail() { echo "FAIL $*"; }; skip() { echo "SKIP $*"; }
|
||||
sudo mountpoint -q /mnt || sudo mount -t 9p -o trans=virtio,version=9p2000.L repo /mnt
|
||||
cd /mnt || exit 1
|
||||
export SCRIPT_DIR=/mnt # steamify.sh sets it; the libs alone read patches/ and services/ from it
|
||||
for lib in common state packages login-manager single-user steam-desktop steam-machine fremont-poweroff vram-booster hdmi-refresh cec boot-session vapor-theme steamos-extras bios desktop-shortcut wizard-shortcut steam-game update-notifier; do
|
||||
# shellcheck source=/dev/null
|
||||
. "lib/$lib.sh" 2>/dev/null
|
||||
done
|
||||
sf() { bash /mnt/steamify.sh "$@" < /dev/null 2>&1 | sed 's/\x1b\[[0-9;]*m//g'; return "${PIPESTATUS[0]}"; }
|
||||
# is <item>: Steamify's own status function (what the menu ticks); isnt <item>
|
||||
is() { "${1}_status" 2>/dev/null; }
|
||||
expect_on() { local i; for i in "$@"; do is "$i" && pass "$i is on" || fail "$i is off"; done; }
|
||||
expect_off() { local i; for i in "$@"; do is "$i" && fail "$i is on" || pass "$i is off"; done; }
|
||||
ticks() { local i; for i in gaming theme glyphs single launcher notify vram cec machine poweroff boot; do is "$i" && printf '%s ' "$i"; done; echo; }
|
||||
Executable
+32
@@ -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)"
|
||||
Reference in new issue
Block a user