ci(packaging): punktfunk-client .deb + RPM subpackage

Hook the Linux client into the existing packaging CI:

- deb.yml builds both binaries and publishes punktfunk-host AND
  punktfunk-client to the Gitea apt registry; new
  packaging/debian/build-client-deb.sh mirrors the host script
  (shlibdeps auto-Depends — GTK4/libadwaita/SDL3/FFmpeg/PipeWire
  sonames; no NVIDIA filter, the client links no CUDA). Built and
  inspected locally on Ubuntu 26.04.
- punktfunk.spec gains a "client" subpackage (binary + desktop entry +
  udev rule); rpm.yml's publish loop picks it up unchanged.
- New shared assets: packaging/linux/io.unom.Punktfunk.desktop and
  scripts/70-punktfunk-client.rules — DualSense hidraw uaccess (USB +
  Bluetooth, steam-devices style) so SDL's HIDAPI driver gets
  touchpad/motion/lightbar/triggers instead of degrading to evdev.
- Builder images learn the client link deps (rust-ci already had
  them; fedora-rpm adds gtk4/libadwaita/SDL3-devel) with idempotent
  install steps in deb.yml/rpm.yml since jobs run against the
  previous push's image.

Workspace check CI (build/clippy/test) already covers the crate since
f09def4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 23:18:12 +00:00
parent 67a32711b3
commit 4ff6f447a8
7 changed files with 217 additions and 20 deletions
+27 -17
View File
@@ -1,7 +1,8 @@
# Build the punktfunk-host .deb and publish it to Gitea's Debian package registry, so the # Build the punktfunk-host and punktfunk-client .debs and publish them to Gitea's Debian
# Ubuntu hosts get new builds via `apt update && apt upgrade`. Runs inside the same Ubuntu # package registry, so Ubuntu boxes get new builds via `apt update && apt upgrade`. Runs
# 26.04 rust-ci builder image as ci.yml, so dpkg-shlibdeps pins the runtime lib package names # inside the same Ubuntu 26.04 rust-ci builder image as ci.yml, so dpkg-shlibdeps pins the
# (libavcodec62, libpipewire-0.3-0t64, …) to exactly what the target boxes run. # runtime lib package names (libavcodec62, libpipewire-0.3-0t64, …) to exactly what the
# target boxes run.
# #
# Registry (public, unom org): https://git.unom.io/unom/-/packages # Registry (public, unom org): https://git.unom.io/unom/-/packages
# Box setup (once): see packaging/debian/README.md # Box setup (once): see packaging/debian/README.md
@@ -30,9 +31,15 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# dpkg-shlibdeps (Depends resolution) + dpkg-deb live in dpkg-dev. # dpkg-shlibdeps (Depends resolution) + dpkg-deb live in dpkg-dev. The client's link
- name: dpkg-dev # deps are also baked into the rust-ci image, but this job runs against the image
run: apt-get update && apt-get install -y --no-install-recommends dpkg-dev # from the PREVIOUS push (docker.yml bootstrap note) — keep it green across image
# changes; a no-op once the image has them.
- name: dpkg-dev + client link deps
run: |
apt-get update
apt-get install -y --no-install-recommends dpkg-dev \
libgtk-4-dev libadwaita-1-dev libsdl3-dev
# Share ci.yml's cache keys so the release build reuses its registry + target artifacts. # Share ci.yml's cache keys so the release build reuses its registry + target artifacts.
- name: Cache keys - name: Cache keys
@@ -50,10 +57,10 @@ jobs:
key: cargo-target-${{ env.rustc }}-${{ hashFiles('Cargo.lock') }} key: cargo-target-${{ env.rustc }}-${{ hashFiles('Cargo.lock') }}
restore-keys: cargo-target-${{ env.rustc }}- restore-keys: cargo-target-${{ env.rustc }}-
- name: Build release host - name: Build release host + client
run: | run: |
git config --global --add safe.directory "$PWD" git config --global --add safe.directory "$PWD"
cargo build --release -p punktfunk-host --locked cargo build --release -p punktfunk-host -p punktfunk-client-linux --locked
- name: Version - name: Version
# Tag v1.2.3 -> 1.2.3 (a real release); a main push -> 0.0.1~ciN.g<sha>, which sorts # Tag v1.2.3 -> 1.2.3 (a real release); a main push -> 0.0.1~ciN.g<sha>, which sorts
@@ -68,16 +75,19 @@ jobs:
echo "VERSION=$V" >> "$GITHUB_ENV" echo "VERSION=$V" >> "$GITHUB_ENV"
echo "package version $V" echo "package version $V"
- name: Build .deb - name: Build .debs
run: VERSION="$VERSION" bash packaging/debian/build-deb.sh run: |
VERSION="$VERSION" bash packaging/debian/build-deb.sh
VERSION="$VERSION" bash packaging/debian/build-client-deb.sh
- name: Publish to the Gitea apt registry - name: Publish to the Gitea apt registry
env: env:
TOKEN: ${{ secrets.REGISTRY_TOKEN }} TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: | run: |
DEB="$(ls dist/*.deb)" for DEB in dist/*.deb; do
echo "uploading $DEB" echo "uploading $DEB"
# PAT owner (enricobuehler), not the push actor — matches docker.yml's registry login. # PAT owner (enricobuehler), not the push actor — matches docker.yml's registry login.
curl -fsS --user "enricobuehler:$TOKEN" --upload-file "$DEB" \ curl -fsS --user "enricobuehler:$TOKEN" --upload-file "$DEB" \
"https://$REGISTRY/api/packages/$OWNER/debian/pool/$DISTRIBUTION/$COMPONENT/upload" "https://$REGISTRY/api/packages/$OWNER/debian/pool/$DISTRIBUTION/$COMPONENT/upload"
echo "published $DEB to $OWNER/debian $DISTRIBUTION/$COMPONENT" done
echo "published to $OWNER/debian $DISTRIBUTION/$COMPONENT"
+6 -1
View File
@@ -33,8 +33,13 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# rpmbuild + git archive need the checkout trusted; cache the crates download. # rpmbuild + git archive need the checkout trusted; cache the crates download.
# The client link deps are also baked into the fedora-rpm image, but this job runs
# against the image from the PREVIOUS push (docker.yml bootstrap note) — keep it
# green across image changes; a no-op once the image has them.
- name: Prep - name: Prep
run: git config --global --add safe.directory "$PWD" run: |
git config --global --add safe.directory "$PWD"
dnf -y install gtk4-devel libadwaita-devel SDL3-devel
- uses: actions/cache@v4 - uses: actions/cache@v4
with: with:
path: /usr/local/cargo/registry path: /usr/local/cargo/registry
+2
View File
@@ -19,6 +19,8 @@ RUN dnf -y install \
# ffmpeg (NVENC), capture/audio/display link deps # ffmpeg (NVENC), capture/audio/display link deps
ffmpeg-devel pipewire-devel wayland-devel libxkbcommon-devel opus-devel \ ffmpeg-devel pipewire-devel wayland-devel libxkbcommon-devel opus-devel \
mesa-libGL-devel mesa-libgbm-devel \ mesa-libGL-devel mesa-libgbm-devel \
# punktfunk-client link deps (GTK4 shell + SDL3 gamepads)
gtk4-devel libadwaita-devel SDL3-devel \
&& dnf clean all && dnf clean all
# 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
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env bash
# Build the punktfunk-client .deb (the native GTK4 client) for Ubuntu/Debian desktops.
#
# Counterpart to build-deb.sh (the host package); same conventions: runtime Depends are
# computed by dpkg-shlibdeps from the binary's DT_NEEDED (GTK4/libadwaita, SDL3, the
# FFmpeg/PipeWire/Opus sonames), so build inside the Ubuntu 26.04 rust-ci image to pin
# the package names the target boxes ship. The client links no NVIDIA libs — no filter
# needed.
#
# Usage: VERSION=0.0.1~ci42.gdeadbee [ARCH=amd64] bash packaging/debian/build-client-deb.sh
# Output: dist/punktfunk-client_<version>_<arch>.deb
set -euo pipefail
VERSION="${VERSION:?set VERSION (e.g. 0.0.1 or 0.0.1~ci42.gdeadbee)}"
ARCH="${ARCH:-amd64}"
PKG="punktfunk-client"
CRATE="punktfunk-client-linux"
ROOTDIR="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$ROOTDIR"
BIN="target/release/$PKG"
if [ ! -x "$BIN" ]; then
echo "==> building $CRATE (release)"
cargo build --release -p "$CRATE" --locked
fi
STAGE="$(mktemp -d)"
trap 'rm -rf "$STAGE"' EXIT
DOCDIR="$STAGE/usr/share/doc/$PKG"
# --- file layout --------------------------------------------------------------
install -Dm0755 "$BIN" "$STAGE/usr/bin/$PKG"
install -Dm0644 packaging/linux/io.unom.Punktfunk.desktop \
"$STAGE/usr/share/applications/io.unom.Punktfunk.desktop"
# DualSense hidraw access (full pad fidelity through SDL's HIDAPI driver).
install -Dm0644 scripts/70-punktfunk-client.rules \
"$STAGE/usr/lib/udev/rules.d/70-punktfunk-client.rules"
install -Dm0644 LICENSE-MIT "$DOCDIR/LICENSE-MIT"
install -Dm0644 LICENSE-APACHE "$DOCDIR/LICENSE-APACHE"
install -Dm0644 README.md "$DOCDIR/README.md"
cat > "$DOCDIR/copyright" <<EOF
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: punktfunk
Source: https://git.unom.io/unom/punktfunk
Files: *
Copyright: punktfunk contributors
License: MIT or Apache-2.0
Dual-licensed. Full texts in /usr/share/doc/$PKG/LICENSE-MIT and
/usr/share/doc/$PKG/LICENSE-APACHE.
EOF
printf '%s (%s) stable; urgency=medium\n\n * Automated build %s.\n\n -- unom <noreply@anthropic.com> %s\n' \
"$PKG" "$VERSION" "$VERSION" "$(date -uR 2>/dev/null || echo 'Thu, 01 Jan 1970 00:00:00 +0000')" \
| gzip -9n > "$DOCDIR/changelog.Debian.gz"
# --- dependencies --------------------------------------------------------------
SHLIB_TMP="$(mktemp -d)"
mkdir -p "$SHLIB_TMP/debian"
cat > "$SHLIB_TMP/debian/control" <<EOF
Source: $PKG
Package: $PKG
Architecture: any
Depends: \${shlibs:Depends}
EOF
SHDEPS="$(cd "$SHLIB_TMP" && dpkg-shlibdeps -O --ignore-missing-info "$ROOTDIR/$BIN" 2>/dev/null \
| sed -n 's/^shlibs:Depends=//p')"
rm -rf "$SHLIB_TMP"
[ -n "$SHDEPS" ] || { echo "dpkg-shlibdeps produced no deps — is dpkg-dev installed?" >&2; exit 1; }
# Manual additions shlibdeps can't see: the PipeWire daemon + session manager are runtime
# services (audio playback / mic capture degrade gracefully without them — Recommends).
RECOMMENDS="pipewire, wireplumber, pipewire-pulse"
INSTALLED_KB="$(du -k -s "$STAGE" | cut -f1)"
install -d "$STAGE/DEBIAN"
cat > "$STAGE/DEBIAN/control" <<EOF
Package: $PKG
Version: $VERSION
Architecture: $ARCH
Maintainer: unom <noreply@anthropic.com>
Installed-Size: $INSTALLED_KB
Section: net
Priority: optional
Homepage: https://git.unom.io/unom/punktfunk
Depends: $SHDEPS
Recommends: $RECOMMENDS
Description: Low-latency desktop/game streaming client (punktfunk/1, GTK4)
The native Linux client for punktfunk, a Linux-first low-latency desktop and
game streaming stack. Discovers hosts on the LAN (mDNS), trusts them via
certificate pinning with a SPAKE2 PIN pairing ceremony, and streams HEVC video
(GF(2^16) Leopard FEC + AES-GCM over UDP, QUIC control plane) with Opus audio,
microphone passthrough, and full gamepad support including DualSense 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. See the punktfunk-host package for the host side.
EOF
cat > "$STAGE/DEBIAN/postinst" <<'EOF'
#!/bin/sh
set -e
if [ "$1" = "configure" ]; then
# Pick up the DualSense hidraw rule without a reboot (best-effort, no-op in containers).
udevadm control --reload-rules 2>/dev/null || true
udevadm trigger --subsystem-match=hidraw 2>/dev/null || true
update-desktop-database /usr/share/applications 2>/dev/null || true
fi
exit 0
EOF
chmod 0755 "$STAGE/DEBIAN/postinst"
mkdir -p dist
OUT="dist/${PKG}_${VERSION}_${ARCH}.deb"
dpkg-deb --root-owner-group --build "$STAGE" "$OUT" >/dev/null
echo "built $OUT"
echo " Depends: $SHDEPS"
dpkg-deb -I "$OUT" | sed -n 's/^/ /p' | grep -E 'Version|Installed-Size' || true
+10
View File
@@ -0,0 +1,10 @@
[Desktop Entry]
Type=Application
Name=Punktfunk
Comment=Stream a remote punktfunk host
Exec=punktfunk-client
Icon=video-display
Terminal=false
Categories=Network;Game;
Keywords=streaming;remote;game;moonlight;
StartupNotify=true
+40 -2
View File
@@ -65,6 +65,10 @@ BuildRequires: pkgconfig(libavutil)
# Zero-copy GPU path: src/zerocopy/ links libGL + libgbm (mesa) via hand-rolled FFI. # Zero-copy GPU path: src/zerocopy/ links libGL + libgbm (mesa) via hand-rolled FFI.
BuildRequires: pkgconfig(gl) BuildRequires: pkgconfig(gl)
BuildRequires: pkgconfig(gbm) BuildRequires: pkgconfig(gbm)
# The client subpackage (GTK4 shell + SDL3 gamepads).
BuildRequires: pkgconfig(gtk4)
BuildRequires: pkgconfig(libadwaita-1)
BuildRequires: pkgconfig(sdl3)
# It ALSO links the NVIDIA CUDA driver lib (-lcuda) via FFI, so libcuda.so must be present # It ALSO links the NVIDIA CUDA driver lib (-lcuda) via FFI, so libcuda.so must be present
# at LINK time. A normal NVIDIA host (or Bazzite -nvidia) has it; a headless COPR/koji builder # at LINK time. A normal NVIDIA host (or Bazzite -nvidia) has it; a headless COPR/koji builder
# without a GPU does NOT — point %build at the CUDA toolkit stub (…/stubs/libcuda.so) there, # without a GPU does NOT — point %build at the CUDA toolkit stub (…/stubs/libcuda.so) there,
@@ -96,14 +100,28 @@ exact resolution and refresh via a per-compositor backend (KWin, gamescope, Mutt
Sway/wlroots), captured zero-copy (dmabuf -> CUDA -> NVENC) and split-encoded above Sway/wlroots), captured zero-copy (dmabuf -> CUDA -> NVENC) and split-encoded above
~1 Gpix/s. Input (mouse/keyboard/gamepads) is injected back into the session. ~1 Gpix/s. Input (mouse/keyboard/gamepads) is injected back into the session.
%package client
Summary: Low-latency desktop/game streaming client (punktfunk/1, GTK4)
# Audio playback / mic capture want the PipeWire daemon; degrade gracefully without it.
Recommends: pipewire
Recommends: wireplumber
%description client
The native Linux client for punktfunk. Discovers hosts on the LAN (mDNS), trusts
them via certificate pinning with a SPAKE2 PIN pairing ceremony, and streams HEVC
video (GF(2^16) Leopard FEC + AES-GCM over UDP, QUIC control plane) with Opus
audio, microphone passthrough, and full gamepad support including DualSense
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.
%prep %prep
%autosetup -n %{name}-%{version} %autosetup -n %{name}-%{version}
%build %build
# Release build of the host binary only (the workspace also has the core lib + clients). # Release build of the host + client binaries (the workspace also has the core lib).
# cargo fetches crates over the network; COPR build hosts allow this. # cargo fetches crates over the network; COPR build hosts allow this.
export RUSTFLAGS="%{?build_rustflags}" export RUSTFLAGS="%{?build_rustflags}"
cargo build --release -p punktfunk-host cargo build --release -p punktfunk-host -p punktfunk-client-linux
%install %install
# Binary # Binary
@@ -119,6 +137,14 @@ install -Dm0644 scripts/99-punktfunk-net.conf %{buildroot}%{_prefix}/lib/sysctl.
# systemd *user* unit (the host runs in the graphical session, not as root). # systemd *user* unit (the host runs in the graphical session, not as root).
install -Dm0644 scripts/punktfunk-host.service %{buildroot}%{_userunitdir}/punktfunk-host.service install -Dm0644 scripts/punktfunk-host.service %{buildroot}%{_userunitdir}/punktfunk-host.service
# --- client subpackage ---
install -Dm0755 target/release/punktfunk-client %{buildroot}%{_bindir}/punktfunk-client
install -Dm0644 packaging/linux/io.unom.Punktfunk.desktop \
%{buildroot}%{_datadir}/applications/io.unom.Punktfunk.desktop
# DualSense hidraw access (full pad fidelity through SDL's HIDAPI driver).
install -Dm0644 scripts/70-punktfunk-client.rules \
%{buildroot}%{_udevrulesdir}/70-punktfunk-client.rules
# Headless session helpers + example config + OpenAPI doc (reference material). # Headless session helpers + example config + OpenAPI doc (reference material).
install -d %{buildroot}%{_datadir}/%{name}/headless install -d %{buildroot}%{_datadir}/%{name}/headless
install -Dm0755 scripts/headless/run-headless-kde.sh %{buildroot}%{_datadir}/%{name}/headless/run-headless-kde.sh install -Dm0755 scripts/headless/run-headless-kde.sh %{buildroot}%{_datadir}/%{name}/headless/run-headless-kde.sh
@@ -137,6 +163,18 @@ install -Dm0644 docs/api/openapi.json %{buildroot}%{_datadir}/%
%dir %{_datadir}/%{name} %dir %{_datadir}/%{name}
%{_datadir}/%{name}/* %{_datadir}/%{name}/*
%files client
%license LICENSE-MIT LICENSE-APACHE
%{_bindir}/punktfunk-client
%{_datadir}/applications/io.unom.Punktfunk.desktop
%{_udevrulesdir}/70-punktfunk-client.rules
%post client
# Pick up the DualSense hidraw rule without a reboot (best-effort; on rpm-ostree it
# applies on the next boot into the layered deployment).
udevadm control --reload-rules 2>/dev/null || :
udevadm trigger --subsystem-match=hidraw 2>/dev/null || :
%post %post
# Reload udev so /dev/uinput picks up the new rule without a reboot (best-effort). # Reload udev so /dev/uinput picks up the new rule without a reboot (best-effort).
udevadm control --reload-rules 2>/dev/null || : udevadm control --reload-rules 2>/dev/null || :
+12
View File
@@ -0,0 +1,12 @@
# punktfunk-client: hidraw access for the seated user's DualSense (SDL's HIDAPI driver
# needs it for touchpad / motion / lightbar / player LEDs / adaptive triggers — without it
# SDL silently degrades to plain evdev, which has none of those). evdev joystick nodes are
# already uaccess-tagged by systemd; hidraw nodes are root-only by default and systemd
# declined a generic gamepad hwdb (systemd#22681), so we ship the rule, steam-devices
# style: the ATTRS match covers USB, the KERNELS match covers Bluetooth.
# DualSense (054c:0ce6)
KERNEL=="hidraw*", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="0ce6", MODE="0660", TAG+="uaccess"
KERNEL=="hidraw*", KERNELS=="*054C:0CE6*", MODE="0660", TAG+="uaccess"
# DualSense Edge (054c:0df2)
KERNEL=="hidraw*", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="0df2", MODE="0660", TAG+="uaccess"
KERNEL=="hidraw*", KERNELS=="*054C:0DF2*", MODE="0660", TAG+="uaccess"