feat(packaging): bundle the web console into the RPM / Arch / bootc host packages
ci / rust (push) Successful in 1m13s
android / android (push) Failing after 1m42s
ci / web (push) Successful in 27s
ci / bench (push) Successful in 1m50s
decky / build-publish (push) Successful in 11s
deb / build-publish (push) Failing after 2m38s
apple / swift (push) Successful in 54s
ci / docs-site (push) Successful in 32s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 2m57s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
flatpak / build-publish (push) Failing after 2s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 2m33s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m20s
docker / deploy-docs (push) Successful in 17s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 5m11s

The punktfunk-web management console (pairing + status) shipped only via apt. Extend it
to the other HOST packaging methods, mirroring the Debian punktfunk-web .deb (flatpak is
the client, correctly excluded):

- rpm/punktfunk.spec: new noarch `punktfunk-web` subpackage (the .output bundle + a
  /usr/bin/punktfunk-web-server node launcher + both systemd --user units + web-init.sh +
  web.env.example), gated behind `%bcond_with web`. OFF by default because building the
  Nitro/Node SSR bundle needs `bun`, which a plain rpmbuild / COPR mock chroot lacks. Host
  package weak-Recommends punktfunk-web.
- ci/fedora-rpm.Dockerfile: install bun (+ unzip) so the CI builder can build the console.
- rpm.yml: build `PF_WITH_WEB=1` (Prep bootstraps bun to stay green pre-image-rebuild); the
  publish loop already globs the new noarch rpm into the registry. build-rpm.sh: `--with web`
  when PF_WITH_WEB=1.
- bootc/Containerfile: install from the Gitea RPM registry (which carries punktfunk-web)
  instead of COPR — `dnf5 install punktfunk punktfunk-web`.
- arch/PKGBUILD: opt-in `punktfunk-web` split member (PF_WITH_WEB=1 appends it + bun) so a
  default makepkg still builds host+client with no JS tooling — matching the spec's bcond.
- docs: packaging/README, rpm/README, copr/README (the no-bun caveat), bazzite/README
  (Path B rewritten COPR→Gitea registry), arch/README — enable + journal-password steps.

Reviewed across methods by an adversarial multi-agent pass (rpm/ci/arch/bootc/consistency
lenses, each blocking finding 3x-verified); fixed the two it confirmed real — the Arch
bun-mandatory regression (now opt-in) and the stale COPR wording in bazzite Path B.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 09:56:58 +00:00
parent 3167c936c0
commit 802e98d3a3
11 changed files with 229 additions and 29 deletions
+11 -1
View File
@@ -50,6 +50,14 @@ 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
# 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).
command -v bun >/dev/null || {
dnf -y install unzip
curl -fsSL https://bun.sh/install | bash
install -m0755 "$HOME/.bun/bin/bun" /usr/local/bin/bun
}
bun --version
- uses: actions/cache@v4 - uses: actions/cache@v4
with: with:
path: /usr/local/cargo/registry path: /usr/local/cargo/registry
@@ -71,7 +79,9 @@ jobs:
echo "rpm $V-$R" echo "rpm $V-$R"
- name: Build RPM - name: Build RPM
run: PF_VERSION="$PF_VERSION" PF_RELEASE="$PF_RELEASE" bash packaging/rpm/build-rpm.sh # PF_WITH_WEB=1 → also build the noarch punktfunk-web subpackage (the publish loop below
# globs it in; the host RPM Recommends it). Needs bun (ensured in Prep).
run: PF_VERSION="$PF_VERSION" PF_RELEASE="$PF_RELEASE" PF_WITH_WEB=1 bash packaging/rpm/build-rpm.sh
- name: Publish to the Gitea RPM registry - name: Publish to the Gitea RPM registry
env: env:
+9 -1
View File
@@ -17,7 +17,8 @@ RUN dnf -y install \
"https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm" \ "https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm" \
&& dnf -y install \ && dnf -y install \
# rpmbuild + source-tarball tooling; nodejs runs the Gitea Actions JS (checkout/cache) # rpmbuild + source-tarball tooling; nodejs runs the Gitea Actions JS (checkout/cache)
rpm-build rpmdevtools systemd-rpm-macros git tar gzip nodejs \ # AND the punktfunk-web .output at runtime; unzip is for the bun installer below.
rpm-build rpmdevtools systemd-rpm-macros git tar gzip nodejs unzip \
# build toolchain + bindgen # build toolchain + bindgen
gcc gcc-c++ clang clang-devel cmake nasm pkgconf-pkg-config curl ca-certificates \ gcc gcc-c++ clang clang-devel cmake nasm pkgconf-pkg-config curl ca-certificates \
# ffmpeg (NVENC), capture/audio/display link deps # ffmpeg (NVENC), capture/audio/display link deps
@@ -27,6 +28,13 @@ RUN dnf -y install \
gtk4-devel libadwaita-devel SDL3-devel \ gtk4-devel libadwaita-devel SDL3-devel \
&& dnf clean all && dnf clean all
# bun — the build tool for the punktfunk-web console (`bun run build` -> the node-server .output
# the punktfunk-web RPM ships and runs with plain node). Not in Fedora repos; install the official
# standalone binary to a system PATH dir so the rpmbuild `%build` (run as any uid) finds it.
RUN curl -fsSL https://bun.sh/install | bash \
&& install -m0755 /root/.bun/bin/bun /usr/local/bin/bun \
&& bun --version
# libcuda link stub — the zerocopy path links a fixed set of cuXxx driver symbols, but CI has # libcuda link stub — the zerocopy path links a fixed set of cuXxx driver symbols, but CI has
# no GPU and never RUNS CUDA. Rather than drag in the NVIDIA userspace stack, synthesize a stub # no GPU and never RUNS CUDA. Rather than drag in the NVIDIA userspace stack, synthesize a stub
# libcuda.so.1 that just defines those symbols (the SAME approach the Ubuntu image takes with the # libcuda.so.1 that just defines those symbols (the SAME approach the Ubuntu image takes with the
+12 -1
View File
@@ -91,9 +91,20 @@ ujust add-user-to-input-group # virtual gamepads need /dev/uinput (the
mkdir -p ~/.config/punktfunk mkdir -p ~/.config/punktfunk
cp /usr/share/punktfunk/host.env.bazzite ~/.config/punktfunk/host.env # edit (gamescope app, etc.) cp /usr/share/punktfunk/host.env.bazzite ~/.config/punktfunk/host.env # edit (gamescope app, etc.)
systemctl --user enable --now punktfunk-host systemctl --user enable --now punktfunk-host
# Management web console (pairing + status) — pulled in by default (the host RPM Recommends it;
# `--no-install-recommends` / headless-only boxes can skip it). Enable it and read the login password:
systemctl --user enable --now punktfunk-web
journalctl --user -u punktfunk-web-init | sed -n 's/.*password generated: //p' # then open http://<host-ip>:3000
``` ```
Pair a stock Moonlight client (mDNS-discovered), or connect the native punktfunk/1 client. Pair a stock Moonlight client (mDNS-discovered), or connect the native punktfunk/1 client — via the
web console at `http://<host-ip>:3000` or directly.
> ⚠️ **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,
> install from the **Gitea RPM registry** (Option A — it carries `punktfunk-web`), which is also why
> `bootc/Containerfile` installs from there rather than COPR.
## Why not Flatpak (for the HOST)? ## Why not Flatpak (for the HOST)?
+41 -1
View File
@@ -14,6 +14,10 @@
# NVIDIA hosts; an AMD Deck-as-HOST needs a VAAPI backend first. The CLIENT decodes via VAAPI # NVIDIA hosts; an AMD Deck-as-HOST needs a VAAPI backend first. The CLIENT decodes via VAAPI
# (AMD/Intel, incl. the Deck) with a software fallback, so it works everywhere. See README.md. # (AMD/Intel, incl. the Deck) with a software fallback, so it works everywhere. See README.md.
pkgbase=punktfunk pkgbase=punktfunk
# punktfunk-web (the browser console) is OPT-IN: building it needs `bun` (AUR-only as bun-bin on
# stock Arch/SteamOS), so a default makepkg builds only host+client with no JS tooling — mirroring
# the RPM spec's `%bcond_with web` (off by default). Set PF_WITH_WEB=1 to also build punktfunk-web
# (appended to pkgname + bun to makedepends below).
pkgname=('punktfunk-host' 'punktfunk-client') pkgname=('punktfunk-host' 'punktfunk-client')
pkgver=0.0.1 pkgver=0.0.1
pkgrel=1 pkgrel=1
@@ -26,6 +30,12 @@ license=('MIT OR Apache-2.0')
makedepends=('rust' 'cargo' 'clang' 'cmake' 'nasm' 'pkgconf' 'git' makedepends=('rust' 'cargo' 'clang' 'cmake' 'nasm' 'pkgconf' 'git'
'gtk4' 'libadwaita' 'sdl3' 'ffmpeg' 'pipewire' 'wayland' 'libxkbcommon' 'opus' 'libei') 'gtk4' 'libadwaita' 'sdl3' 'ffmpeg' 'pipewire' 'wayland' 'libxkbcommon' 'opus' 'libei')
# Opt-in punktfunk-web: only then is bun (build tool; the console runs on plain nodejs) required.
if [ "${PF_WITH_WEB:-0}" = 1 ]; then
pkgname+=('punktfunk-web')
makedepends+=('bun') # `bun-bin` from the AUR if bun isn't in your configured repos
fi
# AUR source (a tagged release). For an in-tree CI build, set PF_SRCDIR to the repo root and # AUR source (a tagged release). For an in-tree CI build, set PF_SRCDIR to the repo root and
# build() uses it instead; see the README. # build() uses it instead; see the README.
source=("git+https://git.unom.io/unom/punktfunk.git#tag=v${pkgver}") source=("git+https://git.unom.io/unom/punktfunk.git#tag=v${pkgver}")
@@ -40,6 +50,10 @@ build() {
# NVIDIA builder. On a GPU-less builder symlink the CUDA stub into the link path first (same # NVIDIA builder. On a GPU-less builder symlink the CUDA stub into the link path first (same
# caveat the RPM documents): ln -s "$(find / -name libcuda.so -path '*stubs*'|head -1)" /usr/lib/ # caveat the RPM documents): ln -s "$(find / -name libcuda.so -path '*stubs*'|head -1)" /usr/lib/
cargo build --release --locked -p punktfunk-host -p punktfunk-client-linux cargo build --release --locked -p punktfunk-host -p punktfunk-client-linux
# Management web console (opt-in): the node-server .output bundle (built with bun, run with node).
if [ "${PF_WITH_WEB:-0}" = 1 ]; then
( cd web && bun install --frozen-lockfile && bun run build )
fi
} }
package_punktfunk-host() { package_punktfunk-host() {
@@ -54,7 +68,8 @@ package_punktfunk-host() {
'mutter: stream a GNOME desktop (Mutter RecordVirtual backend)' 'mutter: stream a GNOME desktop (Mutter RecordVirtual backend)'
'sway: stream a wlroots desktop (Sway VirtualDisplay backend)' 'sway: stream a wlroots desktop (Sway VirtualDisplay backend)'
'xdg-desktop-portal-kde: portal for the headless KDE session helper' 'xdg-desktop-portal-kde: portal for the headless KDE session helper'
'xdg-desktop-portal-wlr: portal for the headless Sway session helper') 'xdg-desktop-portal-wlr: portal for the headless Sway session helper'
'punktfunk-web: browser management console (device pairing + status)')
install=punktfunk-host.install install=punktfunk-host.install
local R; R="$(_repo)"; local T="$srcdir/target/release" local R; R="$(_repo)"; local T="$srcdir/target/release"
@@ -107,3 +122,28 @@ package_punktfunk-client() {
install -Dm0644 "$R/LICENSE-MIT" "$pkgdir/usr/share/licenses/punktfunk-client/LICENSE-MIT" install -Dm0644 "$R/LICENSE-MIT" "$pkgdir/usr/share/licenses/punktfunk-client/LICENSE-MIT"
install -Dm0644 "$R/LICENSE-APACHE" "$pkgdir/usr/share/licenses/punktfunk-client/LICENSE-APACHE" install -Dm0644 "$R/LICENSE-APACHE" "$pkgdir/usr/share/licenses/punktfunk-client/LICENSE-APACHE"
} }
package_punktfunk-web() {
pkgdesc="punktfunk management web console (Nitro/Node SSR) — pairing + status in the browser"
arch=('any')
# Runtime is plain node (the .output is portable JS — bun was only the build tool). Auto-wired to
# the host's mgmt token via the systemd --user units; enable with `systemctl --user enable --now punktfunk-web`.
depends=('nodejs')
local R; R="$(_repo)"
# Pre-built node-server bundle (from build()) + a PATH-stable launcher (matches the .deb/.rpm).
install -d "$pkgdir/usr/share/punktfunk-web/.output"
cp -r "$R/web/.output/server" "$pkgdir/usr/share/punktfunk-web/.output/server"
cp -r "$R/web/.output/public" "$pkgdir/usr/share/punktfunk-web/.output/public"
install -d "$pkgdir/usr/bin"
printf '%s\n' '#!/bin/sh' 'exec /usr/bin/node /usr/share/punktfunk-web/.output/server/index.mjs "$@"' \
> "$pkgdir/usr/bin/punktfunk-web-server"
chmod 0755 "$pkgdir/usr/bin/punktfunk-web-server"
# systemd USER units: the console runs per-user; web-init generates the login password on first start.
install -Dm0644 "$R/scripts/punktfunk-web.service" "$pkgdir/usr/lib/systemd/user/punktfunk-web.service"
install -Dm0644 "$R/scripts/punktfunk-web-init.service" "$pkgdir/usr/lib/systemd/user/punktfunk-web-init.service"
install -Dm0755 "$R/scripts/web-init.sh" "$pkgdir/usr/share/punktfunk-web/web-init.sh"
install -Dm0644 "$R/web/web.env.example" "$pkgdir/usr/share/punktfunk-web/web.env.example"
install -Dm0644 "$R/LICENSE-MIT" "$pkgdir/usr/share/licenses/punktfunk-web/LICENSE-MIT"
install -Dm0644 "$R/LICENSE-APACHE" "$pkgdir/usr/share/licenses/punktfunk-web/LICENSE-APACHE"
}
+12 -2
View File
@@ -1,12 +1,17 @@
# punktfunk on Arch Linux / SteamOS # punktfunk on Arch Linux / SteamOS
Packaging for punktfunk on Arch and Arch-derived immutable distros (SteamOS 3, etc.). The Packaging for punktfunk on Arch and Arch-derived immutable distros (SteamOS 3, etc.). The
`PKGBUILD` is a **split package** producing both **`punktfunk-host`** (the gaming-rig host) and `PKGBUILD` is a **split package** producing **`punktfunk-host`** (the gaming-rig host) and
**`punktfunk-client`** (the GTK4 couch/Deck client) — mirrors the rpm subpackages **`punktfunk-client`** (the GTK4 couch/Deck client) — mirrors the rpm subpackages
(`packaging/rpm/punktfunk.spec`) and the two deb build scripts. On a **Steam Deck you want (`packaging/rpm/punktfunk.spec`) and the deb build scripts. On a **Steam Deck you want
`punktfunk-client`** (it's what the [Decky plugin](../../clients/decky/) launches); on a gaming `punktfunk-client`** (it's what the [Decky plugin](../../clients/decky/) launches); on a gaming
rig, `punktfunk-host`. rig, `punktfunk-host`.
A third member, **`punktfunk-web`** (the browser management console — pairing + status), is
**opt-in**: build it by setting `PF_WITH_WEB=1`, which requires **`bun`** at build time (`bun-bin`
from the AUR if it isn't in your repos; the console then runs on plain `nodejs`). A default
`makepkg` builds only host+client with no JS tooling — mirroring the RPM spec's `%bcond_with web`.
> ⚠️ **Host encode is NVENC-only today.** `crates/punktfunk-host/src/encode/linux.rs` implements > ⚠️ **Host encode is NVENC-only today.** `crates/punktfunk-host/src/encode/linux.rs` implements
> `hevc_nvenc`/`av1_nvenc`/`h264_nvenc` + a CUDA zero-copy path — there is **no VAAPI encoder**. So > `hevc_nvenc`/`av1_nvenc`/`h264_nvenc` + a CUDA zero-copy path — there is **no VAAPI encoder**. So
> `punktfunk-host` works on **Arch + NVIDIA** (incl. `bazzite-deck-nvidia`); an **AMD Deck-as-host** > `punktfunk-host` works on **Arch + NVIDIA** (incl. `bazzite-deck-nvidia`); an **AMD Deck-as-host**
@@ -22,6 +27,8 @@ cd packaging/arch
PF_SRCDIR="$(git rev-parse --show-toplevel)" makepkg -f --holdver PF_SRCDIR="$(git rev-parse --show-toplevel)" makepkg -f --holdver
# …or build the tagged release the AUR way: # …or build the tagged release the AUR way:
makepkg -si makepkg -si
# …add the web console too (needs bun / bun-bin):
PF_WITH_WEB=1 PF_SRCDIR="$(git rev-parse --show-toplevel)" makepkg -f --holdver
``` ```
Then the standard first-run (printed by the install scriptlet): Then the standard first-run (printed by the install scriptlet):
```sh ```sh
@@ -29,6 +36,9 @@ sudo usermod -aG input "$USER" # virtual gamepads; re-login after
mkdir -p ~/.config/punktfunk mkdir -p ~/.config/punktfunk
cp /usr/share/punktfunk/host.env.bazzite ~/.config/punktfunk/host.env # gamescope backend cp /usr/share/punktfunk/host.env.bazzite ~/.config/punktfunk/host.env # gamescope backend
systemctl --user enable --now punktfunk-host systemctl --user enable --now punktfunk-host
# Web console (if you installed the punktfunk-web package): enable it + read the login password.
systemctl --user enable --now punktfunk-web
journalctl --user -u punktfunk-web-init | sed -n 's/.*password generated: //p' # open http://<host-ip>:3000
``` ```
NVENC/EGL come from the NVIDIA driver: `sudo pacman -S --needed nvidia-utils`. Arch's stock NVENC/EGL come from the NVIDIA driver: `sudo pacman -S --needed nvidia-utils`. Arch's stock
`ffmpeg` already has NVENC built in — no RPM-Fusion-style swap needed (unlike Fedora). `ffmpeg` already has NVENC built in — no RPM-Fusion-style swap needed (unlike Fedora).
+18 -9
View File
@@ -32,10 +32,12 @@ There are two supported paths on Bazzite, driven by different files in `packagin
| **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 |
**Trade-off:** Path A is a per-host package layer — simple, but each host accumulates its own **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 + COPR + the package + udev rule layered-package state. Path B builds one image (RPM Fusion + the Gitea RPM repo + the host and
pre-installed) that you push to a registry and rebase hosts onto atomically — no per-host **web console** + udev rule pre-installed) that you push to a registry and rebase hosts onto
`rpm-ostree install` drift, at the cost of running a `podman build`/`push` pipeline. Both atomically — no per-host `rpm-ostree install` drift, at the cost of running a `podman build`/`push`
ultimately install the **same RPM** and require the **same first-run setup** (sections 36). pipeline. Both require the **same first-run setup** (sections 36); 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`).
### Path A — rpm-ostree layering from the COPR ### Path A — rpm-ostree layering from the COPR
@@ -64,8 +66,10 @@ systemctl reboot
The image is built **off-host** (on any machine with `podman`) from 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` `packaging/bootc/Containerfile`, which bases on `ghcr.io/ublue-os/bazzite-nvidia:stable`
(override with `--build-arg BASE_IMAGE=…`), enables RPM Fusion free + nonfree, enables the COPR (override with `--build-arg BASE_IMAGE=…`), enables RPM Fusion free + nonfree, adds the Gitea RPM
(`--build-arg PUNKTFUNK_COPR=…`, default `enricobuehler/punktfunk`), and installs the package. 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 ```sh
# Build + push (run from the repo root, on your builder machine): # Build + push (run from the repo root, on your builder machine):
@@ -76,9 +80,10 @@ 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
``` ```
> ⚠️ The image build runs `dnf5 copr enable enricobuehler/punktfunk` — so **Path B also depends on > ⚠️ The image installs from the **Gitea RPM registry** (group `bazzite`), so **Path B depends on
> the COPR being published** (or on you pointing `PUNKTFUNK_COPR` at a COPR you've built yourself). > that registry being populated** — CI (`.gitea/workflows/rpm.yml`) publishes `punktfunk` +
> If the COPR doesn't exist, the `podman build` fails at the install step. > `punktfunk-web` on every push to `main`. Packages are unsigned with GPG-signed metadata
> (`repo_gpgcheck=1`), matching `packaging/rpm/README.md`.
--- ---
@@ -215,6 +220,10 @@ into the user unit directory.
```sh ```sh
systemctl --user daemon-reload systemctl --user daemon-reload
systemctl --user enable --now punktfunk-host systemctl --user enable --now punktfunk-host
# Management web console (pairing + status), if you installed punktfunk-web (it ships in the Gitea
# RPM registry / bootc image — COPR can't build it; see ../rpm/README.md). Read the login password:
systemctl --user enable --now punktfunk-web
journalctl --user -u punktfunk-web-init | sed -n 's/.*password generated: //p' # then open http://<host-ip>:3000
``` ```
Check health and logs: Check health and logs:
+19 -10
View File
@@ -15,8 +15,12 @@
ARG BASE_IMAGE=ghcr.io/ublue-os/bazzite-nvidia:stable ARG BASE_IMAGE=ghcr.io/ublue-os/bazzite-nvidia:stable
FROM ${BASE_IMAGE} FROM ${BASE_IMAGE}
# COPR project that hosts the punktfunk RPM (see packaging/copr/README). Override at build. # punktfunk's RPMs come from unom's Gitea RPM registry (the recommended path — see
ARG PUNKTFUNK_COPR=enricobuehler/punktfunk # packaging/rpm/README). Use it rather than COPR specifically because it carries the
# punktfunk-web management console subpackage, which COPR's mock chroot can't build (no `bun`).
# Group "bazzite" == the Fedora 43 base; override for a different base. Gitea signs the repo
# metadata (repo_gpgcheck=1); the packages themselves are unsigned (gpgcheck=0).
ARG PUNKTFUNK_RPM_GROUP=bazzite
# RPM Fusion nonfree provides the NVENC-capable ffmpeg-libs punktfunk records/encodes with. # RPM Fusion nonfree provides the NVENC-capable ffmpeg-libs punktfunk records/encodes with.
# (Bazzite usually has RPM Fusion enabled already; this is belt-and-suspenders.) # (Bazzite usually has RPM Fusion enabled already; this is belt-and-suspenders.)
@@ -25,15 +29,20 @@ RUN dnf5 -y install \
https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm \ https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm \
|| true || true
# Enable our COPR and install punktfunk. # Add the Gitea RPM repo and install the host + the web console (punktfunk-web pulls nodejs).
RUN dnf5 -y copr enable ${PUNKTFUNK_COPR} && \ RUN printf '%s\n' \
dnf5 -y install punktfunk && \ '[gitea-unom-punktfunk]' \
dnf5 -y copr disable ${PUNKTFUNK_COPR} && \ 'name=punktfunk (unom)' \
dnf5 clean all "baseurl=https://git.unom.io/api/packages/unom/rpm/${PUNKTFUNK_RPM_GROUP}" \
'enabled=1' 'gpgcheck=0' 'repo_gpgcheck=1' \
'gpgkey=https://git.unom.io/api/packages/unom/rpm/repository.key' \
> /etc/yum.repos.d/punktfunk.repo \
&& dnf5 -y install punktfunk punktfunk-web \
&& dnf5 clean all
# The udev rule + systemd *user* unit ship in the RPM; nothing else to enable at image # The udev rule + systemd *user* units ship in the RPMs; nothing else to enable at image build
# build time (the host runs per-user in the graphical session, enabled with # time (host + console run per-user in the graphical session, enabled after first boot with
# `systemctl --user enable --now punktfunk-host` after first boot). # `systemctl --user enable --now punktfunk-host punktfunk-web`).
# bootc image hygiene: the container build must leave a clean ostree commit. # bootc image hygiene: the container build must leave a clean ostree commit.
RUN ostree container commit RUN ostree container commit
+13
View File
@@ -33,3 +33,16 @@ copr-cli buildscm punktfunk \
Note: COPR caps build time/RAM; a full `cargo build --release` of the host (FFmpeg/PipeWire Note: COPR caps build time/RAM; a full `cargo build --release` of the host (FFmpeg/PipeWire
sys-crates + aws-lc-rs) is heavy but within the default COPR limits. If a chroot OOMs, lower sys-crates + aws-lc-rs) is heavy but within the default COPR limits. If a chroot OOMs, lower
parallelism with `CARGO_BUILD_JOBS` in the spec's `%build`. parallelism with `CARGO_BUILD_JOBS` in the spec's `%build`.
## The web console subpackage (`punktfunk-web`)
The spec can also build the management web console as a noarch `punktfunk-web` subpackage, but it's
gated behind `%bcond_with web` and **OFF by default** — building the Nitro/Node SSR bundle needs
`bun`, which COPR's mock chroot does not provide. So a stock COPR build produces only `punktfunk`
+ `punktfunk-client`.
Two ways to get the console:
- **Recommended:** install it from the Gitea RPM registry (`packaging/rpm/README.md`, Option A),
whose CI builder image has `bun` and builds `--with web`. (This is what `bootc/Containerfile` does.)
- **In COPR:** add `bun` to the chroot (a custom mock config / external repo) and set the build
option `--with web` on the project, then `dnf install punktfunk-web`.
+10 -3
View File
@@ -29,8 +29,10 @@ repo_gpgcheck=1
gpgkey=https://git.unom.io/api/packages/unom/rpm/repository.key gpgkey=https://git.unom.io/api/packages/unom/rpm/repository.key
REPO REPO
# Layer the package, then reboot into the new deployment. # Layer the host + the web console (pairing/status), then reboot into the new deployment.
rpm-ostree install punktfunk # (punktfunk Recommends punktfunk-web; list it explicitly so it's pulled regardless of weak-dep
# settings. The registry carries punktfunk-web because CI builds the spec --with web; COPR can't.)
rpm-ostree install punktfunk punktfunk-web
systemctl reboot systemctl reboot
``` ```
@@ -46,6 +48,9 @@ ujust add-user-to-input-group # virtual gamepads need /dev/uinput (re-
mkdir -p ~/.config/punktfunk mkdir -p ~/.config/punktfunk
cp /usr/share/punktfunk/host.env.bazzite ~/.config/punktfunk/host.env # gamescope defaults cp /usr/share/punktfunk/host.env.bazzite ~/.config/punktfunk/host.env # gamescope defaults
systemctl --user enable --now punktfunk-host systemctl --user enable --now punktfunk-host
# Web console — enable it and read the auto-generated login password (then open http://<host-ip>:3000):
systemctl --user enable --now punktfunk-web
journalctl --user -u punktfunk-web-init | sed -n 's/.*password generated: //p'
``` ```
(See [`../bazzite/README.md`](../bazzite/README.md) for the full appliance walkthrough — (See [`../bazzite/README.md`](../bazzite/README.md) for the full appliance walkthrough —
@@ -65,7 +70,9 @@ tracking: `rpm-ostree override` / `rpm-ostree uninstall punktfunk`.
## Build an RPM locally ## Build an RPM locally
```sh ```sh
PF_VERSION=0.0.1 bash packaging/rpm/build-rpm.sh # -> dist/punktfunk-0.0.1-1.fcNN.x86_64.rpm PF_VERSION=0.0.1 bash packaging/rpm/build-rpm.sh # host + client
PF_VERSION=0.0.1 PF_WITH_WEB=1 bash packaging/rpm/build-rpm.sh # + the noarch punktfunk-web (needs bun on PATH)
# -> dist/punktfunk-0.0.1-1.fcNN.x86_64.rpm (+ punktfunk-web-0.0.1-1.fcNN.noarch.rpm with PF_WITH_WEB=1)
``` ```
Run it inside the Fedora 43 builder image so the deps resolve and match Bazzite: Run it inside the Fedora 43 builder image so the deps resolve and match Bazzite:
+5 -1
View File
@@ -11,6 +11,10 @@ set -euo pipefail
PF_VERSION="${PF_VERSION:-0.0.1}" PF_VERSION="${PF_VERSION:-0.0.1}"
PF_RELEASE="${PF_RELEASE:-1}" PF_RELEASE="${PF_RELEASE:-1}"
# PF_WITH_WEB=1 builds the punktfunk-web subpackage too (needs `bun` on PATH — present in the CI
# builder image, not in a plain mock chroot). Default off so a bare `rpmbuild`/COPR still works.
WEB_OPT=()
[ "${PF_WITH_WEB:-0}" = "1" ] && WEB_OPT=(--with web)
ROOTDIR="$(cd "$(dirname "$0")/../.." && pwd)" ROOTDIR="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$ROOTDIR" cd "$ROOTDIR"
@@ -28,7 +32,7 @@ git archive --format=tar.gz --prefix="punktfunk-${PF_VERSION}/" \
# resolves them from RPMs. Our builder image provides the toolchain via rustup (so # resolves them from RPMs. Our builder image provides the toolchain via rustup (so
# rust-toolchain.toml's pinned channel works) and the -devel libs via dnf, neither of which # rust-toolchain.toml's pinned channel works) and the -devel libs via dnf, neither of which
# rpmbuild's RPM-level check sees — skip it; a genuinely missing dep fails the compile/link. # rpmbuild's RPM-level check sees — skip it; a genuinely missing dep fails the compile/link.
rpmbuild -bb --nodeps \ rpmbuild -bb --nodeps "${WEB_OPT[@]}" \
--define "_topdir $TOP" \ --define "_topdir $TOP" \
--define "pf_version ${PF_VERSION}" \ --define "pf_version ${PF_VERSION}" \
--define "pf_release ${PF_RELEASE}" \ --define "pf_release ${PF_RELEASE}" \
+79
View File
@@ -39,6 +39,13 @@ ExclusiveArch: x86_64 aarch64
# Recommends). Drop it from the auto-Requires, mirroring the Debian package's NVIDIA filter. # Recommends). Drop it from the auto-Requires, mirroring the Debian package's NVIDIA filter.
%global __requires_exclude ^libcuda\\.so.*$ %global __requires_exclude ^libcuda\\.so.*$
# Management web console subpackage (punktfunk-web). OFF by default: building the Nitro/Node SSR
# bundle needs `bun`, which a plain rpmbuild / COPR mock chroot does NOT have. CI's builder image
# (ci/fedora-rpm.Dockerfile) DOES have bun and builds with `--with web`, so the Gitea RPM registry
# carries punktfunk-web. COPR (no bun) builds host+client only — use the Gitea registry for the
# console, or enable bun + `--with web` in the COPR project. Mirrors the Debian punktfunk-web .deb.
%bcond_with web
# --- Build toolchain --------------------------------------------------------- # --- Build toolchain ---------------------------------------------------------
BuildRequires: cargo BuildRequires: cargo
BuildRequires: rust BuildRequires: rust
@@ -90,6 +97,10 @@ Suggests: kwin
Suggests: mutter Suggests: mutter
# NVENC + GPU EGL come from the NVIDIA driver; on Bazzite the -nvidia image has it. # NVENC + GPU EGL come from the NVIDIA driver; on Bazzite the -nvidia image has it.
Recommends: (xorg-x11-drv-nvidia-cuda if xorg-x11-drv-nvidia) Recommends: (xorg-x11-drv-nvidia-cuda if xorg-x11-drv-nvidia)
# The management web console (pairing + status) every user needs — a separate noarch subpackage.
# Weak-dep so `dnf install punktfunk` pulls it where it exists (the Gitea registry); harmless where
# it doesn't (a COPR build without `--with web` simply has no punktfunk-web to satisfy).
Recommends: punktfunk-web
%description %description
punktfunk is a Linux-first, low-latency desktop and game streaming host. It speaks punktfunk is a Linux-first, low-latency desktop and game streaming host. It speaks
@@ -114,6 +125,23 @@ audio, microphone passthrough, and full gamepad support including DualSense
touchpad, motion, adaptive triggers and lightbar through SDL3. The host creates a touchpad, motion, adaptive triggers and lightbar through SDL3. The host creates a
virtual output at exactly this client's resolution and refresh rate no scaling. virtual output at exactly this client's resolution and refresh rate no scaling.
%if %{with web}
%package web
Summary: punktfunk management web console (Nitro/Node SSR + React)
BuildArch: noarch
# Runtime is plain node (the .output is portable JS — bun is only the build tool). Fedora 41+
# ships nodejs >= 20, which the node-server build needs.
Requires: nodejs
%description web
The browser console for a punktfunk streaming host: status, paired devices, and the SPAKE2
PIN pairing flow every client needs. Runs as a systemd --user service on port 3000, login-gated
(a password generated on first start), proxying the host's loopback HTTPS management API with a
bearer token injected server-side (never sent to the browser). Auto-wired to the host on a
packaged install it sources the host's mgmt token and a generated login password, no env
editing. Enable with `systemctl --user enable --now punktfunk-web`.
%endif
%prep %prep
%autosetup -n %{name}-%{version} %autosetup -n %{name}-%{version}
@@ -123,6 +151,16 @@ virtual output at exactly this client's resolution and refresh rate — no scali
export RUSTFLAGS="%{?build_rustflags}" export RUSTFLAGS="%{?build_rustflags}"
cargo build --release -p punktfunk-host -p punktfunk-client-linux cargo build --release -p punktfunk-host -p punktfunk-client-linux
%if %{with web}
# Management web console: build the Nitro/Node SSR bundle (node-server preset) with bun. The
# .output is portable JS run at runtime by plain node; bun is only the build tool (CI image).
(cd web && bun install --frozen-lockfile && bun run build)
if grep -q 'Bun\.serve' web/.output/server/index.mjs; then
echo "ERROR: web build is a bun bundle (Bun.serve) need the node-server preset" >&2
exit 1
fi
%endif
%install %install
# Binary # Binary
install -Dm0755 target/release/punktfunk-host %{buildroot}%{_bindir}/punktfunk-host install -Dm0755 target/release/punktfunk-host %{buildroot}%{_bindir}/punktfunk-host
@@ -177,6 +215,24 @@ install -d %{buildroot}%{_datadir}/%{name}/bazzite
install -Dm0755 packaging/bazzite/kde-desktop-setup.sh %{buildroot}%{_datadir}/%{name}/bazzite/kde-desktop-setup.sh install -Dm0755 packaging/bazzite/kde-desktop-setup.sh %{buildroot}%{_datadir}/%{name}/bazzite/kde-desktop-setup.sh
install -Dm0644 docs/api/openapi.json %{buildroot}%{_datadir}/%{name}/openapi.json install -Dm0644 docs/api/openapi.json %{buildroot}%{_datadir}/%{name}/openapi.json
%if %{with web}
# --- web console subpackage (punktfunk-web) ---
install -d %{buildroot}%{_datadir}/punktfunk-web/.output
cp -r web/.output/server %{buildroot}%{_datadir}/punktfunk-web/.output/server
cp -r web/.output/public %{buildroot}%{_datadir}/punktfunk-web/.output/public
# PATH-stable launcher (matches the .deb's /usr/bin/punktfunk-web-server).
cat > %{buildroot}%{_bindir}/punktfunk-web-server <<'WRAP'
#!/bin/sh
exec /usr/bin/node /usr/share/punktfunk-web/.output/server/index.mjs "$@"
WRAP
chmod 0755 %{buildroot}%{_bindir}/punktfunk-web-server
# systemd --user units: the console runs per-user; web-init generates the login password.
install -Dm0644 scripts/punktfunk-web.service %{buildroot}%{_userunitdir}/punktfunk-web.service
install -Dm0644 scripts/punktfunk-web-init.service %{buildroot}%{_userunitdir}/punktfunk-web-init.service
install -Dm0755 scripts/web-init.sh %{buildroot}%{_datadir}/punktfunk-web/web-init.sh
install -Dm0644 web/web.env.example %{buildroot}%{_datadir}/punktfunk-web/web.env.example
%endif
%files %files
%license LICENSE-MIT LICENSE-APACHE %license LICENSE-MIT LICENSE-APACHE
%doc README.md docs/implementation-plan.md packaging/README.md %doc README.md docs/implementation-plan.md packaging/README.md
@@ -195,6 +251,18 @@ install -Dm0644 docs/api/openapi.json %{buildroot}%{_datadir}/%
%{_udevrulesdir}/70-punktfunk-client.rules %{_udevrulesdir}/70-punktfunk-client.rules
%{_prefix}/lib/sysctl.d/99-punktfunk-client-net.conf %{_prefix}/lib/sysctl.d/99-punktfunk-client-net.conf
%if %{with web}
%files web
%license LICENSE-MIT LICENSE-APACHE
%{_bindir}/punktfunk-web-server
%dir %{_datadir}/punktfunk-web
%{_datadir}/punktfunk-web/.output
%{_datadir}/punktfunk-web/web-init.sh
%{_datadir}/punktfunk-web/web.env.example
%{_userunitdir}/punktfunk-web.service
%{_userunitdir}/punktfunk-web-init.service
%endif
%post client %post client
# Pick up the DualSense hidraw rule without a reboot (best-effort; on rpm-ostree it # Pick up the DualSense hidraw rule without a reboot (best-effort; on rpm-ostree it
# applies on the next boot into the layered deployment). # applies on the next boot into the layered deployment).
@@ -215,6 +283,17 @@ echo "punktfunk installed. Add yourself to the 'input' group (sudo usermod -aG i
echo "then enable the host: systemctl --user enable --now punktfunk-host" echo "then enable the host: systemctl --user enable --now punktfunk-host"
echo "Config: cp %{_datadir}/%{name}/host.env.bazzite ~/.config/punktfunk/host.env" echo "Config: cp %{_datadir}/%{name}/host.env.bazzite ~/.config/punktfunk/host.env"
%if %{with web}
%post web
echo "punktfunk-web installed. Enable the console for your user:"
echo " systemctl --user enable --now punktfunk-web"
echo "A login password is generated on first start read it with:"
echo " journalctl --user -u punktfunk-web-init | sed -n 's/.*password generated: //p'"
echo "Then open http://<host-ip>:3000"
%endif
%changelog %changelog
* Sun Jun 15 2026 punktfunk <noreply@anthropic.com> - 0.0.1-2
- Add punktfunk-web subpackage (management console, --with web; auto-wired to the host token).
* Wed Jun 10 2026 punktfunk <noreply@anthropic.com> - 0.0.1-1 * Wed Jun 10 2026 punktfunk <noreply@anthropic.com> - 0.0.1-1
- Initial RPM: punktfunk-host + udev rule + systemd user unit + headless helpers. - Initial RPM: punktfunk-host + udev rule + systemd user unit + headless helpers.