feat(packaging/bazzite): systemd-sysext replaces rpm-ostree layering as the primary install path
Layering is a last resort per the Bazzite docs (slows every OS update, can block upgrades until removed); a sysext never enters an rpm-ostree transaction, survives OS updates, and installs/updates with no reboot — the mechanism Fedora Atomic ships via fedora-sysexts. - build-sysext.sh wraps the built host+web RPMs into punktfunk-<V-R>-x86-64.raw: /etc payload relocated to /usr/share/punktfunk/etc (a sysext carries only /usr), the punktfunk-sysext helper embedded, ID=fedora + VERSION_ID pinned (merges on Bazzite via ID_LIKE; REFUSED after a major rebase instead of running soname-broken binaries — both behaviors validated live on Bazzite 43). SELinux labels are baked in as squashfs pseudo-xattrs from matchpathcon: unlabeled files run fine for user units but system daemons are DENIED (udev couldn't read the gamepad rule under enforcing) — validated on-glass. Refuses duplicate input package names (a stale noarch punktfunk-web next to the x86_64 one built a chimera image with the dead node launcher once). - punktfunk-sysext.sh: install/update/status/remove against per-Fedora-major feeds (…/generic/punktfunk-sysext/f43[-canary]), SHA-256-verified, applies the udev/sysctl scriptlet work + /etc copies, prints the layering-migration hint. Live-validated on the .41 Bazzite box incl. service restart + web console. - publish-sysext-feed.sh + rpm.yml: build + publish the image per matrix leg (fedver 43/44), canary feeds pruned to 6, stable release assets attached. - update-punktfunk.sh warns when the sysext shadows a layered install. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+75
-41
@@ -12,34 +12,91 @@ flagged explicitly. For the higher-level packaging rationale ("why not Flatpak",
|
||||
> NVENC, from RPM Fusion **nonfree**), `opus`, and `libei`.
|
||||
> Source: `packaging/README.md`, `packaging/rpm/punktfunk.spec`.
|
||||
|
||||
> ⚠️ **Read this first — the COPR is operator-run, not yet published.**
|
||||
> Both install paths below pull the punktfunk RPM from a COPR project named
|
||||
> `enricobuehler/punktfunk`. That COPR is a configuration the maintainer has to **create and
|
||||
> build** (see `packaging/copr/README.md` — it documents how to set it up, not a live repo URL you
|
||||
> can assume exists). If `rpm-ostree install punktfunk` 404s, the COPR hasn't been published yet,
|
||||
> and your only path is to **build the RPM yourself** (see the appendix). The guide flags every
|
||||
> command that depends on the COPR being live.
|
||||
> ⚠️ **COPR note (Path C only).** The legacy layering path's commands reference a COPR project
|
||||
> named `enricobuehler/punktfunk` that is operator-run and may not be published (see
|
||||
> `packaging/copr/README.md`); layer from the **Gitea RPM registry** instead (`../rpm/README.md`,
|
||||
> the repo file `https://git.unom.io/api/packages/unom/rpm/bazzite.repo`) — it's what CI
|
||||
> actually publishes to. Paths A (sysext) and B (bootc) don't involve the COPR at all.
|
||||
|
||||
---
|
||||
|
||||
## 1. Choose an install path
|
||||
|
||||
There are two supported paths on Bazzite, driven by different files in `packaging/`:
|
||||
There are three paths on Bazzite, driven by different files in `packaging/`:
|
||||
|
||||
| Path | Driven by | What it does | Best for |
|
||||
|---|---|---|---|
|
||||
| **A — rpm-ostree layering** | `packaging/copr/README.md` + `packaging/rpm/punktfunk.spec` | Layers the `punktfunk` RPM onto your existing Bazzite deployment with `rpm-ostree install` | One host, quick iteration |
|
||||
| **A — systemd-sysext** ✅ recommended | `packaging/bazzite/punktfunk-sysext.sh` + `build-sysext.sh` (published by `.gitea/workflows/rpm.yml`) | Overlays the host onto `/usr` as a system extension — no layering, no reboot, one-command updates | Everyone; the default |
|
||||
| **B — bootc / OCI image** | `packaging/bootc/Containerfile` | Bakes punktfunk into a `FROM bazzite-nvidia` image once; you `bootc switch` any number of hosts onto it | Fleets, reproducible appliances, no per-host drift |
|
||||
| **C — rpm-ostree layering** (legacy) | `packaging/rpm/` + the Gitea RPM registry | Layers the `punktfunk` RPM onto your deployment with `rpm-ostree install` | Only if you specifically want the RPM database to own the files |
|
||||
|
||||
**Trade-off:** Path A is a per-host package layer — simple, but each host accumulates its own
|
||||
layered-package state. Path B builds one image (RPM Fusion + the Gitea RPM repo + the host and
|
||||
**web console** + udev rule pre-installed) that you push to a registry and rebase hosts onto
|
||||
atomically — no per-host `rpm-ostree install` drift, at the cost of running a `podman build`/`push`
|
||||
pipeline. Both require the **same first-run setup** (sections 3–6); note Path B installs from the
|
||||
**Gitea RPM registry** (which carries `punktfunk-web`), whereas Path A's COPR builds host+client
|
||||
only — for the web console on Path A, layer from the Gitea registry instead (`../rpm/README.md`).
|
||||
**Why A over C:** the Bazzite docs treat layering as a last resort — every layered package makes
|
||||
every OS update slower and can **block upgrades entirely** until removed. A sysext never enters an
|
||||
rpm-ostree transaction: it merges/unmerges at runtime, survives OS updates, and updating punktfunk
|
||||
is one command with **no reboot** (layering needs one per update). It's the mechanism the Fedora
|
||||
Atomic maintainers ship via [fedora-sysexts](https://fedora-sysexts.github.io/). All paths require
|
||||
the **same first-run setup** (sections 3–6).
|
||||
|
||||
### Path A — rpm-ostree layering from the COPR
|
||||
### Path A — systemd-sysext (recommended)
|
||||
|
||||
Run on the Bazzite host:
|
||||
|
||||
```sh
|
||||
# One-time bootstrap; afterwards the tool is on PATH as `punktfunk-sysext` (it ships inside
|
||||
# the image). `--channel canary` for rolling main-branch builds instead of releases.
|
||||
curl -fsSLO https://git.unom.io/unom/punktfunk/raw/branch/main/packaging/bazzite/punktfunk-sysext.sh
|
||||
sudo bash punktfunk-sysext.sh install
|
||||
```
|
||||
|
||||
This downloads the newest image for your Fedora base (host + tray + **web console**,
|
||||
SHA-256-verified from the feed `…/packages/unom/generic/punktfunk-sysext/f<ver>[-canary]/`),
|
||||
installs it as `/var/lib/extensions/punktfunk.raw`, merges it, and immediately applies what the
|
||||
RPM scriptlets would have (udev reload, sysctl) plus the two `/etc` files a sysext can't carry
|
||||
(the gamescope-session drop-in and the tray autostart entry, staged under
|
||||
`/usr/share/punktfunk/etc/`). No reboot at any point. Day-2:
|
||||
|
||||
```sh
|
||||
sudo punktfunk-sysext update # fetch + merge the newest build (then restart the user service)
|
||||
sudo punktfunk-sysext status # merged?, installed vs latest, channel/feed
|
||||
sudo punktfunk-sysext remove # unmerge + delete; ~/.config/punktfunk is left alone
|
||||
```
|
||||
|
||||
Details worth knowing:
|
||||
|
||||
- The image embeds `ID=fedora` + `VERSION_ID` (matched through Bazzite's `ID_LIKE`), so after a
|
||||
**major Bazzite rebase** (F43 → F44) the old image is **refused** instead of merging
|
||||
soname-broken binaries — `punktfunk-sysext update` then fetches the image built for the new
|
||||
base (feeds exist per Fedora major, from the same CI matrix as the RPM groups).
|
||||
- SELinux labels are baked into the image at build time (squashfs pseudo-xattrs computed from
|
||||
the targeted policy) — without them udev couldn't read the gamepad rule under enforcing.
|
||||
Validated live on Bazzite 43.
|
||||
- **Migrating from layering (path C):** install the sysext (it shadows the layered copy at
|
||||
once), then `sudo rpm-ostree uninstall punktfunk punktfunk-web && systemctl reboot`.
|
||||
|
||||
### Path B — bootc image (`FROM bazzite-nvidia`)
|
||||
|
||||
The image is built **off-host** (on any machine with `podman`) from
|
||||
`packaging/bootc/Containerfile`, which bases on `ghcr.io/ublue-os/bazzite-nvidia:stable`
|
||||
(override with `--build-arg BASE_IMAGE=…`), enables RPM Fusion free + nonfree, adds the Gitea RPM
|
||||
repo (`--build-arg PUNKTFUNK_RPM_GROUP=…`, default `bazzite`), and installs the host **and the web
|
||||
console** (`punktfunk punktfunk-web`). It uses the Gitea registry rather than the COPR specifically
|
||||
because the registry carries `punktfunk-web` (COPR's mock chroot can't build it — no `bun`).
|
||||
|
||||
```sh
|
||||
# Build + push (run from the repo root, on your builder machine):
|
||||
podman build -t ghcr.io/<you>/bazzite-punktfunk -f packaging/bootc/Containerfile .
|
||||
podman push ghcr.io/<you>/bazzite-punktfunk
|
||||
|
||||
# On each target Bazzite host:
|
||||
sudo bootc switch ghcr.io/<you>/bazzite-punktfunk && systemctl reboot
|
||||
```
|
||||
|
||||
> ⚠️ The image installs from the **Gitea RPM registry** (group `bazzite`), so **Path B depends on
|
||||
> that registry being populated** — CI (`.gitea/workflows/rpm.yml`) publishes `punktfunk` +
|
||||
> `punktfunk-web` on every push to `main`. Packages are unsigned with GPG-signed metadata
|
||||
> (`repo_gpgcheck=1`), matching `packaging/rpm/README.md`.
|
||||
|
||||
### Path C — rpm-ostree layering (legacy)
|
||||
|
||||
Run on the Bazzite host. (Commands verbatim from `packaging/README.md`.)
|
||||
|
||||
@@ -62,7 +119,7 @@ systemctl reboot
|
||||
> The **reboot is mandatory** — `rpm-ostree install` stages a new deployment that only takes
|
||||
> effect on the next boot. This is normal atomic-distro behavior, not a punktfunk quirk.
|
||||
|
||||
#### Updating a Path-A host — `rpm-ostree upgrade` is NOT enough
|
||||
#### Updating a Path-C host — `rpm-ostree upgrade` is NOT enough
|
||||
|
||||
> ⚠️ **`rpm-ostree upgrade` will not update punktfunk on its own.** `upgrade` bumps the **base
|
||||
> image** and only re-resolves *layered* packages **when the base changes**. A Bazzite base can
|
||||
@@ -94,29 +151,6 @@ sudo bash packaging/bazzite/update-punktfunk.sh --reboot # stage + reboot now
|
||||
> `punktfunk.repo`, canary's `<next-minor>.0-0.ciN` **outranks** the stable `X.Y.Z-1` and the box
|
||||
> silently tracks canary. Enable exactly one channel — set `enabled=0` in the other repo file.
|
||||
|
||||
### Path B — bootc image (`FROM bazzite-nvidia`)
|
||||
|
||||
The image is built **off-host** (on any machine with `podman`) from
|
||||
`packaging/bootc/Containerfile`, which bases on `ghcr.io/ublue-os/bazzite-nvidia:stable`
|
||||
(override with `--build-arg BASE_IMAGE=…`), enables RPM Fusion free + nonfree, adds the Gitea RPM
|
||||
repo (`--build-arg PUNKTFUNK_RPM_GROUP=…`, default `bazzite`), and installs the host **and the web
|
||||
console** (`punktfunk punktfunk-web`). It uses the Gitea registry rather than the COPR specifically
|
||||
because the registry carries `punktfunk-web` (COPR's mock chroot can't build it — no `bun`).
|
||||
|
||||
```sh
|
||||
# Build + push (run from the repo root, on your builder machine):
|
||||
podman build -t ghcr.io/<you>/bazzite-punktfunk -f packaging/bootc/Containerfile .
|
||||
podman push ghcr.io/<you>/bazzite-punktfunk
|
||||
|
||||
# On each target Bazzite host:
|
||||
sudo bootc switch ghcr.io/<you>/bazzite-punktfunk && systemctl reboot
|
||||
```
|
||||
|
||||
> ⚠️ The image installs from the **Gitea RPM registry** (group `bazzite`), so **Path B depends on
|
||||
> that registry being populated** — CI (`.gitea/workflows/rpm.yml`) publishes `punktfunk` +
|
||||
> `punktfunk-web` on every push to `main`. Packages are unsigned with GPG-signed metadata
|
||||
> (`repo_gpgcheck=1`), matching `packaging/rpm/README.md`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Prerequisites — what Bazzite gives you vs. what you must still do
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the punktfunk systemd-sysext image for Bazzite / Fedora Atomic from the built RPMs —
|
||||
# the no-layering install path (rpm-ostree layering slows every update and can block upgrades;
|
||||
# a sysext never enters an rpm-ostree transaction). The .raw overlays /usr read-only from
|
||||
# /var/lib/extensions/, survives OS updates, and is toggled/updated without a reboot.
|
||||
#
|
||||
# Counterpart to ../arch/build-sysext.sh (which wraps a pacman package for SteamOS). This one
|
||||
# wraps the Fedora RPMs (punktfunk + punktfunk-web) and additionally:
|
||||
# * relocates the RPMs' /etc payload to /usr/share/punktfunk/etc/ (a sysext carries ONLY /usr;
|
||||
# punktfunk-sysext(8) copies these into the real /etc on install),
|
||||
# * bakes SELinux labels in as squashfs pseudo-xattrs, computed with matchpathcon from the
|
||||
# build container's targeted policy. Without them every file is unlabeled_t at runtime:
|
||||
# fine for the user session + systemd --user units (unconfined), but system daemons are
|
||||
# DENIED — udev couldn't read 60-punktfunk.rules and systemd-sysctl couldn't read the
|
||||
# sysctl drop-in (validated live on Bazzite 43, SELinux enforcing, 2026-07-04),
|
||||
# * pins compatibility via ID=fedora + VERSION_ID: merges on Bazzite/Silverblue/Aurora of the
|
||||
# SAME Fedora major (ID_LIKE matching, systemd >= 256) and is REFUSED after a major rebase
|
||||
# instead of running soname-broken binaries (`punktfunk-sysext update` then re-resolves),
|
||||
# * embeds the punktfunk-sysext helper so an installed box can update itself.
|
||||
#
|
||||
# Build in the matching Fedora container (ci/fedora*-rpm.Dockerfile) — matchpathcon needs the
|
||||
# Fedora targeted policy (libselinux-utils + selinux-policy-targeted), and the RPMs are
|
||||
# soname-coupled to their base anyway. Needs: rpm2cpio, cpio, mksquashfs (>= 4.6), matchpathcon.
|
||||
#
|
||||
# Usage:
|
||||
# bash build-sysext.sh --version-id 43 --out dist/punktfunk-0.7.1-1-x86-64.raw \
|
||||
# dist/punktfunk-0.7.1-1.fc43.x86_64.rpm dist/punktfunk-web-0.7.1-1.fc43.noarch.rpm
|
||||
#
|
||||
# The installed image MUST be named punktfunk.raw (the embedded extension-release marker is
|
||||
# extension-release.punktfunk; systemd-sysext requires marker == image name) — the feed carries
|
||||
# versioned filenames and punktfunk-sysext installs to the fixed name.
|
||||
set -euo pipefail
|
||||
|
||||
VERSION_ID="" OUT="" RPMS=()
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--version-id) VERSION_ID="${2:?}"; shift 2 ;;
|
||||
--out) OUT="${2:?}"; shift 2 ;;
|
||||
*) RPMS+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$VERSION_ID" ] || { echo "missing --version-id <fedora major, e.g. 43>" >&2; exit 1; }
|
||||
[ -n "$OUT" ] || { echo "missing --out <image.raw>" >&2; exit 1; }
|
||||
[ "${#RPMS[@]}" -gt 0 ] || { echo "no RPMs given" >&2; exit 1; }
|
||||
for tool in rpm2cpio cpio mksquashfs matchpathcon; do
|
||||
command -v "$tool" >/dev/null || { echo "missing tool: $tool" >&2; exit 1; }
|
||||
done
|
||||
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
STAGE="$(mktemp -d)"
|
||||
trap 'rm -rf "$STAGE"' EXIT
|
||||
|
||||
# SYSEXT_VERSION_ID from the punktfunk RPM (V-R without the dist tag): what
|
||||
# `punktfunk-sysext status` reports as the installed version.
|
||||
PF_VR=""
|
||||
SEEN_NAMES=" "
|
||||
for rpm in "${RPMS[@]}"; do
|
||||
[ -f "$rpm" ] || { echo "no such RPM: $rpm" >&2; exit 1; }
|
||||
name="$(rpm -qp --qf '%{NAME}' "$rpm" 2>/dev/null)"
|
||||
# Two RPMs of the same NAME (e.g. a stale noarch next to the current x86_64 from a sloppy
|
||||
# download glob) silently shadow each other's files — refuse instead of building a chimera.
|
||||
case "$SEEN_NAMES" in *" $name "*) echo "duplicate RPM name '$name' in inputs — pass exactly one RPM per package" >&2; exit 1 ;; esac
|
||||
SEEN_NAMES="$SEEN_NAMES$name "
|
||||
if [ "$name" = punktfunk ]; then
|
||||
PF_VR="$(rpm -qp --qf '%{VERSION}-%{RELEASE}' "$rpm" 2>/dev/null)"
|
||||
PF_VR="${PF_VR%.fc*}"
|
||||
fi
|
||||
rpm2cpio "$rpm" | ( cd "$STAGE" && cpio -idmu --quiet )
|
||||
done
|
||||
[ -n "$PF_VR" ] || { echo "the punktfunk (host) RPM must be among the inputs" >&2; exit 1; }
|
||||
|
||||
# A sysext carries only /usr. Relocate the RPMs' /etc payload (gamescope-session drop-in, tray
|
||||
# autostart entry) under /usr/share/punktfunk/etc/ — punktfunk-sysext copies it into /etc.
|
||||
if [ -d "$STAGE/etc" ]; then
|
||||
mkdir -p "$STAGE/usr/share/punktfunk/etc"
|
||||
cp -a "$STAGE/etc/." "$STAGE/usr/share/punktfunk/etc/"
|
||||
rm -rf "${STAGE:?}/etc"
|
||||
fi
|
||||
rm -rf "${STAGE:?}/var" # rpm ghosts etc. — nothing outside /usr may remain
|
||||
|
||||
# Self-update: the helper rides inside the image.
|
||||
install -Dm0755 "$HERE/punktfunk-sysext.sh" "$STAGE/usr/bin/punktfunk-sysext"
|
||||
|
||||
# Compatibility marker. ID=fedora matches Bazzite & friends through os-release ID_LIKE;
|
||||
# VERSION_ID makes a major-rebased host refuse the old ABI instead of merging it.
|
||||
install -d "$STAGE/usr/lib/extension-release.d"
|
||||
cat > "$STAGE/usr/lib/extension-release.d/extension-release.punktfunk" <<EOF
|
||||
ID=fedora
|
||||
VERSION_ID=$VERSION_ID
|
||||
ARCHITECTURE=x86-64
|
||||
SYSEXT_ID=punktfunk
|
||||
SYSEXT_VERSION_ID=$PF_VR
|
||||
EXTENSION_RELOAD_MANAGER=1
|
||||
EOF
|
||||
|
||||
# SELinux labels as pseudo-xattrs (see header). matchpathcon resolves each target path against
|
||||
# the targeted policy's file_contexts; <<none>> means "no specific entry" — skip those (the
|
||||
# handful of matches all resolve to real contexts for our payload).
|
||||
PSEUDO="$STAGE.pseudo"
|
||||
( cd "$STAGE" && find . -mindepth 1 \( -type f -o -type d \) -printf '/%P\n' ) | sort \
|
||||
| while IFS= read -r path; do
|
||||
ctx="$(matchpathcon -n "$path" 2>/dev/null || true)"
|
||||
case "$ctx" in ''|'<<none>>') continue ;; esac
|
||||
printf '%s x security.selinux=%s\n' "$path" "$ctx"
|
||||
done > "$PSEUDO"
|
||||
[ -s "$PSEUDO" ] || { echo "matchpathcon produced no labels — refusing to build an unlabeled image" >&2; exit 1; }
|
||||
|
||||
rm -f "$OUT"; mkdir -p "$(dirname "$OUT")"
|
||||
# -xattrs-exclude drops any security.selinux the staging fs already had (would collide with the
|
||||
# pseudo defs when building on an SELinux host); -all-root because cpio extracted as the CI uid.
|
||||
mksquashfs "$STAGE" "$OUT" -all-root -noappend -quiet \
|
||||
-xattrs-exclude '^security.selinux' -pf "$PSEUDO"
|
||||
rm -f "$PSEUDO"
|
||||
echo "built $OUT (punktfunk $PF_VR, fedora $VERSION_ID, $(du -h "$OUT" | cut -f1))"
|
||||
echo " install on the box: punktfunk-sysext install (or --from-file $OUT)"
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
# Publish a punktfunk sysext image into its feed on the Gitea generic package registry —
|
||||
# called by .gitea/workflows/rpm.yml after the RPM publish. A feed is one fixed URL
|
||||
# (…/punktfunk-sysext/<feed>/) holding versioned .raw files plus a SHA256SUMS manifest;
|
||||
# punktfunk-sysext(8) on the boxes reads SHA256SUMS to find + verify the newest image
|
||||
# (the layout is also exactly what systemd-sysupdate's url-file source expects, so a
|
||||
# .transfer feed can be added later without re-publishing anything).
|
||||
#
|
||||
# Usage: TOKEN=… [KEEP=6] bash publish-sysext-feed.sh <feed> <image.raw>
|
||||
# <feed> e.g. f43, f43-canary, f44 (Fedora major x channel)
|
||||
# KEEP newest images to keep in the feed; 0/unset-for-stable = keep all
|
||||
# Env: REGISTRY (git.unom.io), OWNER (unom), TOKEN (write:package PAT), CURL_USER (login name)
|
||||
set -euo pipefail
|
||||
|
||||
FEED="${1:?usage: publish-sysext-feed.sh <feed> <image.raw>}"
|
||||
RAW="${2:?usage: publish-sysext-feed.sh <feed> <image.raw>}"
|
||||
[ -f "$RAW" ] || { echo "no such image: $RAW" >&2; exit 1; }
|
||||
REGISTRY="${REGISTRY:-git.unom.io}"
|
||||
OWNER="${OWNER:-unom}"
|
||||
KEEP="${KEEP:-0}"
|
||||
AUTH=(--user "${CURL_USER:-enricobuehler}:${TOKEN:?TOKEN (write:package PAT) required}")
|
||||
BASE="https://$REGISTRY/api/packages/$OWNER/generic/punktfunk-sysext/$FEED"
|
||||
|
||||
FNAME="$(basename "$RAW")"
|
||||
SHA="$(sha256sum "$RAW" | cut -d' ' -f1)"
|
||||
|
||||
# Merge into the existing manifest: drop any prior line for this filename, append ours.
|
||||
SUMS="$(mktemp)"; trap 'rm -f "$SUMS"' EXIT
|
||||
curl -fsS "${AUTH[@]}" "$BASE/SHA256SUMS" 2>/dev/null | grep -v " $FNAME\$" > "$SUMS" || true
|
||||
printf '%s %s\n' "$SHA" "$FNAME" >> "$SUMS"
|
||||
|
||||
# Prune: keep only the newest $KEEP images (by version sort) in manifest + registry.
|
||||
PRUNE=()
|
||||
if [ "$KEEP" -gt 0 ]; then
|
||||
mapfile -t PRUNE < <(awk '{print $2}' "$SUMS" | sort -V | head -n -"$KEEP")
|
||||
for f in "${PRUNE[@]:-}"; do
|
||||
[ -n "$f" ] && sed -i "\| $f\$|d" "$SUMS"
|
||||
done
|
||||
fi
|
||||
|
||||
# Upload order keeps consumers consistent: image first, then the manifest referencing it,
|
||||
# then prune deletions (already absent from the manifest). Delete-before-put makes workflow
|
||||
# re-runs idempotent (the registry 409s on duplicate filenames; first-publish 404s are fine).
|
||||
curl -fsS -o /dev/null "${AUTH[@]}" -X DELETE "$BASE/$FNAME" || true
|
||||
curl -fsS -o /dev/null "${AUTH[@]}" --upload-file "$RAW" "$BASE/$FNAME"
|
||||
curl -fsS -o /dev/null "${AUTH[@]}" -X DELETE "$BASE/SHA256SUMS" || true
|
||||
curl -fsS -o /dev/null "${AUTH[@]}" --upload-file "$SUMS" "$BASE/SHA256SUMS"
|
||||
for f in "${PRUNE[@]:-}"; do
|
||||
[ -n "$f" ] && { echo "pruning $f"; curl -fsS -o /dev/null "${AUTH[@]}" -X DELETE "$BASE/$f" || true; }
|
||||
done
|
||||
echo "published $FNAME -> $BASE ($(wc -l <"$SUMS") image(s) in the feed)"
|
||||
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env bash
|
||||
# punktfunk-sysext — install/update the punktfunk host on Bazzite / Fedora Atomic as a
|
||||
# systemd-sysext, the no-layering path (rpm-ostree layering is a last resort per the Bazzite
|
||||
# docs: it slows every update and can block upgrades; a sysext never enters an rpm-ostree
|
||||
# transaction, needs no reboot, and is trivially removable).
|
||||
#
|
||||
# The image overlays /usr from /var/lib/extensions/punktfunk.raw with the host, tray and web
|
||||
# console + their udev/sysctl/systemd-user payload; the RPMs' two /etc files (gamescope
|
||||
# session drop-in, tray autostart) ride inside at /usr/share/punktfunk/etc/ and are copied
|
||||
# into the real /etc here (a sysext can only carry /usr).
|
||||
#
|
||||
# Bootstrap (the script also ships inside the image as /usr/bin/punktfunk-sysext):
|
||||
# curl -fsSLO https://git.unom.io/unom/punktfunk/raw/branch/main/packaging/bazzite/punktfunk-sysext.sh
|
||||
# sudo bash punktfunk-sysext.sh install # or: install --channel canary
|
||||
# Thereafter:
|
||||
# sudo punktfunk-sysext update | status | remove
|
||||
#
|
||||
# Feed: the Gitea generic package registry, one feed per Fedora major x channel
|
||||
# (…/punktfunk-sysext/f43/, f43-canary, f44, …), each a SHA256SUMS + versioned .raw files —
|
||||
# published by .gitea/workflows/rpm.yml from the same RPMs the (legacy) layering path uses.
|
||||
# The image pins ID=fedora + VERSION_ID, so after a major OS rebase the old image is refused
|
||||
# (not merged broken) and `punktfunk-sysext update` re-resolves against the new release.
|
||||
set -euo pipefail
|
||||
|
||||
REGISTRY="${PUNKTFUNK_SYSEXT_REGISTRY:-https://git.unom.io/api/packages/unom/generic/punktfunk-sysext}"
|
||||
CONF=/etc/punktfunk-sysext.conf
|
||||
EXT_DIR=/var/lib/extensions
|
||||
IMG="$EXT_DIR/punktfunk.raw"
|
||||
SIDECAR="$EXT_DIR/.punktfunk.version"
|
||||
MARKER=/usr/lib/extension-release.d/extension-release.punktfunk
|
||||
ETC_SRC=/usr/share/punktfunk/etc
|
||||
|
||||
usage() {
|
||||
sed -n 's/^#\( \|$\)//p' "$0" | sed -n '1,20p'
|
||||
echo "usage: punktfunk-sysext install [--channel stable|canary] [--from-file X.raw]"
|
||||
echo " punktfunk-sysext update [--from-file X.raw] | status | remove"
|
||||
exit "${1:-0}"
|
||||
}
|
||||
need_root() { [ "$(id -u)" = 0 ] || { echo "run as root (sudo)" >&2; exit 1; }; }
|
||||
|
||||
os_version_id() { . /etc/os-release; echo "${VERSION_ID%%.*}"; }
|
||||
channel() { # shellcheck disable=SC1090
|
||||
[ -f "$CONF" ] && . "$CONF"; echo "${CHANNEL:-stable}"; }
|
||||
feed_url() {
|
||||
local suffix=""
|
||||
[ "$(channel)" = canary ] && suffix="-canary"
|
||||
echo "$REGISTRY/f$(os_version_id)$suffix"
|
||||
}
|
||||
|
||||
# latest -> "VERSION FILENAME SHA256" from the feed's SHA256SUMS (highest by version sort).
|
||||
latest() {
|
||||
local feed; feed="$(feed_url)"
|
||||
curl -fsSL "$feed/SHA256SUMS" \
|
||||
| awk '$2 ~ /^punktfunk-.*-x86-64\.raw$/ { v=$2; sub(/^punktfunk-/,"",v); sub(/-x86-64\.raw$/,"",v); print v, $2, $1 }' \
|
||||
| sort -V | tail -n1
|
||||
}
|
||||
|
||||
installed_version() {
|
||||
if [ -f "$MARKER" ]; then
|
||||
sed -n 's/^SYSEXT_VERSION_ID=//p' "$MARKER"
|
||||
elif [ -f "$SIDECAR" ]; then
|
||||
cat "$SIDECAR"
|
||||
fi
|
||||
}
|
||||
merged() { [ -f "$MARKER" ]; }
|
||||
|
||||
post_merge() {
|
||||
if ! merged; then
|
||||
echo "!! image installed but NOT merged — 'systemd-sysext status' / 'journalctl -u systemd-sysext'" >&2
|
||||
echo "!! (an OS release the image doesn't match? 'punktfunk-sysext update' fetches the right one)" >&2
|
||||
return 1
|
||||
fi
|
||||
# What the RPM scriptlets would have done: pick up the uinput/uhid rule + the UDP buffer
|
||||
# sysctl now, no reboot (both also auto-apply at boot once merged — the files live in /usr/lib).
|
||||
udevadm control --reload 2>/dev/null || :
|
||||
udevadm trigger --subsystem-match=misc 2>/dev/null || :
|
||||
for f in /usr/lib/sysctl.d/99-punktfunk-net.conf /usr/lib/sysctl.d/99-punktfunk-client-net.conf; do
|
||||
[ -f "$f" ] && sysctl -q -p "$f" 2>/dev/null || :
|
||||
done
|
||||
# The /etc payload a sysext can't carry. The gamescope-session drop-in is %config(noreplace):
|
||||
# only seed it, never clobber a local edit. The tray autostart entry is not user config.
|
||||
if [ -f "$ETC_SRC/gamescope-session-plus/sessions.d/steam" ] \
|
||||
&& [ ! -e /etc/gamescope-session-plus/sessions.d/steam ]; then
|
||||
install -Dm0644 "$ETC_SRC/gamescope-session-plus/sessions.d/steam" \
|
||||
/etc/gamescope-session-plus/sessions.d/steam
|
||||
fi
|
||||
if [ -f "$ETC_SRC/xdg/autostart/io.unom.Punktfunk.Tray.desktop" ]; then
|
||||
install -Dm0644 "$ETC_SRC/xdg/autostart/io.unom.Punktfunk.Tray.desktop" \
|
||||
/etc/xdg/autostart/io.unom.Punktfunk.Tray.desktop
|
||||
fi
|
||||
}
|
||||
|
||||
# do_install VERSION FILENAME SHA256 | do_install --from-file X.raw
|
||||
do_install() {
|
||||
need_root
|
||||
mkdir -p "$EXT_DIR"
|
||||
local tmp="$EXT_DIR/.punktfunk.raw.new" ver
|
||||
if [ "$1" = --from-file ]; then
|
||||
ver="(local: $(basename "$2"))"
|
||||
cp -f "$2" "$tmp"
|
||||
else
|
||||
ver="$1"
|
||||
echo "downloading punktfunk $ver ($(channel), fedora $(os_version_id))…"
|
||||
curl -fL --progress-bar -o "$tmp" "$(feed_url)/$2"
|
||||
echo "$3 $tmp" | sha256sum -c --quiet
|
||||
fi
|
||||
mv -f "$tmp" "$IMG" # marker inside is extension-release.punktfunk — name must match
|
||||
echo "$ver" > "$SIDECAR"
|
||||
systemctl enable --now systemd-sysext.service >/dev/null 2>&1 || :
|
||||
systemd-sysext refresh
|
||||
post_merge
|
||||
echo "punktfunk $ver merged into /usr."
|
||||
}
|
||||
|
||||
layering_hint() {
|
||||
if command -v rpm-ostree >/dev/null 2>&1 \
|
||||
&& rpm-ostree status 2>/dev/null | grep -q 'LayeredPackages:.*punktfunk'; then
|
||||
cat >&2 <<'EOF'
|
||||
!! punktfunk is ALSO layered via rpm-ostree. The sysext now shadows it, but remove the
|
||||
!! layer so it stops slowing/blocking OS updates (the reason this sysext exists):
|
||||
!! sudo rpm-ostree uninstall punktfunk punktfunk-web && systemctl reboot
|
||||
EOF
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_install() {
|
||||
need_root
|
||||
local from_file=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--channel) printf 'CHANNEL=%s\n' "${2:?}" > "$CONF"; shift 2 ;;
|
||||
--from-file) from_file="${2:?}"; shift 2 ;;
|
||||
*) usage 1 ;;
|
||||
esac
|
||||
done
|
||||
if [ -n "$from_file" ]; then
|
||||
do_install --from-file "$from_file"
|
||||
else
|
||||
local l; l="$(latest)"
|
||||
[ -n "$l" ] || { echo "no image in the feed $(feed_url)" >&2; exit 1; }
|
||||
# shellcheck disable=SC2086
|
||||
do_install $l
|
||||
fi
|
||||
layering_hint
|
||||
cat <<'EOF'
|
||||
|
||||
First-run (once):
|
||||
ujust add-user-to-input-group # virtual gamepads; then log out + back in
|
||||
mkdir -p ~/.config/punktfunk
|
||||
cp /usr/share/punktfunk/host.env.bazzite ~/.config/punktfunk/host.env
|
||||
systemctl --user daemon-reload && systemctl --user enable --now punktfunk-host
|
||||
Updates: sudo punktfunk-sysext update
|
||||
EOF
|
||||
}
|
||||
|
||||
cmd_update() {
|
||||
need_root
|
||||
if [ "${1:-}" = --from-file ]; then do_install --from-file "${2:?}"; return; fi
|
||||
local cur l ver
|
||||
cur="$(installed_version)"
|
||||
l="$(latest)"
|
||||
[ -n "$l" ] || { echo "no image in the feed $(feed_url)" >&2; exit 1; }
|
||||
ver="${l%% *}"
|
||||
if [ "$ver" = "$cur" ] && merged; then
|
||||
echo "already on $cur (channel $(channel)) — nothing to do."
|
||||
return
|
||||
fi
|
||||
echo "updating: ${cur:-<none>} -> $ver"
|
||||
# shellcheck disable=SC2086
|
||||
do_install $l
|
||||
echo "restart the host to pick up the new binary: systemctl --user restart punktfunk-host"
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
echo "channel: $(channel)"
|
||||
echo "feed: $(feed_url)"
|
||||
echo "image: $([ -f "$IMG" ] && du -h "$IMG" | cut -f1 || echo '(not installed)')"
|
||||
echo "merged: $(merged && echo yes || echo no)"
|
||||
echo "installed: $(installed_version || true)"
|
||||
echo "latest: $(latest 2>/dev/null | cut -d' ' -f1 || true)"
|
||||
}
|
||||
|
||||
cmd_remove() {
|
||||
need_root
|
||||
# /etc cleanup needs the /usr payload for the unmodified-compare — do it BEFORE unmerging.
|
||||
if merged; then
|
||||
if cmp -s "$ETC_SRC/gamescope-session-plus/sessions.d/steam" \
|
||||
/etc/gamescope-session-plus/sessions.d/steam 2>/dev/null; then
|
||||
rm -f /etc/gamescope-session-plus/sessions.d/steam
|
||||
fi
|
||||
fi
|
||||
rm -f /etc/xdg/autostart/io.unom.Punktfunk.Tray.desktop
|
||||
rm -f "$IMG" "$SIDECAR" "$CONF"
|
||||
systemd-sysext refresh 2>/dev/null || :
|
||||
echo "punktfunk sysext removed (user config in ~/.config/punktfunk is untouched)."
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
install) shift; cmd_install "$@" ;;
|
||||
update) shift; cmd_update "$@" ;;
|
||||
status) shift; cmd_status ;;
|
||||
remove) shift; cmd_remove ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
@@ -23,6 +23,14 @@ if [[ $EUID -ne 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The sysext path (packaging/bazzite/punktfunk-sysext.sh) supersedes layering entirely — if the
|
||||
# box runs the sysext, it shadows any layered copy and THIS script won't change what executes.
|
||||
if [[ -f /var/lib/extensions/punktfunk.raw ]]; then
|
||||
echo "NOTE: the punktfunk sysext is installed — update with 'punktfunk-sysext update' instead." >&2
|
||||
echo " (a layered punktfunk is shadowed by the sysext; consider removing the layer:" >&2
|
||||
echo " rpm-ostree uninstall punktfunk punktfunk-web)" >&2
|
||||
fi
|
||||
|
||||
# Which punktfunk packages are actually layered right now (host, web, or both).
|
||||
mapfile -t layered < <(rpm-ostree status --json 2>/dev/null \
|
||||
| grep -oE '"punktfunk(-web)?"' | tr -d '"' | sort -u)
|
||||
|
||||
Reference in New Issue
Block a user