Merge pull request #13 from theupriser/feat/steamify
Desktop ISO / Build (push) Skipped
Desktop ISO / Testing (grub / btrfs) (push) Skipped
Desktop ISO / Testing (grub / ext4) (push) Skipped
Desktop ISO / Testing (grub / f2fs) (push) Skipped
Desktop ISO / Testing (grub / xfs) (push) Skipped
Desktop ISO / Testing (grub / zfs) (push) Skipped
Desktop ISO / Testing (limine / btrfs) (push) Skipped
Desktop ISO / Testing (limine / ext4) (push) Skipped
Desktop ISO / Testing (limine / f2fs) (push) Skipped
Desktop ISO / Testing (limine / xfs) (push) Skipped
Desktop ISO / Testing (limine / zfs) (push) Skipped
Desktop ISO / Testing (refind / btrfs) (push) Skipped
Desktop ISO / Testing (refind / ext4) (push) Skipped
Desktop ISO / Testing (refind / f2fs) (push) Skipped
Desktop ISO / Testing (refind / xfs) (push) Skipped
Desktop ISO / Testing (refind / zfs) (push) Skipped
Desktop ISO / Testing (systemd-boot / btrfs) (push) Skipped
Desktop ISO / Testing (systemd-boot / ext4) (push) Skipped
Desktop ISO / Testing (systemd-boot / f2fs) (push) Skipped
Desktop ISO / Testing (systemd-boot / xfs) (push) Skipped
Desktop ISO / Testing (systemd-boot / zfs) (push) Skipped

Release feat/steamify: renamed ISO workflows, mirror sync via secrets
This commit is contained in:
theupriser authored and GitHub committed 2026-09-30 08:14:57 +02:00
commit 2b7d2cde83
25 files changed
+1184 -11

No files matched your search

+3
View File
@@ -11,6 +11,9 @@ concurrency:
jobs:
build:
# CachyOS's plain ISO and its quicktest matrix: only in CachyOS's own repo, not in this fork
# (the Steamify ISO is iso-2-gitea-build.yml).
if: github.repository == 'CachyOS/CachyOS-Live-ISO'
runs-on: ubuntu-latest
container:
image: archlinux:base-devel
+69
View File
@@ -0,0 +1,69 @@
# The Steamify ISO's release, step 1 of 2 (GitHub): names it and creates its tag and a release with the notes.
# The Gitea mirror (git.upriser.nl) gets the tag with its next sync (triggered here when GIT_UPRISER_TOKEN is set),
# builds the ISO on that tag and attaches it to its own release for it (iso-2-gitea-build.yml). The ISO lives there:
# GitHub releases take at most 2 GB per file, the ISO is bigger, and a release on the mirror only survives its
# syncs when its tag comes from here.
# Name: the Steamify version the ISO gets (its newest release) plus the time (UTC, GitHub's clock when the release starts):
# master releases v<steamify>-<YYYY.MM.DD>-<HHMM>, feat/steamify a pre-release v<steamify>-dev.<...>.
name: ISO 1/2 · Tag and release (GitHub)
# Started by a new Steamify release (its bundle.yml, secret ISO_DISPATCH_TOKEN) or by hand (Run workflow,
# on master for a release, on feat/steamify for a test build); not by every push.
on:
workflow_dispatch:
permissions:
contents: write
env:
GITEA_URL: ${{ secrets.GIT_UPRISER_URL }}
GITEA_REPO: theupriser/steamify-cachyos-live-iso
jobs:
tag:
if: github.server_url == 'https://github.com'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Tag and release
env:
GH_TOKEN: ${{ github.token }}
run: |
steamify="$(gh release view -R theupriser/steamify-cachyos --json tagName -q .tagName)"
steamify="${steamify#v}"
stamp="$(date -u +%Y.%m.%d-%H%M)"
if [ "${{ github.ref }}" = refs/heads/master ]; then
tag="v$steamify-$stamp" pre="" name="CachyOS with Steamify Live ISO $steamify ($stamp UTC)"
else
tag="v$steamify-dev.$stamp" pre="--prerelease" name="CachyOS with Steamify Live ISO $steamify (test build $stamp UTC)"
fi
# The ISO's file on the mirror is named after the tag (iso-2-gitea-build.yml, profiledef.sh), so its
# download link is known now.
file="steamify-cachyos-${tag#v}-x86_64.iso" dl="$GITEA_URL/$GITEA_REPO/releases/download/$tag"
# The Steamify section of CHANGELOG.md ("## CachyOS with Steamify Live ISO"), without its heading.
section="$(awk '/^## CachyOS with Steamify Live ISO/ {found = 1; next} found && /^#/ {exit} found' CHANGELOG.md)"
notes="The ISO is built from \`$(git rev-parse --short HEAD)\` (${{ github.ref_name }}) with Steamify $steamify and published on the mirror (GitHub releases take at most 2 GB per file), about half an hour after this release (the mirror's sync, then the build):
**Download: [$file]($dl/$file)** ([SHA-256]($dl/$file.sha256), [release page]($GITEA_URL/$GITEA_REPO/releases/tag/$tag))
Sources: [Steamify](https://github.com/theupriser/steamify-cachyos/tree/v$steamify), this repository at this tag, and the packages' sources at [CachyOS](https://github.com/CachyOS) and [Arch Linux](https://archlinux.org/packages/).
$section"
# An annotated tag: its own date, so the mirror sorts releases on the same commit in order (a
# lightweight tag only has its commit's date).
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "$tag" -m "$name" "$GITHUB_SHA" && git push origin "refs/tags/$tag"
gh release create "$tag" --verify-tag $pre --title "$name" --notes "$notes"
echo "Released $tag"
# Optional: sync the mirror at once instead of at its interval (a Gitea token with repository write
# access as the secret GIT_UPRISER_TOKEN).
- name: Sync the Gitea mirror
env:
GIT_UPRISER_TOKEN: ${{ secrets.GIT_UPRISER_TOKEN }}
run: |
if [ -z "$GIT_UPRISER_TOKEN" ]; then echo "No GIT_UPRISER_TOKEN: the mirror picks the tag up at its next sync."; exit 0; fi
curl -fsS --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" &&
echo "Mirror sync started"
+112
View File
@@ -0,0 +1,112 @@
# The Steamify ISO's release, step 2 of 2 (the Gitea mirror, git.upriser.nl, on its own runner): builds the
# ISO for a release tag that iso-1-github-tag.yml created on GitHub (it comes in with the mirror's sync, and a
# release on the mirror only survives its syncs when its tag comes from GitHub), then attaches the ISO, its
# checksums and package list to the mirror's release for that tag. The tag names the Steamify version and
# the time: v<steamify>-<YYYY.MM.DD>-<HHMM> (master) or v<steamify>-dev.<...> (feat/steamify, a pre-release);
# the ISO gets exactly that Steamify release, and its label the same moment. GitHub skips it.
# The runner needs privileged containers (archiso mounts loop devices) and ~20 GB of disk; Gitea's
# [repository.release] FILE_MAX_SIZE must allow the ISO (default 2 GB).
name: ISO 2/2 · Build and publish (Gitea)
on:
push:
tags: ['v*']
workflow_dispatch:
concurrency:
group: steamify-iso-${{ github.ref }}
cancel-in-progress: true
jobs:
iso:
if: github.server_url != 'https://github.com'
runs-on: ubuntu-latest
container:
image: docker.io/cachyos/cachyos:latest
options: --privileged
defaults:
run:
shell: bash
steps:
- name: Tools
run: |
pacman-key --init && pacman-key --populate
# nodejs: the actions below run in this container
pacman -Syu --noconfirm --needed archiso mkinitcpio-archiso git squashfs-tools grub sudo nodejs curl jq
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Version and time from the tag
run: |
tag="${{ github.ref_name }}"
[[ "$tag" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)-(dev\.)?([0-9]{4})\.([0-9]{2})\.([0-9]{2})-([0-9]{4})$ ]] ||
{ echo "::error::$tag is not a Steamify ISO release tag (v<steamify>-[dev.]<YYYY.MM.DD>-<HHMM>)"; exit 1; }
m=("${BASH_REMATCH[@]}")
{
echo "STEAMIFY_VERSION=${m[1]}"
# One moment for the ISO's label (profiledef.sh) and the release's name: the tag's (UTC).
echo "STEAMIFY_BUILD_STAMP=${m[3]}${m[4]}${m[5]}_${m[6]}"
echo "ISO_PRERELEASE=$([ -n "${m[2]}" ] && echo true || echo false)"
# The ISO's file name: the tag without its v (profiledef.sh).
echo "STEAMIFY_ISO_VERSION=${tag#v}"
} >> "$GITHUB_ENV"
- name: Steamify on the ISO (the version the tag names)
run: ./steamify-prepare.sh
- name: Build
run: |
./build-live-modules.sh
./build-calamares-modules.sh
./buildiso.sh -p desktop -w || ./buildiso.sh -p desktop -c -w
- name: Release name and notes
id: rel
env:
TOKEN: ${{ github.token }}
run: |
iso="$(ls out/desktop/*.iso | head -n 1)"
[ -f "$iso" ] || { echo "::error::no ISO in out/desktop"; exit 1; }
tag="${{ github.ref_name }}" b="$STEAMIFY_BUILD_STAMP"
stamp="${b:0:4}.${b:4:2}.${b:6:2}-${b:9:4}"
if [ "$ISO_PRERELEASE" = true ]; then
name="CachyOS with Steamify Live ISO $STEAMIFY_VERSION (test build $stamp UTC)"
else
name="CachyOS with Steamify Live ISO $STEAMIFY_VERSION ($stamp UTC)"
fi
# Never overwritten: a release for this tag that already has the ISO is left alone.
api="${{ github.server_url }}/api/v1/repos/${{ github.repository }}"
if curl -fsS -H "Authorization: token $TOKEN" "$api/releases/tags/$tag" -o /tmp/rel.json &&
[ "$(jq '[.assets[] | select(.name | endswith(".iso"))] | length' /tmp/rel.json)" -gt 0 ]; then
echo "::warning::$tag already has its ISO and won't be overwritten."
echo "publish=false" >> "$GITHUB_OUTPUT"; exit 0
fi
{ echo "publish=true"; echo "name=$name"; } >> "$GITHUB_OUTPUT"
section="$(awk '/^## CachyOS with Steamify Live ISO/ {found = 1; next} found && /^#/ {exit} found' CHANGELOG.md)"
{
echo "Built from \`$(git rev-parse --short HEAD)\` ($tag) with Steamify $STEAMIFY_VERSION."
echo
echo "SHA-256: \`$(cut -d' ' -f1 "$iso.sha256")\`, label \`STEAMIFY_${STEAMIFY_VERSION//./_}_$b\`"
echo
echo "Sources: [Steamify](https://github.com/theupriser/steamify-cachyos/tree/v$STEAMIFY_VERSION), this repository at this tag, and the packages' sources at [CachyOS](https://github.com/CachyOS) and [Arch Linux](https://archlinux.org/packages/)."
echo
echo "$section"
} > release-notes.md
cat release-notes.md
- name: Publish ${{ github.ref_name }}
if: steps.rel.outputs.publish == 'true'
uses: akkuman/gitea-release-action@v1.3.7
with:
token: ${{ github.token }}
tag_name: ${{ github.ref_name }}
name: ${{ steps.rel.outputs.name }}
body_path: release-notes.md
prerelease: ${{ env.ISO_PRERELEASE }}
files: |
out/desktop/*.iso
out/desktop/*.iso.sha256
out/desktop/*.iso.sha1
out/desktop/*.pkgs.txt
+6
View File
@@ -21,3 +21,9 @@ archiso/airootfs/etc/environment
archiso/airootfs/etc/pacman.d/cachyos-mirrorlist
archiso/airootfs/etc/edition-tag
archiso/airootfs/etc/version-tag
archiso/airootfs/usr/local/share/steamify/
archiso/airootfs/usr/lib/libboost_*
archiso/airootfs/etc/steammachine-iso-build
archiso/airootfs/usr/lib/modules/
archiso/airootfs/etc/modules-load.d/steamify-fremont-poweroff.conf
archiso/airootfs/usr/lib/calamares/modules/packagechooserq/
+132
View File
@@ -0,0 +1,132 @@
# AGENTS.md
Guidance for AI coding agents working on this repository (the Steam Machine
live ISO: CachyOS + the Steamify installer step). See `README.md` and
`TODO.md` for what's built and what's left; this file is operational
knowledge — how to test/build/debug it without rediscovering the same
things every session. `TODO.md` should stay a plain task list; lessons and
gotchas belong here instead.
Full build/test workflow (VM scripts, snapshots, cache setup) lives in the
`steam-machine-iso` and `cachyos-vm-testing` skills in `steamify-cachyos-dev`
— load those first. This file is the overflow: things worth knowing that
don't fit a skill's step-by-step flow.
## Calamares modules
- Verify a module is actually installed (`ls /usr/lib/calamares/modules`)
before writing QML/config for it. A `.conf` file existing is not proof —
CachyOS's own `cachyos-calamares-next` build explicitly skips
`packagechooserq` (`-DSKIP_MODULES=...`).
- Read a module's *actual* logged global-storage writes (Calamares debug
log, `-D6`), not its UI label text, to know what a `mode`/`method` really
does.
- `packagechooser`'s multi-select only works with Shift/Ctrl-click (a plain
click replaces the selection) and its UI has no visible checkboxes — not
usable as the Steamify page. Use `packagechooserq` (custom QML,
qtplugin/legacy) or `netinstall` (checkbox tree) instead.
- `packagechooserq` is built out of tree against the installed Calamares
CMake config (`/usr/lib/cmake/Calamares`) with
`-DWITH_QML=ON -DQT_VERSION_SUFFIX=-qt6 -Dqtname=Qt6 -DWITH_QT6=ON`;
`find_package(KF6CoreAddons)` must come before `find_package(Calamares)`
or the build fails with "KF6::CoreAddons target not found". Its `Config`
class (and `prettyStatus()`'s raw `Install option: <ids>` text) is shared
with the plain `packagechooser` module — override `prettyStatus()` in
`PackageChooserQmlViewStep.cpp` itself (not `Config.cpp`) to change only
this module's Summary display without affecting `packagechooser@desktop`
etc. elsewhere in the installer.
- Both `packagechooserq` (QML swap) and `netinstall` (config swap) are
testable live in a booted ISO VM with no rebuild: edit
`/etc/calamares/modules/*.conf` + `settings.conf`
(`steamify-cachyos-dev/share/sq.sh` / `sn.sh`), relaunch
`pkexec-wrapper calamares -D6` in a **separate Konsole tab** (it blocks
the tab it runs in; typing into that same tab just echoes inertly into
its blocked stdin). A C++ module change needs an actual rebuild
(out-of-tree, in the build VM) and redeploying the `.so` — no shortcut.
## Shell pitfalls
`local a=/x b="$a/y"` expands every value before assigning any, so `b` is
`/y`: `calamares-online.sh` wrote the downloaded `steamify.sh` to
`/steamify.sh` and `items.json` to a nonexistent `/qml/`, silently (stderr
went to `/dev/null`), so the Steamify page always ran on build-time rows and
the Summary had no labels. Declare a variable in its own `local` before
using it, and send a step's errors to the launcher's log, not `/dev/null`.
## ISO permissions
`steamify-prepare.sh` fetches `steamify.sh` with `curl -o`, which doesn't
carry the executable bit, and `archiso/profiledef.sh`'s `file_permissions`
map is the only other place permissions get fixed up on the ISO — any path
not listed there stays at whatever `curl`/`cp` gave it. `calamares-online.sh`
execs `steamify.sh` directly (not via `bash`) to regenerate `Items.qml`/
`items.json` at boot; without +x that failed *silently* (its stderr is
redirected to `/dev/null`), so the page kept working off build-time
fallbacks with no visible error. When something the ISO downloads or copies
at build time needs to run directly (not `bash script.sh`), add it to
`file_permissions` — don't assume the source already had the right mode.
## Driving a live/installer session via QMP
- Don't screenshot-poll for VM readiness. Pass `VM_SERIAL=<file>` to
`run.sh`/`vmisoboot.sh` (logs `console=ttyS0` to a plain text file) and
grep that instead — near-zero tokens vs. repeated screenshot + image
analysis. Only screenshot at real decision points (to show the user, or
to check a layout), not as a polling mechanism.
- The live session's keyboard layout keeps drifting to Dutch (e.g. after
Calamares' Welcome page's keyboard preview, or after opening System
Settings), which silently breaks `qmptype.py`'s US-layout character map
(`/` → `-`, `&`/`>` fail outright) with no error for the common
substitutions. Check with a no-symbols probe (`echo test123`) after any
language/keyboard step. Typing `setxkbmap us` via QMP does fix it.
- `qmptype.py` types `&`, `>`, `<`, `?`, `{`, `}` since 2026-09-28 (before,
`>` raised "no key for" and left half a command in the prompt: send
Ctrl+C before retyping). Its map is US-layout, see above.
- Send QMP keystrokes one command at a time with a beat (~1-2s) in between.
Firing several `qmptype.py` calls back-to-back can interleave with the
guest's own prompt redraw and garble the input (seen as commands merging
or a stray leading character).
- Calamares page Next/Back buttons move vertically as page content
grows/shrinks: re-screenshot 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 (Cancel/Afbreken, then confirm the
"really cancel?" dialog).
## Build VM SSH flakiness
The build VM's guest sshd applies OpenSSH's `PerSourcePenalties`: repeated
quick reconnect attempts within its window make `ssh`/`scp` fail instantly
with "Connection closed" (verbose: "Not allowed at this time"), and further
quick retries seem to extend the penalty rather than reset it. Don't
retry-loop through it — that makes it worse. Either back off for a good
while (minutes, not seconds) with zero attempts in between, or just restart
the VM, which clears its in-memory penalty state immediately and is
usually faster than waiting it out.
Large `scp` transfers (e.g. the ~3.2GB built ISO) over this same flaky link
can report "Connection closed" while actually still having copied fully —
check the destination file's size/timestamp before assuming a real
failure, and retry the copy (it's idempotent) rather than the whole build.
## Releases (GitHub tag, ISO built and attached on the Gitea mirror)
Full write-up, with every trap: `steamify-iso-release` in steamify-cachyos-dev. What matters here:
- `.github/workflows/iso-1-github-tag.yml` (GitHub, `workflow_dispatch` only: by hand, or started by a new Steamify
release, whose `bundle.yml` needs the secret `ISO_DISPATCH_TOKEN`) names the release: the Steamify version plus
GitHub's UTC time, `vX.Y.Z-[dev.]YYYY.MM.DD-HHMM`, an **annotated** tag and a release with notes and the direct
download link. `feat/steamify` makes a `dev.` pre-release, `master` a real one.
- `.github/workflows/iso-2-gitea-build.yml` (Gitea, `on: push: tags: v*`, skipped on GitHub) builds the ISO for that
tag and attaches it to the mirror's release. The tag decides everything: Steamify version (`steamify-prepare.sh`
takes `STEAMIFY_VERSION`), file name (`STEAMIFY_ISO_VERSION` -> `iso_version` in `profiledef.sh`), label
(`STEAMIFY_BUILD_STAMP`), boot menu and `/etc/steammachine-iso-build`. The build reads no clock; without a
tag everything says `local`.
- `util-iso.sh` runs `sudo mkarchiso`: sudo drops the environment, so `--preserve-env=STEAMIFY_ISO_VERSION,
STEAMIFY_BUILD_STAMP` is required (without it the ISO comes out `-local-`).
- Never create a release on the mirror by hand or from a Gitea-only tag: the pull mirror's next sync deletes tags
GitHub doesn't have, and the release with them. GitHub releases can't hold the ISO (2 GB per file).
- Gitea needs `[repository.release] FILE_MAX_SIZE` above the ISO (413 otherwise) and the runner
`container: privileged: true` (archiso mounts `/proc`); the workflow's `--privileged` option is ignored.
- CachyOS's own `Desktop ISO` workflow (`build.yml`) only runs in `CachyOS/CachyOS-Live-ISO`.
+40
View File
@@ -26,6 +26,46 @@
* Correctly handle VM environments
* Correctly quote pacman arguments
## CachyOS with Steamify Live ISO
CachyOS 26.08 with Steamify built in: install CachyOS and turn the PC into a Steam Machine in one go.
Its version is the Steamify version on it plus the build date and time (UTC, when the release starts on GitHub; a release on `master` is `v<Steamify version>-<date>-<HHMM>`, e.g. `v2.9.3-2026.09.29-2015`). A new Steamify release starts one (a test build for now); by hand: Run workflow on "ISO 1/2 · Tag and release (GitHub)".
Releases: GitHub creates the tag and a release with the notes; the ISO itself (over GitHub's 2 GB limit) is built on the Gitea mirror and attached to its release for that tag (git.upriser.nl).
**Features:**
* **ISO:** named after its release tag (`steamify-cachyos-2.9.3-2026.09.29-2030-x86_64.iso`; built by hand, without a tag: `local`), labelled after the Steamify version on it and the build time (`STEAMIFY_2_9_1_20260929_1432`, was CachyOS's `COS_<year><month>`); CachyOS Hello installs it like CachyOS's own
release (the ISO's own release, its tag without the v, is in `/etc/steammachine-iso-build` and the boot menu)
* **Installer (CachyOS - Steamify Installer):**
* A Steamify page after the packages page: which Steam Machine parts to set up (rows from
`steamify.sh --defaults --list`, the newest Steamify fetched before Calamares starts) and where
the PC starts (gaming mode or the desktop)
* The Summary lists every chosen item and where the PC starts
* The new user's Steam Machine setup runs during the install (`steamify-install`, a Calamares step
before cleanup): the first boot is already a Steam Machine
* Only what Steamify supports is offered: KDE Plasma, and the boot loaders Limine, systemd-boot and
GRUB (not rEFInd)
* **Live session:** keeps CachyOS's own name; the power-off fix is built for the ISO's kernels, and
the login has the Vapor look
**Fixes:**
* **systemd-boot:** the installed system gets its "Linux Boot Manager" EFI boot entry, first in the
boot order. cachyos-installer runs `bootctl install` in a chroot, where bootctl leaves the EFI
variables alone, so the entry was missing: the PC could only boot through the disk's fallback path
(`\EFI\BOOT\BOOTX64.EFI`). Most PCs find it early and boot normally, but firmware that tries the
network first waits minutes on every boot (QEMU/OVMF: PXE, HTTP boot and the EFI shell come first),
and anything else that writes the fallback file (another OS) takes the boot over.
`steamify-install` now registers it with `efibootmgr` from the live system, with the ESP's real
disk and partition (bootctl in the chroot writes an entry without a partition, which the firmware
can't load); the output is in `/var/log/steamify-bootentry.log`
* **Installer:** Steamify's install-time logs (`/var/log/steamify-install.log`,
`steamify-bootentry.log`) are readable after the first boot: `/var/log` is its own btrfs subvolume
(`@log`) that wasn't mounted yet when Steamify ran, so they ended up hidden under it
* **Installer:** Steamify's setup never asks for a password (its sudo rule is read after CachyOS's
`%wheel` rule); Calamares starts with the Boost version it was linked against
* **Build:** finishes cleanly as root and reports no error after a successful build; the Steamify page's
Calamares module is built with the ISO and left out when Calamares' version differs
# 26.07
**Features:**
+60
View File
@@ -1,3 +1,63 @@
# Steamify CachyOS ISO for the Steam Machine
A CachyOS desktop ISO for the Valve Steam Machine with
[Steamify](https://github.com/theupriser/steamify-cachyos) built in:
- The live session has the Steam Machine drivers (power-off fix).
- The installer ("CachyOS with Steamify Installer") has a Steamify page where
you pick the options and whether the PC starts in gaming mode or on the
desktop. Steamify is applied during the install.
> **Choose KDE Plasma in the installer.** Steamify only works with KDE Plasma
> (the default). With another desktop (GNOME, Hyprland, Cosmic, ...) Steamify
> is not supported.
## Building the ISO yourself
You need about 20 GB of free space, and a Linux PC with
[podman](https://podman.io) (or Docker: replace `podman` with `docker`). The
build runs in CachyOS's own container, so the PC doesn't have to run CachyOS.
```bash
git clone -b feat/steamify https://github.com/theupriser/steamify-cachyos-live-iso
cd steamify-cachyos-live-iso
# Put Steamify on the ISO: the newest release. To use your own checkout,
# pass its path: ./steamify-prepare.sh ~/steamify-cachyos
./steamify-prepare.sh
mkdir -p ~/iso-cache # downloaded packages, kept for the next build
sudo podman run --rm -t --privileged --network=host \
--pids-limit=-1 --ulimit nofile=65536:65536 \
-v ~/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'
```
The ISO is written to `out/desktop/steamify-cachyos-<date>.iso`. Write it to
a USB stick (for example with `dd` or Fedora Media Writer) and boot the Steam
Machine from it.
**Why these steps:**
- `build-live-modules.sh` builds the power-off fix for the ISO's kernels.
Without it, the Steam Machine starts up again right after shutting down.
- `build-calamares-modules.sh` builds the installer module that shows the
Steamify page. CachyOS's installer package leaves it out.
- `--pids-limit=-1` and `--ulimit nofile=...` are needed because pacman runs
out of processes or open files in the container otherwise (GPGME errors).
- `--network=host` gives the container DNS.
- `-w` removes the build folder afterwards. Before building again, run
`sudo rm -rf build out`.
To build on CachyOS directly, without a container, install the same packages
with pacman and run the three scripts with `sudo`.
---
The notes below are from CachyOS's original repository.
These are the basic needed files and folders to build CachyOS system.
### buildiso
+89
View File
@@ -0,0 +1,89 @@
# To do (Steam Machine edition)
## Open
1. **Steamify installer page** (2026-09-28, working end to end; see
`AGENTS.md` for the module/build/testing details and gotchas behind
this). `packagechooserq` (custom QML) is built with each ISO by
`build-calamares-modules.sh` into `archiso/airootfs/usr/lib/calamares/modules/packagechooserq/`.
`netinstall` (checkbox tree) stays as a manually-swappable fallback
(`steamify-cachyos-dev/share/sn.sh` + `nisteamify.conf`) for when the
module can't load, but isn't wired into `calamares-online.sh`
automatically yet.
- **Still open**:
- A Python job (e.g. `steamifychoice`, before `packages@online`) to
normalize either page's choice (packagechooserq's
`packagechooser_steamifypage` GS key, or netinstall's
packageOperations markers) into one `steamifyChoice` GS key, so
`shellprocess_steamify.conf` doesn't need to know which page ran.
2. **Names**: only the installer says Steamify ("CachyOS with Steamify
Installer", productName set by calamares-online.sh); the live session,
Hello and About this System keep CachyOS's own names (legal clarity). The
ISO file stays `steamify-cachyos-<date>.iso`.
3. **Check the names** after the next build: installer title, Hello and
About this System say CachyOS.
4. ~~Steamify PR for `feat/defaults-options`~~ — done: 2.7.0 (`--options`,
`--boot`, `steamify.sh --boot` standalone) and 2.8.0
(`--defaults --list`, what the Steamify page's rows come from) are both
released. **The ISO builder is up to date with this**: no checkout
argument needed, `steamify-prepare.sh` (no args) and `calamares-online.sh`
at boot both just fetch the newest release, so the ISO always has 2.8.0+
without a rebuild for a Steamify-side change alone. Only pass
`steamify-prepare.sh <checkout>` / `vmisobuild.sh --steamify <path>` when
testing an *unreleased* Steamify change.
5. **Build in the test VM** instead of on the Steam Machine
(steam-machine-iso skill), and install the result unattended with
`scripts/vminstall.sh --iso` (vm-install skill; the Calamares Steamify
step then needs `steamify-install` run from the live script).
6. **Test on the real Steam Machine** from a USB stick (gamescope, LEDs, CEC,
power-off) before calling the ISO usable.
## Done
- **Full ISO install with the Steamify page** (2026-09-28, ISO VM,
`--fremont`, Start in: desktop): Summary listed the choices, the
install step ran Steamify 2.8.0 with every item OK, `--boot desktop`
applied, `exit: 0`; the installed system boots into the desktop.
- **Steamify Summary** (2026-09-28, ISO VM): an unordered list of the chosen
items' names plus "Starts in gaming mode/the desktop"
(`patches/packagechooserq-steamify-summary.patch`), with `items.json`
written by the launcher (its `$sdir` bug fixed, `0cc7367`).
- **packagechooserq built with the ISO** (2026-09-28):
`build-calamares-modules.sh` builds it in the build container/VM from
CachyOS's Calamares source against the current `cachyos-calamares-next`
(no prebuilt `.so` in git), with `built-for`; `calamares-online.sh` leaves
the Steamify page out when the Calamares it runs differs (the step then
installs everything on) instead of risking "Initialization Failed".
- **Steamify 2.6.0** released (`--defaults`, install-time mode).
- **Live session** (VM, `--fremont`): Vapor look and layout (Steam Deck
wallpaper) at the live login; the power-off module is built for both ISO
kernels and loaded at boot, in the VM it returns "No such device" (no
AMDI0030 GPIO controller), as designed. Whether it keeps the real Steam
Machine off is part of the hardware test.
- **Regression test 2.6.0 on an existing desktop install** (test VM from
ssh-ready, `--fremont`): full first run, re-apply, notifications/CEC/theme
off and on again. No errors; user units enabled and running, theme applied
and restored live, no first-login autostart with a session.
- **ISO name**: `steamify-cachyos-<date>-x86_64.iso` (`iso_name` in
`archiso/profiledef.sh`); the volume label stays `COS_<yyyymm>`.
- **VM install from the ISO (2026-09-27, `f42859a`):** a VM install from the ISO (Hello's
Install, no manual fixes) ran Steamify's step: every component OK,
`exit: 0`, SDDM autologin into gamescope. First desktop login (session switched to plasma in the VM) passed too:
Vapor layout, the app opened with everything on. The first-login script
runs the newest *release*, so it only works once 2.6.0 is released (in the
test it was pointed at the bundle).
The VM's text
console doesn't show (virgl): read the installed disk with
`qemu-nbd -r` + `mount -o ro,rescue=nologreplay,subvol=@` (logs in `@log`).
## Later
- **Branding**: only the installer carries Steamify's name; check with the
CachyOS team whether that and the ISO file name are fine with them.
- **Release**: a CI job that builds the ISO (privileged container, flags from
the steam-machine-iso skill) and hosts it (>2 GB, not a GitHub release asset).
- **Drop the Boost 1.91 workaround** in `steamify-prepare.sh` once
`cachyos-calamares-next` is rebuilt against the repos' Boost.
Build and test details: the `steam-machine-iso` skill in steamify-cachyos-dev.
@@ -0,0 +1,10 @@
# Steamify: its page (the app's switches, /usr/local/share/steamify-installer/
# SteamifyPage.qml). Legacy method: the page's choice (the ids that are on,
# plus "boot" for Boot into Desktop) goes to global storage as
# packagechooser_steamifypage, for shellprocess_steamify.conf.
---
method: legacy
qmlFilename: /usr/local/share/steamify-installer/SteamifyPage.qml
labels:
step: "Steamify"
packageChoice: gaming,theme,glyphs,single,launcher,notify,vram,cec,machine,poweroff
@@ -0,0 +1,10 @@
# Steamify: the Steam Machine setup for the new user, before the first boot.
---
dontChroot: true
timeout: 3600
script:
- "-/usr/local/bin/steamify-install ${ROOT} ${USER} \"${gs[packagechooser_steamifypage]}\""
i18n:
name: "Setting up the Steam Machine (Steamify)"
@@ -0,0 +1,7 @@
[Desktop Entry]
Type=Application
Name=Steamify live theme
Exec=/usr/local/bin/steamify-live-theme
OnlyShowIn=KDE;
X-KDE-autostart-phase=2
NoDisplay=true
@@ -33,6 +33,24 @@ main() {
echo "USING ISO VERSION: ${ISO_VERSION}"
sudo pacman -Sy --noconfirm cachyos-calamares-next
# The installer's window title ("%1 Installer") and welcome text use the
# branding's productName; the reinstall above restored CachyOS's.
sudo sed -i 's/^\( productName: *\)CachyOS$/\1CachyOS with Steamify/' \
/usr/share/calamares/branding/cachyos/branding.desc
# Only what Steamify supports, like CachyOS's handheld edition does:
# KDE Plasma as the desktop, and the boot loaders Steamify knows how to
# update (Limine, systemd-boot, GRUB; not rEFInd). Drops every other
# ` - id:` item block; `default:` (KDE-Desktop, limine) stays valid.
local keep_items='/^ - id: / { id = $3; gsub(/"/, "", id); skip = id == "" || index(" " keep " ", " " id " ") == 0 }
/^[^ ]/ { skip = 0 }
!skip'
local chooser keep
for chooser in "desktop:KDE-Desktop" "bootloader:limine systemd-boot grub"; do
keep="${chooser#*:}"
chooser="/etc/calamares/modules/packagechooser_${chooser%%:*}.conf"
awk -v keep="$keep" "$keep_items" "$chooser" | sudo tee "$chooser.new" > /dev/null &&
sudo mv "$chooser.new" "$chooser"
done
# Get Hardware Informations
inxi -F > "$log"
@@ -44,7 +62,64 @@ main() {
########## System: $SYSTEM
EOF
# Steamify: try the newest release before Calamares starts, so the page
# and the install step both show/use it; keep the ISO's copy (from
# steamify-prepare.sh) on any failure (no network, GitHub unreachable).
# Separate statements: `local` expands all its values before assigning
# any, so "$sdir" would still be empty in the same one.
local sdir=/usr/local/share/steamify
local sbin="$sdir/steamify.sh" sqml="$sdir/qml"
local tmp; tmp="$(mktemp)"
if curl -fsSL --max-time 20 https://github.com/theupriser/steamify-cachyos/releases/latest/download/steamify.sh -o "$tmp" &&
grep -q -- '--defaults' "$tmp"; then
sudo install -Dm755 "$tmp" "$sbin"
fi
rm -f "$tmp"
tmp="$(mktemp -d)"
if curl -fsSL --max-time 20 https://github.com/theupriser/steamify-cachyos/releases/latest/download/steamify-app.tar.gz |
tar -xz -C "$tmp" --strip-components=2 ui/qml/Theme.qml ui/qml/Texts.qml 2>/dev/null; then
sudo cp "$tmp/Theme.qml" "$tmp/Texts.qml" "$sqml/"
fi
rm -rf "$tmp"
# The page's rows, from whichever steamify.sh ended up on the ISO
# (regenerated even when the download failed: the fallback Items.qml
# doesn't know this machine, e.g. no VRAM region or no Steam account yet).
# JSON is valid JS array-literal syntax, so the output goes straight into
# the singleton as-is (--list guarantees one line of only its own JSON
# punctuation and text fields Steamify itself writes: nothing here can
# break out of the property).
tmp="$(mktemp)"
if "$sbin" --defaults --list > "$tmp" 2>> "$log" && [[ -s "$tmp" ]]; then
{ printf 'pragma Singleton\nimport QtQuick\n\nQtObject {\n readonly property var rows: '
cat "$tmp"
printf '\n}\n'
} | sudo tee "$sqml/Items.qml" > /dev/null
# Same rows, plain JSON: packagechooserq's prettyStatus() override
# reads this to show the Summary step's choice as labels, not ids.
sudo cp "$tmp" "$sqml/items.json"
fi
rm -f "$tmp"
sudo cp "/usr/share/calamares/settings_${mode}.conf" /etc/calamares/settings.conf
# Steamify: its page (packagechooserq@steamifypage, right after the
# packages page) and its step (modules/shellprocess_steamify.conf) before
# the installer cleans up after itself.
# The page's module is built for one cachyos-calamares-next
# (build-calamares-modules.sh); the pacman -Sy above may have brought
# another, which it might not load in ("Initialization Failed" blocks the
# installer). Then leave the page out: the step installs the page's
# default, everything on.
local built installed
built="$(cat /usr/lib/calamares/modules/packagechooserq/built-for 2>/dev/null)"
installed="$(pacman -Q cachyos-calamares-next | cut -d' ' -f2)"
local page=(-e 's/^ - netinstall$/&\n - packagechooserq@steamifypage/')
if [[ -z "$built" || "$built" != "$installed" ]]; then
echo "Steamify page left out: built for cachyos-calamares-next ${built:-?}, running $installed." >> "$log"
page=()
fi
sudo sed -i -e 's/^- id: cleanup_calamares$/- id: steamifypage\n module: packagechooserq\n config: packagechooserq_steamifypage.conf\n\n- id: steamify\n module: shellprocess\n config: shellprocess_steamify.conf\n\n&/' \
"${page[@]}" \
-e 's/^ - shellprocess@cleanup_calamares$/ - shellprocess@steamify\n&/' /etc/calamares/settings.conf
exec pkexec-wrapper calamares -D6 >> $log
}
+62
View File
@@ -0,0 +1,62 @@
#!/bin/bash
# steamify-install <target root> <user> <choice>: runs Steamify's setup for
# the new user inside the installed system (Calamares, after the users and
# services steps), as chosen on the installer's Steamify page: <choice> is the
# ids that are on, comma-separated, plus "boot" for Boot into Desktop, or
# "none" (packagechooser_steamifypage). Steamify refuses root and asks sudo
# per command, so the
# user gets a NOPASSWD rule for this run only, named to be read last: sudo
# takes the last matching rule, and a later %wheel rule would ask again. Never fails the install: a
# problem leaves the log and Steamify can be run again from the desktop.
root="$1" user="$2" choice="${3:-}"
# Not filled in by Calamares (empty, or the literal ${gs[...]}): the page's
# default, everything on.
[[ -z "$choice" || "$choice" == *'${'* ]] && choice=gaming,theme,glyphs,single,launcher,notify,vram,cec,machine,poweroff
boot=gamescope options=""
for id in ${choice//,/ }; do
case "$id" in
boot) boot=desktop ;;
none) ;;
*) options+="${options:+,}$id" ;;
esac
done
# /var/log is its own btrfs subvolume (@log) that the installer doesn't mount at $root/var/log: what is
# written there now (these logs, Steamify's) would end up hidden under the mount after boot. Mount it
# from the new system's fstab and leave it mounted: the caller reads the log afterwards, and unmounts
# all of $root at the end anyway (Calamares' umount step, the test's poweroff).
if [[ -n "$root" && -d "$root/var/log" ]] && ! mountpoint -q "$root/var/log"; then
read -r dev _ _ opts _ < <(awk '$2 == "/var/log" && $1 !~ /^#/' "$root/etc/fstab" 2>/dev/null)
[[ -n "${dev:-}" ]] && mount -o "$opts" "$dev" "$root/var/log" 2>/dev/null
fi
# systemd-boot: cachyos-installer runs `bootctl install` in a chroot, and bootctl leaves the EFI
# variables alone there: no "Linux Boot Manager" boot entry, so the firmware tries the network and
# its other entries first and finds the disk's fallback path last (or never). bootctl can't be made
# to do it from the chroot either (it writes the entry without the partition: HD(0,GPT,0000...)).
# So register it from the live system, with the ESP's real disk and partition; efibootmgr puts it first.
if [[ -n "$root" && -f "$root/boot/EFI/systemd/systemd-bootx64.efi" && -d /sys/firmware/efi/efivars ]]; then
{
esp="$(findmnt -no SOURCE "$root/boot")"
disk="/dev/$(lsblk -no PKNAME "$esp" | head -n 1)"
part="$(cat "/sys/class/block/${esp##*/}/partition")"
echo "ESP: $esp (disk $disk, partition $part)"
for n in $(efibootmgr | sed -n 's/^Boot\([0-9A-Fa-f]\{4\}\)\*\{0,1\} \(Fallback \)\{0,1\}Linux Boot Manager.*/\1/p'); do
efibootmgr -q -b "$n" -B
done
efibootmgr --create --disk "$disk" --part "$part" --label "Linux Boot Manager" --loader '\EFI\systemd\systemd-bootx64.efi'
} > "$root/var/log/steamify-bootentry.log" 2>&1
fi
log="$root/var/log/steamify-install.log"
rule="$root/etc/sudoers.d/zz-steamify-install"
[[ -d "$root" && -n "$user" ]] || exit 0
# Everything switched off on the page: nothing to set up.
[[ -n "$options" ]] || { echo "Nothing chosen on the Steamify page." > "$log"; exit 0; }
install -Dm755 /usr/local/share/steamify/steamify.sh "$root/var/tmp/steamify.sh"
printf '%s ALL=(ALL) NOPASSWD: ALL\n' "$user" > "$rule"
chmod 440 "$rule"
arch-chroot "$root" runuser -u "$user" -- env -i HOME="/home/$user" USER="$user" LOGNAME="$user" \
PATH=/usr/local/bin:/usr/bin TERM=dumb LANG=C.UTF-8 \
bash -c 'cd ~ && bash /var/tmp/steamify.sh --defaults --options "$0" --boot "$1" < /dev/null' "$options" "$boot" > "$log" 2>&1
echo "exit: $?" >> "$log"
rm -f "$rule" "$root/var/tmp/steamify.sh"
exit 0
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
# The live session in the Vapor look, with its SteamOS desktop layout.
# CachyOS's own layout comes from /etc/skel, which the live user gets before
# this runs; lookandfeeltool needs plasmashell up to replace it.
for _ in $(seq 30); do pgrep -u "$USER" -x plasmashell >/dev/null && break; sleep 1; done
sleep 2
lookandfeeltool -a com.valve.vapor.desktop --resetLayout
@@ -0,0 +1,20 @@
// The Steamify page's rows when `steamify.sh --defaults --list` can't be run
// (see calamares-online.sh, which writes the real list over this copy in
// /usr/local/share/steamify/qml at the start of the installer).
pragma Singleton
import QtQuick
QtObject {
readonly property var rows: [
{ id: "gaming", label: "SteamOS conversion", hint: "boot into gaming mode, Steam on the desktop", kind: "toggle", parent: "", on: true, selectable: true },
{ id: "boot", label: "Boot into", hint: "", kind: "choice", parent: "gaming", on: false, selectable: true },
{ id: "theme", label: "Install SteamOS theme", hint: "Vapor look (cachyos-vapor)", kind: "toggle", parent: "", on: true, selectable: true },
{ id: "glyphs", label: "Install Steam Deck/Machine icons", hint: "Deck button icons in gaming mode", kind: "toggle", parent: "", on: true, selectable: true },
{ id: "single", label: "Single user mode", hint: "no password, lock screen or log out (SDDM)", kind: "toggle", parent: "", on: true, selectable: true },
{ id: "launcher", label: "Steamify shortcut", hint: "the app on the desktop, Steamify Terminal in the launcher", kind: "toggle", parent: "", on: true, selectable: true },
{ id: "notify", label: "Update notifications", hint: "a notification when there's a new Steamify", kind: "toggle", parent: "", on: true, selectable: true },
{ id: "cec", label: "HDMI-CEC", hint: "use Steam with the TV remote, TV on/off with the PC", kind: "toggle", parent: "", on: true, selectable: true },
{ id: "machine", label: "Steam Machine support", hint: "LED bar driver, hardware settings in Steam", kind: "toggle", parent: "", on: true, selectable: true },
{ id: "poweroff", label: "Power-off fix", hint: "the Steam Machine stays off after shutting down", kind: "toggle", parent: "machine", on: true, selectable: true }
]
}
@@ -0,0 +1,200 @@
// Steamify's page in Calamares (packagechooserq@steamifypage, method legacy),
// styled like the rest of CachyOS's installer: its palette, standard
// controls, CachyOS's turquoise for the selected row. Left the options,
// right what the selected (or hovered) one does.
// The rows come from Steamify itself: calamares-online.sh writes
// ../steamify/qml/Items.qml from `steamify.sh --defaults --list` (the newest
// Steamify, else the ISO's), so new options show up without a new ISO; the
// explanations are the app's (Texts.qml, same folder).
// The choice goes to Calamares' global storage as packagechooser_steamifypage:
// the ids that are on, comma-separated, plus "boot" for Boot into Desktop
// ("none" when nothing is on); steamify-install passes it to
// `steamify.sh --defaults --options ... --boot ...`.
import io.calamares.core 1.0
import io.calamares.ui 1.0
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "../steamify/qml"
Item {
id: page
width: parent.width
height: parent.height
SystemPalette { id: pal; colorGroup: SystemPalette.Active }
readonly property color accent: "#00CED1"
Rectangle { anchors.fill: parent; color: pal.window }
// Only what can be set up here (Steamify leaves out the rest).
readonly property var rows: Items.rows.filter(function (r) { return r.selectable; })
property var want: ({})
property string boot: "gamescope"
property int sel: 0
function label(r) { return (Texts.items[r.id] || {}).label || r.label; }
function hint(r) { return (Texts.items[r.id] || {}).hint || r.hint; }
function shown(r) { return !r.parent || !!want[r.parent]; }
// Steamify's own rules: a sub-option needs its parent (turning a parent
// off turns its sub-options off); single user mode needs the conversion.
function toggle(id) {
var w = Object.assign({}, want);
w[id] = !w[id];
for (var i = 0; i < rows.length; i++) {
var r = rows[i];
if (w[id] && r.id === id && r.parent) w[r.parent] = true;
if (!w[id] && r.parent === id && r.kind === "toggle") w[r.id] = false;
if (w[id] && r.parent === id && r.kind === "toggle") w[r.id] = true; // opt-out sub-options
}
if (id === "single" && w.single) w.gaming = true;
if (id === "gaming" && !w.gaming) { w.single = false; boot = "gamescope"; }
want = w;
}
function choice() {
var on = [];
for (var i = 0; i < rows.length; i++)
if (rows[i].kind === "toggle" && want[rows[i].id]) on.push(rows[i].id);
if (want.gaming && boot === "desktop") on.push("boot");
// ", " (not ","): Calamares' packagechooser Summary step shows this
// value raw ("Install option: <value>"); steamify-install still
// parses it fine (${choice//,/ } + bash word-splitting collapses
// the extra space).
return on.length ? on.join(", ") : "none";
}
onWantChanged: config.packageChoice = choice()
onBootChanged: config.packageChoice = choice()
Component.onCompleted: {
// Preselected like Steamify's first run.
var w = {};
for (var i = 0; i < rows.length; i++) if (rows[i].kind === "toggle") w[rows[i].id] = rows[i].on;
want = w;
config.packageChoice = choice();
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 12
Label {
Layout.fillWidth: true
text: "Steamify sets up the SteamOS experience while CachyOS installs. Everything can be changed later with the Steamify app."
wrapMode: Text.WordWrap
color: pal.windowText
}
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 16
ListView {
id: list
Layout.fillHeight: true
Layout.fillWidth: true
Layout.preferredWidth: 55
clip: true
spacing: 4
boundsBehavior: Flickable.StopAtBounds
model: page.rows
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
delegate: Rectangle {
id: row
required property var modelData
required property int index
readonly property var it: modelData
readonly property bool vis: page.shown(it)
readonly property bool selected: vis && index === page.sel
width: list.width - 25
height: vis ? 52 : 0
visible: vis
radius: 4
color: pal.base
border.width: selected ? 2 : 1
border.color: selected ? page.accent : Qt.rgba(pal.windowText.r, pal.windowText.g, pal.windowText.b, 0.15)
MouseArea {
anchors.fill: parent; hoverEnabled: true
// Hovering explains a row too, like selecting it.
onEntered: page.sel = row.index
onClicked: { page.sel = row.index; if (row.it.kind === "toggle") page.toggle(row.it.id); }
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: row.it.parent ? 12 : 12
anchors.rightMargin: 12
spacing: 8
Label {
visible: !!row.it.parent
text: "└"
color: pal.text
opacity: 0.6
font.pointSize: 11
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
Label { Layout.fillWidth: true; text: page.label(row.it); font.bold: true; elide: Text.ElideRight; color: pal.text }
Label { Layout.fillWidth: true; text: page.hint(row.it); elide: Text.ElideRight; color: pal.text; opacity: 0.7; font.pointSize: 8.5 }
}
Switch {
visible: row.it.kind === "toggle"
checked: !!page.want[row.it.id]
onToggled: { page.sel = row.index; page.toggle(row.it.id); }
}
RowLayout {
visible: row.it.kind === "choice"
spacing: 4
RadioButton { text: "Gaming mode"; checked: page.boot === "gamescope"
onClicked: { page.sel = row.index; page.boot = "gamescope"; } }
RadioButton { text: "Desktop"; checked: page.boot === "desktop"
onClicked: { page.sel = row.index; page.boot = "desktop"; } }
}
}
}
}
// What the selected (or hovered) option does.
Rectangle {
id: detail
readonly property var it: page.rows[page.sel] || ({})
readonly property var tx: Texts.items[it.id] || ({})
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredWidth: 45
radius: 4
color: pal.base
border.width: 1
border.color: Qt.rgba(pal.windowText.r, pal.windowText.g, pal.windowText.b, 0.15)
onItChanged: flick.contentY = 0
Flickable {
id: flick
anchors.fill: parent
anchors.margins: 16
clip: true
contentHeight: col.implicitHeight
boundsBehavior: Flickable.StopAtBounds
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
ColumnLayout {
id: col
width: flick.width - 12
spacing: 10
Label { Layout.fillWidth: true; text: detail.it.id ? page.label(detail.it) : ""; font.bold: true; font.pointSize: 13
wrapMode: Text.WordWrap; color: pal.text }
Label { Layout.fillWidth: true; text: detail.tx.body || page.hint(detail.it) || ""; wrapMode: Text.WordWrap; color: pal.text }
Label { visible: (detail.tx.changes || []).length > 0; text: "What it changes"; font.bold: true; color: pal.text; topPadding: 4 }
Repeater {
model: detail.tx.changes || []
Label { required property string modelData; Layout.fillWidth: true; text: "• " + modelData
wrapMode: Text.WordWrap; color: pal.text }
}
}
}
}
}
}
}
+1
View File
@@ -90,6 +90,7 @@ linux-cachyos
linux-cachyos-nvidia-open
linux-cachyos-zfs
linux-firmware
cachyos-vapor
linux-firmware-marvell
lsb-release
lsof
+21 -4
View File
@@ -1,11 +1,20 @@
#!/usr/bin/env bash
# shellcheck disable=SC2034
iso_name="cachyos"
iso_label="COS_$(date --date="@${SOURCE_DATE_EPOCH:-$(date +%s)}" +%Y%m)"
iso_name="steamify-cachyos"
# The label follows the release tag, which GitHub names (iso-1-github-tag.yml): the Steamify version on the ISO
# (steamify-prepare.sh put it there) plus the tag's date and time, e.g. STEAMIFY_2_9_3_20260929_2030 (ISO 9660:
# at most 32 characters, A-Z 0-9 _). STEAMIFY_BUILD_STAMP (YYYYMMDD_HHMM, UTC) comes from the tag; the build
# reads no clock of its own. Built by hand, without a tag: LOCAL.
_steamify="$(sed -n 's/^VERSION=//p' "${BASH_SOURCE[0]%/*}/airootfs/usr/local/share/steamify/steamify.sh" 2>/dev/null | head -n 1)"
_stamp="${STEAMIFY_BUILD_STAMP:-LOCAL}"
iso_label="STEAMIFY_${_steamify:+${_steamify//./_}_}${_stamp}"
iso_label="${iso_label:0:32}"
iso_publisher="CachyOS <https://cachyos.org>"
iso_application="CachyOS Live/Rescue DVD"
iso_version="$(date --date="@${SOURCE_DATE_EPOCH:-$(date +%s)}" +%Y.%m.%d)"
iso_application="Steamify CachyOS Live (based on CachyOS)"
# The file name (<iso_name>-<iso_version>-x86_64.iso) is the release tag without its v (STEAMIFY_ISO_VERSION
# from the tag: steamify-cachyos-2.9.3-dev.2026.09.29-2030-x86_64.iso). Built by hand, without a tag: local.
iso_version="${STEAMIFY_ISO_VERSION:-local}"
install_dir="arch"
buildmodes=('iso')
## GRUB
@@ -27,6 +36,8 @@ file_permissions=(
["/root/.gnupg"]="0:0:700"
["/usr/local/bin/choose-mirror"]="0:0:755"
["/usr/local/bin/dmcheck"]="0:0:755"
["/usr/local/bin/steamify-install"]="0:0:755"
["/usr/local/bin/steamify-live-theme"]="0:0:755"
["/usr/local/bin/calamares-online.sh"]="0:0:755"
["/usr/local/bin/remove-nvidia"]="0:0:755"
["/usr/local/bin/removeun"]="0:0:755"
@@ -34,4 +45,10 @@ file_permissions=(
["/usr/local/bin/prepare-live-desktop.sh"]="0:0:755"
["/usr/local/bin/nvidia-module-loader"]="0:0:755"
["/usr/local/bin/pkexec-wrapper"]="0:0:755"
# steamify-prepare.sh writes this via curl -o, which doesn't carry the
# executable bit; calamares-online.sh runs it directly ("$sbin" --defaults
# --list) to regenerate Items.qml/items.json at boot, so without this it
# silently fails (stderr redirected) and the Steamify page/Summary step
# fall back to build-time data.
["/usr/local/share/steamify/steamify.sh"]="0:0:755"
)
+51
View File
@@ -0,0 +1,51 @@
#!/bin/bash
# build-calamares-modules.sh: builds Calamares' packagechooserq module (the
# QML page the Steamify page runs in) for the ISO. CachyOS's
# cachyos-calamares-next leaves it out of its own build, so it's built here,
# out of tree, from CachyOS's Calamares source against the installed
# cachyos-calamares-next, and installed into airootfs. Run as root in the
# build container, before buildiso.sh: the package comes from the same repos
# at the same time as the ISO's, so the module matches the ISO's Calamares.
# Records the version it was built for next to it (calamares-online.sh
# compares that with the Calamares it ends up with).
set -euo pipefail
cd "$(dirname "$0")"
dest=archiso/airootfs/usr/lib/calamares/modules/packagechooserq
src_url=https://github.com/CachyOS/cachyos-calamares/archive/refs/heads/cachyos.tar.gz
pacman -S --needed --noconfirm cachyos-calamares-next cmake extra-cmake-modules \
qt6-declarative qt6-tools qt6-svg kcoreaddons yaml-cpp base-devel patch >/dev/null
work="$(mktemp -d)"
mkdir -p "$work/src" "$work/cmake"
# The module and the packagechooser sources it shares (Config, PackageModel),
# plus the CMake helper its CMakeLists includes, which Calamares doesn't install.
curl -fsSL "$src_url" -o "$work/calamares.tar.gz"
tar -xzf "$work/calamares.tar.gz" --strip-components=3 -C "$work/src" \
cachyos-calamares-cachyos/src/modules/packagechooserq cachyos-calamares-cachyos/src/modules/packagechooser
tar -xzf "$work/calamares.tar.gz" --strip-components=2 -C "$work/cmake" \
cachyos-calamares-cachyos/CMakeModules/AppStreamHelper.cmake
# The Steamify page's Summary line shows labels instead of ids.
patch -s -d "$work/src" -p1 < patches/packagechooserq-steamify-summary.patch
# KF6CoreAddons before Calamares: Calamares' CMake config needs its target.
cat > "$work/src/CMakeLists.txt" << EOF
cmake_minimum_required(VERSION 3.16)
project(steamify_packagechooserq LANGUAGES CXX)
set(WITH_QML ON)
set(WITH_QT6 ON)
set(qtname Qt6)
set(QT_VERSION_SUFFIX -qt6)
find_package(ECM REQUIRED NO_MODULE)
set(CMAKE_MODULE_PATH \${ECM_MODULE_PATH} /usr/lib/cmake/Calamares $work/cmake \${CMAKE_MODULE_PATH})
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Qml Quick)
find_package(KF6CoreAddons REQUIRED)
find_package(Calamares REQUIRED)
add_subdirectory(packagechooserq)
EOF
cmake -S "$work/src" -B "$work/build" -DCMAKE_BUILD_TYPE=Release > "$work/cmake.log" 2>&1 ||
{ tail -30 "$work/cmake.log"; exit 1; }
make -s -C "$work/build" -j"$(nproc)"
install -Dm755 "$work/build/packagechooserq/libcalamares_viewmodule_packagechooserq.so" "$dest/libcalamares_viewmodule_packagechooserq.so"
install -Dm644 "$work/build/packagechooserq/module.desc" "$dest/module.desc"
pacman -Q cachyos-calamares-next | cut -d' ' -f2 > "$dest/built-for"
rm -rf "$work"
echo "packagechooserq built for cachyos-calamares-next $(cat "$dest/built-for")."
+32
View File
@@ -0,0 +1,32 @@
#!/bin/bash
# build-live-modules.sh: builds Steamify's power-off fix for the live ISO's
# kernels, so the Steam Machine stays off after shutting down from the live
# session (recent kernels keep a firmware wake bit set; see Steamify's
# patches/steamify-fremont-poweroff.c). Run as root in the build container,
# after steamify-prepare.sh and before buildiso.sh: the headers come from the
# same repos at the same time as the ISO's kernels, so the versions match.
# The module checks for Fremont itself and does nothing on other hardware.
set -euo pipefail
cd "$(dirname "$0")"
src=archiso/airootfs/usr/local/share/steamify/steamify-fremont-poweroff.c
[[ -f "$src" ]] || { echo "No $src: run steamify-prepare.sh first." >&2; exit 1; }
pacman -S --needed --noconfirm base-devel clang llvm lld linux-cachyos-headers linux-cachyos-lts-headers >/dev/null
for build in /usr/lib/modules/*/build; do
kver="$(basename "$(dirname "$build")")"
# A fresh folder per kernel: `make clean modules` in one call makes
# kbuild re-run itself endlessly for an external module.
work="$(mktemp -d)"
cp "$src" "$work/"
echo 'obj-m += steamify-fremont-poweroff.o' > "$work/Makefile"
# CachyOS's kernels differ in compiler (linux-cachyos is clang-built):
# build with the one the kernel was built with.
llvm=()
grep -q '^CONFIG_CC_IS_CLANG=y' "$build/.config" && llvm=(LLVM=1)
make -s -C "$build" M="$work" "${llvm[@]}" modules
install -Dm644 "$work/steamify-fremont-poweroff.ko" \
"archiso/airootfs/usr/lib/modules/$kver/extra/steamify-fremont-poweroff.ko"
rm -rf "$work"
echo "Power-off fix built for $kver."
done
install -Dm644 /dev/stdin archiso/airootfs/etc/modules-load.d/steamify-fremont-poweroff.conf <<< 'steamify-fremont-poweroff'
+5 -1
View File
@@ -76,6 +76,10 @@ for sig in TERM HUP QUIT; do
done
trap 'trap_exit INT "$(gettext "Aborted by user! Exiting...")"' INT
trap 'trap_exit USR1 "$(gettext "An unknown error has occurred. Exiting...")"' ERR
trap 'trap_exit EXIT "$(gettext "An unknown error has occurred. Exiting...")"' EXIT
# Steamify: unconditional, so it printed "An unknown error has occurred"
# (and tried to self-kill with the bogus signal name EXIT) after every
# build, success included. Only report it, and only exit through
# trap_exit's self-kill, on an actual nonzero exit.
trap 'ec=$?; if [[ $ec -ne 0 ]]; then trap_exit EXIT "$(gettext "An unknown error has occurred. Exiting...")"; else umount_fs; fi' EXIT
run_build "${build_list_iso}"
@@ -0,0 +1,81 @@
diff -ruN a/packagechooserq/PackageChooserQmlViewStep.cpp b/packagechooserq/PackageChooserQmlViewStep.cpp
--- a/packagechooserq/PackageChooserQmlViewStep.cpp 2026-09-28 16:56:21
+++ b/packagechooserq/PackageChooserQmlViewStep.cpp 2026-09-28 16:57:29
@@ -17,6 +17,12 @@
#include "utils/System.h"
#include "utils/Variant.h"
+#include <QFile>
+#include <QHash>
+#include <QJsonArray>
+#include <QJsonDocument>
+#include <QJsonObject>
+
CALAMARES_PLUGIN_FACTORY_DEFINITION( PackageChooserQmlViewStepFactory, registerPlugin< PackageChooserQmlViewStep >(); )
PackageChooserQmlViewStep::PackageChooserQmlViewStep( QObject* parent )
@@ -35,9 +41,61 @@
QString
PackageChooserQmlViewStep::prettyStatus() const
{
- //QString option = m_pkgc;
- //return tr( "Install option: %1" ).arg( option );
- return m_config->prettyStatus();
+ // Steamify page (steamify-cachyos-live-iso): its choice is Steamify's
+ // item ids ("boot" = start in the desktop, "none" = nothing on); show
+ // their labels, from the rows calamares-online.sh writes next to the
+ // page, instead of the raw ids. Any other use keeps the stock text.
+ const QString choice = m_config->packageChoice();
+ QFile rows( QStringLiteral( "/usr/local/share/steamify/qml/items.json" ) );
+ if ( moduleInstanceKey().id() != QStringLiteral( "steamifypage" ) || choice.isEmpty()
+ || !rows.open( QIODevice::ReadOnly ) )
+ {
+ return m_config->prettyStatus();
+ }
+ QHash< QString, QString > labels;
+ const auto items = QJsonDocument::fromJson( rows.readAll() ).array();
+ for ( const auto& item : items )
+ {
+ const auto row = item.toObject();
+ labels.insert( row.value( QStringLiteral( "id" ) ).toString(),
+ row.value( QStringLiteral( "label" ) ).toString() );
+ }
+ if ( labels.isEmpty() )
+ {
+ return m_config->prettyStatus();
+ }
+ QStringList on;
+ bool desktop = false, gaming = false;
+ for ( const auto& part : choice.split( QLatin1Char( ',' ) ) )
+ {
+ const QString id = part.trimmed();
+ if ( id.isEmpty() || id == QStringLiteral( "none" ) )
+ {
+ continue;
+ }
+ if ( id == QStringLiteral( "boot" ) )
+ {
+ desktop = true;
+ continue;
+ }
+ gaming = gaming || id == QStringLiteral( "gaming" );
+ on << labels.value( id, id ).toHtmlEscaped();
+ }
+ if ( gaming )
+ {
+ on << ( desktop ? tr( "Starts in the desktop" ) : tr( "Starts in gaming mode" ) );
+ }
+ if ( on.isEmpty() )
+ {
+ return tr( "Steamify: <strong>nothing</strong>" );
+ }
+ QString status = tr( "Steamify:" ) + QStringLiteral( "<ul>" );
+ for ( const auto& item : on )
+ {
+ status += QStringLiteral( "<li>" ) + item + QStringLiteral( "</li>" );
+ }
+ status += QStringLiteral( "</ul>" );
+ return status;
}
bool
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
# steamify-customize.sh <live root>: changes to the live system after its
# packages are installed (mkarchiso runs it, see modify_mkarchiso in
# util-iso.sh); files in airootfs are copied before the packages, which
# overwrite them.
set -euo pipefail
root="$1"
# Nothing yet: the live session keeps CachyOS's own name (only the installer
# says "CachyOS with Steamify", see calamares-online.sh).
exit 0
+50
View File
@@ -0,0 +1,50 @@
#!/bin/bash
# steamify-prepare.sh [steamify checkout]: puts the Steamify bundle on the
# ISO (archiso/airootfs/usr/local/share/steamify/steamify.sh), used by the
# installer's Steamify step, and what its Steamify page shows (qml/). Built from a checkout when given, else the
# newest release, or exactly release $STEAMIFY_VERSION (e.g. 2.9.2: the ISO release workflow, whose tag names
# the version). Run before buildiso.sh.
set -euo pipefail
rel="https://github.com/theupriser/steamify-cachyos/releases/${STEAMIFY_VERSION:+download/v$STEAMIFY_VERSION}"
[[ -n "${STEAMIFY_VERSION:-}" ]] || rel+="latest/download"
src_ref="${STEAMIFY_VERSION:+v$STEAMIFY_VERSION}"; src_ref="${src_ref:-main}"
dest="$(cd "$(dirname "$0")" && pwd)/archiso/airootfs/usr/local/share/steamify/steamify.sh"
mkdir -p "$(dirname "$dest")"
if [[ -n "${1:-}" ]]; then
(cd "$1" && .github/tools/bundle.sh "$dest")
else
curl -fsSL "$rel/steamify.sh" -o "$dest"
fi
# The power-off fix's source, for build-live-modules.sh (live session).
poweroff="$(dirname "$dest")/steamify-fremont-poweroff.c"
if [[ -n "${1:-}" ]]; then
cp "$1/patches/steamify-fremont-poweroff.c" "$poweroff"
else
curl -fsSL "https://raw.githubusercontent.com/theupriser/steamify-cachyos/$src_ref/patches/steamify-fremont-poweroff.c" -o "$poweroff"
fi
# The installer's Steamify page (/usr/local/share/steamify-installer/
# SteamifyPage.qml) shows the app's explanations (Theme.qml, Texts.qml, from the
# checkout, else the newest release's app) and its rows (Items.qml: this
# fallback until calamares-online.sh writes the real list).
qml="$(dirname "$dest")/qml"
rm -rf "$qml"; mkdir -p "$qml"
if [[ -n "${1:-}" ]]; then
cp "$1"/ui/qml/{Theme,Texts}.qml "$qml/"
else
curl -fsSL "$rel/steamify-app.tar.gz" |
tar -xz -C "$qml" --strip-components=2 ui/qml/Theme.qml ui/qml/Texts.qml
fi
cp "$(cd "$(dirname "$0")" && pwd)/archiso/airootfs/usr/local/share/steamify-installer/Items.qml" "$qml/"
printf '%s\n' 'singleton Theme 1.0 Theme.qml' 'singleton Texts 1.0 Texts.qml' 'singleton Items 1.0 Items.qml' > "$qml/qmldir"
grep -q -- '--defaults' "$dest" || { echo "This Steamify has no --defaults (needs 2.6.0 or newer)." >&2; exit 1; }
chmod 755 "$dest"
# Temporary: cachyos-calamares-next 3.4.2-13 is linked against Boost 1.91
# while the repos ship 1.92, so the installer doesn't start. Boost's
# libraries carry their version in the name: 1.91's sit next to 1.92's.
# Drop this once CachyOS rebuilds the installer.
boost=boost-libs-1.91.0-2-x86_64.pkg.tar.zst
libs="$(cd "$(dirname "$0")" && pwd)/archiso/airootfs"
curl -fsSL "https://archive.archlinux.org/packages/b/boost-libs/$boost" -o "/tmp/$boost"
tar -xf "/tmp/$boost" -C "$libs" --wildcards 'usr/lib/libboost_*.so.1.91.0'
rm -f "/tmp/$boost"
echo "Steamify $(grep -m1 '^VERSION=' "$dest" | cut -d= -f2) on the ISO."
+31 -6
View File
@@ -104,7 +104,14 @@ generate_version_tag() {
local _profile="$1"
local _version="$2"
if [ "$_profile" == "desktop" ]; then
echo "${_version}" > ${src_dir}/archiso/airootfs/etc/version-tag
# Steam Machine edition: CachyOS Hello won't install an ISO newer
# than CachyOS's newest release (it calls it a testing ISO), so the
# tag is that release, the one this build follows; the build's own
# date goes to /etc/steammachine-iso-build.
local _release
_release="$(curl -fsS --max-time 20 https://cachyos.org/versions.json | grep -o '"desktopISOVersion":"[0-9]*"' | grep -o '[0-9]\+')"
echo "${_release:-${_version}}" > ${src_dir}/archiso/airootfs/etc/version-tag
echo "${_version}" > ${src_dir}/archiso/airootfs/etc/steammachine-iso-build
fi
}
@@ -122,6 +129,10 @@ modify_mkarchiso() {
else
msg "mkarchiso is already patched!"
fi
# Steamify: live-system changes after the packages (they overwrite airootfs).
if ! grep -q 'steamify-customize.sh' /usr/bin/mkarchiso; then
sudo sed "s#_run_once _make_customize_airootfs#_run_once _make_customize_airootfs\n\t${src_dir}/steamify-customize.sh \"\${pacstrap_dir}\"#" -i /usr/bin/mkarchiso
fi
}
prepare_profile(){
@@ -129,7 +140,10 @@ prepare_profile(){
info "Profile: [%s]" "${profile}"
local _iso_version="$(date +%y%m%d)"
# The release tag without its v when the release workflow builds (STEAMIFY_ISO_VERSION): the boot menu
# and /etc/steammachine-iso-build name the same release as the file, label and tag (no clock of its own).
# By hand, without a tag: local.
local _iso_version="${STEAMIFY_ISO_VERSION:-local}"
change_grub_version "${_iso_version}"
# Fetch up-to-date version of CachyOS repo mirrorlist
@@ -187,11 +201,22 @@ run_build() {
[ -d "$outFolder/$_profile" ] || mkdir -p "$outFolder/$_profile"
cd ${work_dir}/archiso/
sudo mkarchiso -v -w ${work_dir} -o "$outFolder/$_profile" ${work_dir}/archiso/
sudo chown $USER $outFolder
# sudo drops the environment: the release tag's file name and label (profiledef.sh) must get through.
sudo --preserve-env=STEAMIFY_ISO_VERSION,STEAMIFY_BUILD_STAMP mkarchiso -v -w ${work_dir} -o "$outFolder/$_profile" ${work_dir}/archiso/
# $USER is empty when this runs as root (e.g. in a container): an empty
# chown failed and aborted the rest (the build was done, the exit code 1).
sudo chown "${SUDO_USER:-${USER:-$(id -un)}}" "$outFolder"
cp ${work_dir}/iso/arch/pkglist.x86_64.txt "$outFolder/$_profile/$(gen_iso_fn).pkgs.txt"
mv "$outFolder/$_profile/cachyos-$(date --date="@${SOURCE_DATE_EPOCH:-$(date +%s)}" +%Y.%m.%d)-x86_64.iso" "$outFolder/$_profile/${iso_file}"
# Steamify: profiledef.sh's iso_name (steamify-cachyos) names the ISO, so
# CachyOS's rename only applies when mkarchiso wrote cachyos-<date>.
local built="$outFolder/$_profile/cachyos-$(date --date="@${SOURCE_DATE_EPOCH:-$(date +%s)}" +%Y.%m.%d)-x86_64.iso"
if [[ -e "$built" ]]; then
mv "$built" "$outFolder/$_profile/${iso_file}"
else
built="$(ls -t "$outFolder/$_profile"/*.iso | head -1)"
iso_file="$(basename "$built")"
fi
cp ${work_dir}/iso/arch/pkglist.x86_64.txt "$outFolder/$_profile/${iso_file%.iso}.pkgs.txt"
msg "Done [Build ISO] ${iso_file}"
msg "Finished building [%s]" "${_profile}"