# punktfunk on Bazzite — packaging and install paths

The packager/ops view of the Bazzite (Fedora Atomic) target: which install path to ship, how each
one is built and updated, and the gotchas of layered/bootc hosts. **Setting a host up** — udev and
the `input` group, `host.env`, the service, the firewall, streaming the KDE desktop, verifying —
is the docs' job: follow **[docs.punktfunk.unom.io/docs/bazzite](https://docs.punktfunk.unom.io/docs/bazzite)**
(firewall specifics are on [Fedora §4](https://docs.punktfunk.unom.io/docs/fedora), same firewalld
service definitions). Nothing here duplicates those pages. For the higher-level packaging
rationale ("why not Flatpak", the build), see [`../README.md`](../README.md).

> ⚠️ **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 three paths on Bazzite, driven by different files in `packaging/`:

| Path | Driven by | What it does | Best for |
|---|---|---|---|
| **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 |

**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/).

### Path A — systemd-sysext (recommended)

Install, day-2 commands, channel switching, rollback, feed-signature refusals and the
major-rebase behavior are all on the [docs page](https://docs.punktfunk.unom.io/docs/bazzite) —
that's the walkthrough to hand a user. Packager-side facts:

- CI (`.gitea/workflows/rpm.yml`) builds the image per Fedora major and publishes it to the feed
  `…/packages/unom/generic/punktfunk-sysext/f<ver>[-canary]/` — `SHA256SUMS` plus a detached
  OpenPGP signature from `packages@unom.io` (`AF245C506F4E4763`, the RPM signing key). The public
  key is baked into `punktfunk-sysext.sh`; the script refuses a feed it can't verify
  (`PUNKTFUNK_SYSEXT_ALLOW_UNSIGNED=1` is the documented escape hatch for pre-signing feeds).
- The image embeds `ID=fedora` + `VERSION_ID` (matched through Bazzite's `ID_LIKE`), so after a
  major rebase the old image is refused instead of merging soname-broken binaries; 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.
- Install also applies what the RPM scriptlets would have (udev reload, sysctl) and seeds the two
  `/etc` files a sysext can't carry (the gamescope-session drop-in, the tray autostart entry),
  staged under `/usr/share/punktfunk/etc/`.

### 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`.)

```sh
# 1. RPM Fusion (free + nonfree) — provides the NVENC-capable ffmpeg-libs.
#    Usually already enabled on Bazzite; harmless to re-run.
rpm-ostree install \
  https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm \
  https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm

# 2. Enable the punktfunk COPR repo  ⚠️ requires the COPR to be published (see callout above)
sudo wget -O /etc/yum.repos.d/_copr_punktfunk.repo \
  https://copr.fedorainfracloud.org/coprs/enricobuehler/punktfunk/repo/fedora-$(rpm -E %fedora)/

# 3. Layer punktfunk and reboot to activate the new deployment.
rpm-ostree install punktfunk
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-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
> sit frozen for months (a pinned `:stable` tag, a paused rebase), so `rpm-ostree upgrade` keeps
> reporting *"No updates available"* and your layered `punktfunk` stays put even after new RPMs
> land in the repo. (Diagnose: `rpm-ostree status` shows the base `Version:` unchanged, while
> `dnf -q repoquery --upgrades punktfunk` lists newer builds.)

To actually pull a newer host on a static base, force rpm-ostree to re-resolve just the punktfunk
layer — remove + re-add the same names in one transaction:

```sh
sudo rpm-ostree refresh-md --force
sudo rpm-ostree update \
  --uninstall punktfunk --uninstall punktfunk-web \
  --install   punktfunk --install   punktfunk-web
systemctl reboot
```

Or just run the helper, which detects what's layered and does the above. The `punktfunk` RPM
installs it, so on a layered box it's already there — no checkout needed (it's the same command the
web console's update hint prints):

```sh
sudo /usr/share/punktfunk/update-punktfunk.sh          # stage; reboot when ready
sudo /usr/share/punktfunk/update-punktfunk.sh --reboot # stage + reboot now
```

From a repo checkout (e.g. before the first install, or to run a newer helper than the layered RPM
carries) the same script runs directly — it only shells out to `rpm-ostree`/`rpm`/`systemctl`:

```sh
sudo bash packaging/bazzite/update-punktfunk.sh --reboot
```

> **Channel gotcha:** the re-resolve picks the highest version across **every enabled**
> `/etc/yum.repos.d/punktfunk*.repo`. If `punktfunk-canary.repo` is enabled alongside the stable
> `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.

## 2. First-run setup

All of it — `input`/`punktfunk` groups, `host.env`, enabling the service, the firewall, KDE
desktop mode, verifying the first stream — lives on the
[docs page](https://docs.punktfunk.unom.io/docs/bazzite) and applies identically to all three
install paths. Don't restate it here; a fact stated twice is a fact that drifts (see
"Where facts live" in [`CONTRIBUTING.md`](../../CONTRIBUTING.md)).

## 3. Gotchas

All user-facing ones live on the docs site now — the ffmpeg-libs weak dependency (NVENC fails at
runtime without RPM Fusion's build:
[Troubleshooting](https://docs.punktfunk.unom.io/docs/troubleshooting#no-video-on-fedora-nvenc-fails-ffmpeg-libs-is-missing))
and the ds_inhibit SELinux storm with DualSense-type pads
([Troubleshooting](https://docs.punktfunk.unom.io/docs/troubleshooting#stream-lags-then-freezes-with-a-dualsense-pad-bazzite-selinux);
the `dontaudit`-vs-`allow` rationale is the header of `punktfunk-ds-inhibit.cil`).

One packager-only note: `scripts/punktfunk-host.service` (the upstream/dev unit) assumes the binary
at `%h/punktfunk/target/release/punktfunk-host`; the packaged binary is `/usr/bin/punktfunk-host`.
If `systemctl --user cat punktfunk-host` shows `ExecStart` pointing into a home dir, drop an override
(`systemctl --user edit punktfunk-host`) setting `ExecStart=/usr/bin/punktfunk-host serve`.

## Appendix — if the COPR isn't published yet

The COPR (`enricobuehler/punktfunk`) is **operator-run and may not be live**. If `rpm-ostree install
punktfunk` can't find the package, build the RPM yourself on a **Fedora** machine/toolbox (not
Debian/Ubuntu — the host links system FFmpeg/PipeWire and won't build there), per
`packaging/README.md`:

```sh
git archive --format=tar.gz --prefix=punktfunk-0.3.0/ \
  -o ~/rpmbuild/SOURCES/punktfunk-0.3.0.tar.gz HEAD    # 0.3.0 = the spec's default version
rpmbuild -ba packaging/rpm/punktfunk.spec    # needs the spec's BuildRequires + RPM Fusion
```

To publish the COPR for others (so `rpm-ostree install punktfunk` / the bootc image work), follow
`packaging/copr/README.md` — create the project, point build-from-SCM at the repo with spec path
`packaging/rpm/punktfunk.spec`, add RPM Fusion nonfree as an external repo, and select chroots
matching your Bazzite Fedora base (`rpm -E %fedora`).
