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:
@@ -35,8 +35,10 @@ jobs:
|
|||||||
include:
|
include:
|
||||||
- image: punktfunk-fedora-rpm # Fedora 43 == Bazzite base
|
- image: punktfunk-fedora-rpm # Fedora 43 == Bazzite base
|
||||||
group: bazzite
|
group: bazzite
|
||||||
|
fedver: 43
|
||||||
- image: punktfunk-fedora44-rpm # Fedora 44 == Fedora KDE spin
|
- image: punktfunk-fedora44-rpm # Fedora 44 == Fedora KDE spin
|
||||||
group: fedora-44
|
group: fedora-44
|
||||||
|
fedver: 44
|
||||||
container:
|
container:
|
||||||
image: git.unom.io/unom/${{ matrix.image }}:latest
|
image: git.unom.io/unom/${{ matrix.image }}:latest
|
||||||
timeout-minutes: 90
|
timeout-minutes: 90
|
||||||
@@ -53,6 +55,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
git config --global --add safe.directory "$PWD"
|
git config --global --add safe.directory "$PWD"
|
||||||
dnf -y install gtk4-devel libadwaita-devel SDL3-devel
|
dnf -y install gtk4-devel libadwaita-devel SDL3-devel
|
||||||
|
# sysext build (packaging/bazzite/build-sysext.sh): squashfs + SELinux labeling.
|
||||||
|
dnf -y install squashfs-tools cpio libselinux-utils selinux-policy-targeted
|
||||||
# bun builds the punktfunk-web console (--with web). Baked into the image; install it
|
# bun builds the punktfunk-web console (--with web). Baked into the image; install it
|
||||||
# here too so the job stays green against the PREVIOUS image (docker.yml bootstrap note).
|
# here too so the job stays green against the PREVIOUS image (docker.yml bootstrap note).
|
||||||
command -v bun >/dev/null || {
|
command -v bun >/dev/null || {
|
||||||
@@ -117,6 +121,27 @@ jobs:
|
|||||||
done
|
done
|
||||||
echo "published to $OWNER/rpm/$GROUP"
|
echo "published to $OWNER/rpm/$GROUP"
|
||||||
|
|
||||||
|
# The no-layering Bazzite path: wrap the just-built host + web RPMs into a systemd-sysext
|
||||||
|
# image and publish it to the per-Fedora-major feed (punktfunk-sysext/f43[-canary], …) that
|
||||||
|
# `punktfunk-sysext install|update` reads. Same RPMs, same channels — just no rpm-ostree.
|
||||||
|
- name: Build the sysext image
|
||||||
|
run: |
|
||||||
|
bash packaging/bazzite/build-sysext.sh --version-id "${{ matrix.fedver }}" \
|
||||||
|
--out "dist-sysext/punktfunk-${PF_VERSION}-${PF_RELEASE}-x86-64.raw" \
|
||||||
|
dist/punktfunk-"${PF_VERSION}-${PF_RELEASE}"*.rpm \
|
||||||
|
dist/punktfunk-web-"${PF_VERSION}-${PF_RELEASE}"*.rpm
|
||||||
|
|
||||||
|
- name: Publish the sysext feed
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
run: |
|
||||||
|
case "$GROUP" in
|
||||||
|
*-canary) FEED="f${{ matrix.fedver }}-canary"; KEEP=6 ;; # rolling: bound the pile-up
|
||||||
|
*) FEED="f${{ matrix.fedver }}"; KEEP=0 ;; # stable: keep every release
|
||||||
|
esac
|
||||||
|
KEEP=$KEEP bash packaging/bazzite/publish-sysext-feed.sh "$FEED" \
|
||||||
|
"dist-sysext/punktfunk-${PF_VERSION}-${PF_RELEASE}-x86-64.raw"
|
||||||
|
|
||||||
# On a real release, also attach the .rpms to the unified Gitea Release. Both Fedora bases
|
# On a real release, also attach the .rpms to the unified Gitea Release. Both Fedora bases
|
||||||
# (bazzite=F43, fedora-44) build the SAME filename, so suffix the asset with the base to keep
|
# (bazzite=F43, fedora-44) build the SAME filename, so suffix the asset with the base to keep
|
||||||
# both on the release; canary builds live in the `*-canary` rpm groups (no release page).
|
# both on the release; canary builds live in the `*-canary` rpm groups (no release page).
|
||||||
@@ -132,3 +157,6 @@ jobs:
|
|||||||
base="$(basename "$rpm" .rpm)"
|
base="$(basename "$rpm" .rpm)"
|
||||||
upsert_asset "$RID" "$rpm" "${base}.${{ matrix.group }}.rpm"
|
upsert_asset "$RID" "$rpm" "${base}.${{ matrix.group }}.rpm"
|
||||||
done
|
done
|
||||||
|
for raw in dist-sysext/*.raw; do
|
||||||
|
upsert_asset "$RID" "$raw" "$(basename "$raw" .raw).f${{ matrix.fedver }}.raw"
|
||||||
|
done
|
||||||
|
|||||||
@@ -24,36 +24,43 @@ mid-stream. You flip between Gaming Mode and Desktop with Bazzite's normal Steam
|
|||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
The host ships as an RPM in punktfunk's **Gitea RPM registry** (public), so a Bazzite / Fedora
|
The host installs as a **systemd system extension (sysext)** — no `rpm-ostree` layering. The
|
||||||
Atomic box layers and updates it with `rpm-ostree`. Add the repo, then layer the host plus the web
|
Bazzite docs treat layering as a last resort (layered packages slow every OS update and can block
|
||||||
console and reboot:
|
upgrades until removed); a sysext never enters an rpm-ostree transaction: it overlays `/usr`
|
||||||
|
read-only from `/var/lib/extensions/`, survives OS updates, installs and updates **without a
|
||||||
|
reboot**, and is removable in one command. This is the same mechanism the Fedora Atomic
|
||||||
|
maintainers ship via the [fedora-sysexts](https://fedora-sysexts.github.io/) project.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Add the repo. Packages are GPG-signed (gpgcheck=1, the packages@unom.io key) AND the repo
|
# One-time bootstrap (afterwards the updater is on PATH as `punktfunk-sysext`):
|
||||||
# metadata is Gitea-signed (repo_gpgcheck=1); gpgkey lists both keys so dnf imports each.
|
curl -fsSLO https://git.unom.io/unom/punktfunk/raw/branch/main/packaging/bazzite/punktfunk-sysext.sh
|
||||||
sudo tee /etc/yum.repos.d/punktfunk.repo >/dev/null <<'REPO'
|
sudo bash punktfunk-sysext.sh install # add `--channel canary` for rolling builds
|
||||||
[gitea-unom-bazzite]
|
|
||||||
name=punktfunk (unom, Bazzite)
|
|
||||||
baseurl=https://git.unom.io/api/packages/unom/rpm/bazzite
|
|
||||||
enabled=1
|
|
||||||
gpgcheck=1
|
|
||||||
repo_gpgcheck=1
|
|
||||||
gpgkey=https://git.unom.io/api/packages/unom/rpm/repository.key
|
|
||||||
https://git.unom.io/api/packages/unom/generic/punktfunk-keys/1/RPM-GPG-KEY-punktfunk
|
|
||||||
REPO
|
|
||||||
|
|
||||||
# Layer the host + the web console, then reboot into the new deployment.
|
|
||||||
# (punktfunk Recommends punktfunk-web; list it explicitly so it's pulled regardless of weak-dep
|
|
||||||
# settings — the Gitea registry carries punktfunk-web, which COPR can't build.)
|
|
||||||
rpm-ostree install punktfunk punktfunk-web
|
|
||||||
systemctl reboot
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`rpm-ostree upgrade` then tracks new builds automatically (Bazzite's auto-update timer does this
|
That downloads the newest image (host + tray + web console, SHA-256-verified over HTTPS from
|
||||||
for you). For a fully baked appliance image there's also a **bootc** Containerfile that installs
|
punktfunk's package registry), merges it, and applies the udev/sysctl setup on the spot — the
|
||||||
the same RPMs from this registry — see `packaging/bootc/` and `packaging/rpm/README.md` in the repo.
|
host is usable immediately, no reboot. From then on:
|
||||||
Building from source works too (Bazzite is Fedora Atomic underneath, and its FFmpeg builds the host
|
|
||||||
fine — same steps as [Fedora KDE](/docs/fedora-kde)), but the registry is the supported path.
|
```sh
|
||||||
|
sudo punktfunk-sysext update # fetch + merge the newest build
|
||||||
|
sudo punktfunk-sysext status # channel, installed vs latest version
|
||||||
|
sudo punktfunk-sysext remove # unmerge and delete — the box is back to stock
|
||||||
|
```
|
||||||
|
|
||||||
|
Two things to know:
|
||||||
|
|
||||||
|
- **After a Bazzite major rebase** (Fedora 43 → 44) the old image **refuses to load** rather than
|
||||||
|
run against mismatched system libraries — run `sudo punktfunk-sysext update` once and it fetches
|
||||||
|
the image built for the new base.
|
||||||
|
- **Already layering punktfunk?** Install the sysext (it shadows the layered copy immediately),
|
||||||
|
then drop the layer so it stops slowing your updates:
|
||||||
|
`sudo rpm-ostree uninstall punktfunk punktfunk-web && systemctl reboot`.
|
||||||
|
|
||||||
|
For a fully baked appliance image there's also a **bootc** Containerfile that installs the RPMs
|
||||||
|
from the registry at image-build time — see `packaging/bootc/` in the repo. Plain `rpm-ostree`
|
||||||
|
layering from the [RPM registry](https://git.unom.io/unom/-/packages) keeps working too (see
|
||||||
|
`packaging/bazzite/README.md`), but the sysext is the supported default. Building from source
|
||||||
|
also works (Bazzite is Fedora Atomic underneath — same steps as [Fedora KDE](/docs/fedora-kde)).
|
||||||
|
|
||||||
## Allow controller input
|
## Allow controller input
|
||||||
|
|
||||||
|
|||||||
+25
-8
@@ -17,13 +17,15 @@ packaging/
|
|||||||
rpm/punktfunk.spec # the RPM (builds punktfunk-host from source with cargo)
|
rpm/punktfunk.spec # the RPM (builds punktfunk-host from source with cargo)
|
||||||
bazzite/host.env # gamescope-default config for a Bazzite appliance
|
bazzite/host.env # gamescope-default config for a Bazzite appliance
|
||||||
bazzite/README.md # step-by-step Bazzite setup guide
|
bazzite/README.md # step-by-step Bazzite setup guide
|
||||||
|
bazzite/*sysext*.sh # the no-layering path: build/install/publish the systemd-sysext
|
||||||
bootc/Containerfile # bake punktfunk into a Bazzite-based atomic image
|
bootc/Containerfile # bake punktfunk into a Bazzite-based atomic image
|
||||||
copr/ # COPR build-from-SCM settings
|
copr/ # COPR build-from-SCM settings
|
||||||
```
|
```
|
||||||
|
|
||||||
The other packaging targets have their own READMEs: [`debian/`](debian/README.md) (apt),
|
The other packaging targets have their own READMEs: [`debian/`](debian/README.md) (apt),
|
||||||
[`arch/`](arch/README.md) (PKGBUILD + sysext), [`flatpak/`](flatpak/README.md) (the client),
|
[`arch/`](arch/README.md) (pacman binary repo + PKGBUILD + SteamOS sysext),
|
||||||
[`windows/`](windows/README.md) (host installer + drivers), plus `kde/` and `linux/` helpers.
|
[`flatpak/`](flatpak/README.md) (the client), [`windows/`](windows/README.md) (host installer +
|
||||||
|
drivers), plus `kde/` and `linux/` helpers.
|
||||||
|
|
||||||
## What's needed beyond base Fedora
|
## What's needed beyond base Fedora
|
||||||
|
|
||||||
@@ -38,7 +40,22 @@ On **Bazzite** the only genuinely new runtime bits are `ffmpeg-libs` (RPM Fusion
|
|||||||
`libei` — the rest of the stack is already there. The default backend is **gamescope**
|
`libei` — the rest of the stack is already there. The default backend is **gamescope**
|
||||||
(`packaging/bazzite/host.env`), which the host spawns headless per session — no desktop login.
|
(`packaging/bazzite/host.env`), which the host spawns headless per session — no desktop login.
|
||||||
|
|
||||||
## Option A — Gitea RPM registry (recommended; per-host, `rpm-ostree`)
|
## Option A — systemd-sysext (recommended; no layering, no reboot)
|
||||||
|
|
||||||
|
On Bazzite / Fedora Atomic the recommended install is the **systemd-sysext** image — rpm-ostree
|
||||||
|
layering is a last resort per the Bazzite docs (it slows every OS update and can block upgrades),
|
||||||
|
while a sysext overlays `/usr` at runtime, survives OS updates, and updates in one command with
|
||||||
|
no reboot. CI wraps the same RPMs below into the image, so content and channels are identical.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -fsSLO https://git.unom.io/unom/punktfunk/raw/branch/main/packaging/bazzite/punktfunk-sysext.sh
|
||||||
|
sudo bash punktfunk-sysext.sh install # then: sudo punktfunk-sysext update | status | remove
|
||||||
|
```
|
||||||
|
|
||||||
|
Full walkthrough (incl. the F43→F44 rebase behavior and migration off layering):
|
||||||
|
[`bazzite/README.md`](bazzite/README.md).
|
||||||
|
|
||||||
|
## Option B — Gitea RPM registry (per-host, `rpm-ostree` layering)
|
||||||
|
|
||||||
The host's RPM is published to **unom's self-hosted Gitea RPM registry** (CI builds it on every
|
The host's RPM is published to **unom's self-hosted Gitea RPM registry** (CI builds it on every
|
||||||
push), mirroring the [Debian/apt](debian/README.md) setup. Add one repo file, install, and track
|
push), mirroring the [Debian/apt](debian/README.md) setup. Add one repo file, install, and track
|
||||||
@@ -60,7 +77,7 @@ rpm-ostree install punktfunk && systemctl reboot
|
|||||||
# updates: rpm-ostree upgrade && systemctl reboot
|
# updates: rpm-ostree upgrade && systemctl reboot
|
||||||
```
|
```
|
||||||
|
|
||||||
## Option B — COPR (per-host, `rpm-ostree install`)
|
## Option C — COPR (per-host, `rpm-ostree install`)
|
||||||
|
|
||||||
1. Create a COPR project, enable **build-from-SCM** pointing at this repo, spec path
|
1. Create a COPR project, enable **build-from-SCM** pointing at this repo, spec path
|
||||||
`packaging/rpm/punktfunk.spec` (see `copr/README.md`). Under *External Repositories* add
|
`packaging/rpm/punktfunk.spec` (see `copr/README.md`). Under *External Repositories* add
|
||||||
@@ -78,7 +95,7 @@ rpm-ostree install punktfunk && systemctl reboot
|
|||||||
systemctl reboot
|
systemctl reboot
|
||||||
```
|
```
|
||||||
|
|
||||||
## Option C — bootc (image-based, atomic)
|
## Option D — bootc (image-based, atomic)
|
||||||
|
|
||||||
Layer punktfunk into a Bazzite image once, then rebase any number of hosts onto it — no
|
Layer punktfunk into a Bazzite image once, then rebase any number of hosts onto it — no
|
||||||
per-host drift. See `bootc/Containerfile`:
|
per-host drift. See `bootc/Containerfile`:
|
||||||
@@ -89,7 +106,7 @@ podman push ghcr.io/<you>/bazzite-punktfunk
|
|||||||
sudo bootc switch ghcr.io/<you>/bazzite-punktfunk && systemctl reboot
|
sudo bootc switch ghcr.io/<you>/bazzite-punktfunk && systemctl reboot
|
||||||
```
|
```
|
||||||
|
|
||||||
## First-run setup (either option)
|
## First-run setup (all options)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
ujust add-user-to-input-group # virtual gamepads need /dev/uinput (then re-login).
|
ujust add-user-to-input-group # virtual gamepads need /dev/uinput (then re-login).
|
||||||
@@ -109,8 +126,8 @@ web console at `https://<host-ip>:47992` or directly.
|
|||||||
|
|
||||||
> ⚠️ **COPR caveat:** COPR's mock chroot has no `bun`, so a COPR build produces only
|
> ⚠️ **COPR caveat:** COPR's mock chroot has no `bun`, so a COPR build produces only
|
||||||
> `punktfunk` + `punktfunk-client` — **not** `punktfunk-web`. For the console on a COPR/bootc host,
|
> `punktfunk` + `punktfunk-client` — **not** `punktfunk-web`. For the console on a COPR/bootc host,
|
||||||
> install from the **Gitea RPM registry** (Option A — it carries `punktfunk-web`), which is also why
|
> install from the **Gitea RPM registry** (Option B — it carries `punktfunk-web`; the sysext image
|
||||||
> `bootc/Containerfile` installs from there rather than COPR.
|
> includes it too), which is also why `bootc/Containerfile` installs from there rather than COPR.
|
||||||
|
|
||||||
## Why not Flatpak (for the HOST)?
|
## Why not Flatpak (for the HOST)?
|
||||||
|
|
||||||
|
|||||||
+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`.
|
> NVENC, from RPM Fusion **nonfree**), `opus`, and `libei`.
|
||||||
> Source: `packaging/README.md`, `packaging/rpm/punktfunk.spec`.
|
> Source: `packaging/README.md`, `packaging/rpm/punktfunk.spec`.
|
||||||
|
|
||||||
> ⚠️ **Read this first — the COPR is operator-run, not yet published.**
|
> ⚠️ **COPR note (Path C only).** The legacy layering path's commands reference a COPR project
|
||||||
> Both install paths below pull the punktfunk RPM from a COPR project named
|
> named `enricobuehler/punktfunk` that is operator-run and may not be published (see
|
||||||
> `enricobuehler/punktfunk`. That COPR is a configuration the maintainer has to **create and
|
> `packaging/copr/README.md`); layer from the **Gitea RPM registry** instead (`../rpm/README.md`,
|
||||||
> build** (see `packaging/copr/README.md` — it documents how to set it up, not a live repo URL you
|
> the repo file `https://git.unom.io/api/packages/unom/rpm/bazzite.repo`) — it's what CI
|
||||||
> can assume exists). If `rpm-ostree install punktfunk` 404s, the COPR hasn't been published yet,
|
> actually publishes to. Paths A (sysext) and B (bootc) don't involve the COPR at all.
|
||||||
> 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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Choose an install path
|
## 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 |
|
| 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 |
|
| **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
|
**Why A over C:** the Bazzite docs treat layering as a last resort — every layered package makes
|
||||||
layered-package state. Path B builds one image (RPM Fusion + the Gitea RPM repo + the host and
|
every OS update slower and can **block upgrades entirely** until removed. A sysext never enters an
|
||||||
**web console** + udev rule pre-installed) that you push to a registry and rebase hosts onto
|
rpm-ostree transaction: it merges/unmerges at runtime, survives OS updates, and updating punktfunk
|
||||||
atomically — no per-host `rpm-ostree install` drift, at the cost of running a `podman build`/`push`
|
is one command with **no reboot** (layering needs one per update). It's the mechanism the Fedora
|
||||||
pipeline. Both require the **same first-run setup** (sections 3–6); note Path B installs from the
|
Atomic maintainers ship via [fedora-sysexts](https://fedora-sysexts.github.io/). All paths require
|
||||||
**Gitea RPM registry** (which carries `punktfunk-web`), whereas Path A's COPR builds host+client
|
the **same first-run setup** (sections 3–6).
|
||||||
only — for the web console on Path A, layer from the Gitea registry instead (`../rpm/README.md`).
|
|
||||||
|
|
||||||
### 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`.)
|
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
|
> 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.
|
> 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
|
> ⚠️ **`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
|
> 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
|
> `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.
|
> 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
|
## 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
|
exit 1
|
||||||
fi
|
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).
|
# Which punktfunk packages are actually layered right now (host, web, or both).
|
||||||
mapfile -t layered < <(rpm-ostree status --json 2>/dev/null \
|
mapfile -t layered < <(rpm-ostree status --json 2>/dev/null \
|
||||||
| grep -oE '"punktfunk(-web)?"' | tr -d '"' | sort -u)
|
| grep -oE '"punktfunk(-web)?"' | tr -d '"' | sort -u)
|
||||||
|
|||||||
Reference in New Issue
Block a user