Files
punktfunk/packaging/arch/PKGBUILD
enricobuehler b1e0525872 fix(packaging/arch): pacman could upgrade FFmpeg out from under the host and brick it
`depends=('ffmpeg' ...)` carried no version bound, and pacman is the only one of our
packaging formats that does not derive dependencies from ELF DT_NEEDED — rpm
auto-generates `libavcodec.so.62()(64bit)`, dpkg-shlibdeps emits `libavcodec62`, nix
pins the closure. So when Arch shipped ffmpeg 2:9.0-5 on 2026-08-08 and every soname
moved (libavutil .60->.61, libavcodec .62->.63, libavfilter .11->.12, libavdevice
.62->.63, libswscale .9->.10), a plain `pacman -Syu` walked every Arch/CachyOS install
straight across the break. The result is not a crash we can log: the dynamic loader
cannot start the binary at all, so it is exit 127 *before* main() in a systemd restart
loop, and because punktfunk-web is a separate bun service with no libav linkage it keeps
serving happily while :47990 has nothing listening — which reads as "the mgmt API is
broken" rather than "the host is not running". `ldd /usr/bin/punktfunk-host | grep
"not found"` is the one-line diagnosis.

Depend on the sonames instead of the package. Arch's ffmpeg declares the matching
`provides=(libavcodec.so=63-64 ...)`, and makepkg rewrites each bare `libfoo.so` listed
in depends into `libfoo.so=<soname>-<arch>` by reading the built binary's DT_NEEDED, so
the bound tracks whatever FFmpeg the builder linked against with nothing to hand-maintain
across the next bump. pacman now refuses the ffmpeg upgrade rather than bricking the
install. A hand-written `ffmpeg<2:9` would have gone stale on the very next major; not
bundling FFmpeg the way the .deb does, because that exists only because Ubuntu 24.04 LTS
is frozen on 6.1 and can never satisfy the dep, while rolling Arch always ships a current
one.

Verified on a real ffmpeg-9 box (192.168.1.21): the built package records
libavcodec.so=63-64, libavutil.so=61-64, libavfilter.so=12-64, libavdevice.so=63-64 and
libswscale.so=10-64, exactly matching DT_NEEDED, with the two libs --as-needed drops left
bare and satisfied by any ffmpeg.

The new arch.yml step asserts that expansion actually happened. If it ever stops — Arch
dropping the soname provides, someone tidying the entries out of depends — the dep
silently degrades to an unversioned name that any ffmpeg satisfies, which is exactly the
state that caused this, and it is invisible in a green build until a box bricks weeks later.
2026-08-08 02:34:59 +02:00

394 lines
28 KiB
Bash

# Maintainer: unom <packages@unom.io>
#
# Arch Linux / SteamOS split package: punktfunk-host (the gaming-rig HOST, NVENC) and
# punktfunk-client (the native GTK4/libadwaita Linux CLIENT). Mirrors the rpm subpackages
# (packaging/rpm/punktfunk.spec) and the two deb build scripts. On a Steam Deck you want
# `punktfunk-client` (it's what the Decky plugin launches); on a gaming rig, `punktfunk-host`.
#
# Two build modes:
# - AUR / standalone: makepkg in this dir (fetches the git tag below).
# - In-tree / CI: PF_SRCDIR=$(git rev-parse --show-toplevel) makepkg --holdver
# (builds the working tree instead of the tagged source — see build()).
#
# Host encode: NVENC on NVIDIA (nvidia-utils), VAAPI on AMD/Intel (mesa) — PUNKTFUNK_ENCODER=auto
# picks per GPU. The CLIENT decodes via VAAPI (AMD/Intel, incl. the Deck) with a software
# fallback, so it works everywhere. See README.md.
pkgbase=punktfunk
# punktfunk-web (the browser console) and punktfunk-scripting (the plugin/script runner) are OPT-IN:
# both need `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` / `%bcond_with
# scripting` (off by default). Set PF_WITH_WEB=1 / PF_WITH_SCRIPTING=1 to also build them (each
# appends its pkgname + bun to makedepends below).
# aarch64 (Arch Linux ARM) is CLIENT-ONLY — see the `arch` note below. Dropping the host from
# `pkgname` (rather than giving package_punktfunk-host a per-package arch) is what keeps makepkg
# from entering the host's build/package path at all, and mirrors how PF_WITH_WEB extends this
# array further down.
if [ "${CARCH:-x86_64}" = 'aarch64' ]; then
pkgname=('punktfunk-client')
else
pkgname=('punktfunk-host' 'punktfunk-client')
fi
# CI (.gitea/workflows/arch.yml) drives the version: stable tags -> X.Y.Z-1, main pushes ->
# X.Y.Z-0.<run#> in the separate punktfunk-canary repo (mirrors the RPM's 0.ciN release; pkgrel
# allows only digits+dots, so the run number carries the monotonic ordering).
pkgver="${PF_PKGVER:-0.7.0}"
pkgrel="${PF_PKGREL:-1}"
# The CLIENT builds and runs on aarch64 (Arch Linux ARM); the HOST does not — its encode stack is
# NVENC/QSV/AMF, all x86. On aarch64 the split below drops punktfunk-host from `pkgname`, so
# makepkg builds only the client and never tries to compile the host crate.
arch=('x86_64' 'aarch64')
url="https://git.unom.io/unom/punktfunk"
license=('MIT OR Apache-2.0')
# !lto: makepkg's `lto` option injects -flto=auto into CFLAGS; aws-lc-sys (rustls' crypto)
# compiles its C with those flags and GCC LTO bitcode objects are unreadable by rust's lld
# linker -> "undefined symbol: aws_lc_*" at link (reproduced 2026-07-04, Arch + rust 1.90).
# !debug: skip the -debug split package (debuginfo bloat, not shipped).
options=('!lto' '!debug')
# All build deps for both packages (Arch runtime packages ship their own headers, so these cover
# build + link). aws-lc/ring need clang+cmake; nasm is for asm. ffmpeg stays because the HOST's
# encoder links libav* — the CLIENT dropped it in M10 and decodes natively. `ffmpeg` is
# deliberately UNVERSIONED here: ffmpeg-sys-next auto-detects the installed FFmpeg, so the package
# builds against whatever the builder ships, and the RUNTIME bound follows automatically from the
# soname deps makepkg derives from the linked binary (see package_punktfunk-host's depends). Pinning
# a version at build time would just have to be re-edited on every FFmpeg major. No vulkan-headers:
# nothing in the workspace compiles against the system Vulkan headers any more (pyrowave-sys builds
# against its own vendored copy, and both binaries reach Vulkan through ash, which dlopens it).
makedepends=('rust' 'cargo' 'clang' 'cmake' 'nasm' 'pkgconf' 'git'
'gtk4' 'libadwaita' 'sdl3' 'ffmpeg' 'pipewire' 'wayland' 'libxkbcommon' 'opus' 'libei')
# Opt-in punktfunk-web / punktfunk-scripting: only then is bun (the build tool AND the vendored
# runtime) required. Adding bun twice is harmless (makepkg dedups makedepends).
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
if [ "${PF_WITH_SCRIPTING:-0}" = 1 ]; then
pkgname+=('punktfunk-scripting')
makedepends+=('bun')
fi
# AUR source (a tagged release). For an in-tree CI build, set PF_SRCDIR to the repo root —
# build() uses it instead AND the fetch is skipped entirely (a canary pkgver has no tag to
# clone, and CI already has the checkout).
if [ -z "${PF_SRCDIR:-}" ]; then
source=("git+https://git.unom.io/unom/punktfunk.git#tag=v${pkgver}")
sha256sums=('SKIP')
else
source=()
sha256sums=()
fi
_repo() { printf '%s' "${PF_SRCDIR:-$srcdir/punktfunk}"; }
build() {
cd "$(_repo)"
export RUSTUP_TOOLCHAIN=stable CARGO_TARGET_DIR="$srcdir/target"
export PUNKTFUNK_BUILD_VERSION="${pkgver}-${pkgrel}" # stamp --version / mgmt /health (build.rs)
# The host's zero-copy FFI link-needs libcuda at build time; nvidia-utils provides it on an
# 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/
# punktfunk-client-session is the Vulkan/Skia streamer the shell (punktfunk-client) execs for
# a connect — both binaries must ship or streaming from the desktop client breaks.
# `--features punktfunk-host/nvenc` compiles in the direct-SDK NVENC path (real RFI + recovery
# anchor on Linux NVIDIA; design/linux-direct-nvenc.md). AMD/Intel-SAFE: the NVENC/CUDA entry
# points are dlopen'd at RUNTIME (libloading), never link-imported — `objdump -p` shows the same
# DT_NEEDED as a plain build (no libcuda/libnvidia-encode), so the binary starts fine driver-less
# and only touches NVIDIA on a CUDA capture frame (default on NVIDIA; PUNKTFUNK_NVENC_DIRECT=0
# opts back to libav). AMD/Intel never reach it — the `cuda` gate leaves them on VAAPI.
# `punktfunk-host/vulkan-encode` is the AMD/Intel twin: a raw VK_KHR_video_encode_h265 backend with
# real RFI (clean P-frame recovery anchor via DPB reference slots; design/linux-vulkan-video-encode.md).
# Pure Rust `ash` (no new lib, no link-time deps); default on for HEVC (PUNKTFUNK_VULKAN_ENCODE=0
# opts back to libav VAAPI), and a failed open falls back to VAAPI so unsupported devices are safe.
if [ "$CARCH" = 'aarch64' ]; then
# Client-only: no host crate, so none of the encode features below apply. pf-update still
# builds — the client package ships its own copy for `--apply-update`.
cargo build --release --locked -p punktfunk-client-linux -p punktfunk-client-session \
-p punktfunk-cli -p pf-update
else
cargo build --release --locked --features punktfunk-host/nvenc,punktfunk-host/vulkan-encode \
-p punktfunk-host -p punktfunk-client-linux -p punktfunk-client-session -p punktfunk-cli \
-p pf-update
# The status tray in its OWN cargo invocation — load-bearing, not tidiness. Cargo unifies features
# across everything in one build, so co-building the tray with the host pulls the host's
# ashpd -> zbus/tokio onto the tray's shared zbus; the tray (ksni async-io + blocking, no tokio
# runtime by design) then panics at startup ("there is no reactor running, must be called from the
# context of a Tokio 1.x runtime"). Built alone, its zbus stays on async-io. (Same split the .deb does.)
cargo build --release --locked -p punktfunk-tray
fi
# Management web console (opt-in): the Nitro `bun`-preset .output bundle (Bun.serve TLS),
# built AND run with bun.
if [ "${PF_WITH_WEB:-0}" = 1 ]; then
( cd web && bun install --frozen-lockfile && bun run build )
fi
# Plugin/script runner (opt-in): one self-contained JS via `bun build --target=bun` (effect + the
# SDK inlined; the dynamic plugin import stays a runtime import). bun is also its vendored runtime.
if [ "${PF_WITH_SCRIPTING:-0}" = 1 ]; then
( cd sdk && bun install --frozen-lockfile --ignore-scripts && \
bun build src/runner-cli.ts --target=bun --outfile="$srcdir/runner-cli.js" )
grep -q 'attempt=' "$srcdir/runner-cli.js" \
|| { echo "ERROR: runner bundle missing the dynamic plugin import — wrong build" >&2; exit 1; }
fi
}
package_punktfunk-host() {
pkgdesc="Low-latency desktop/game streaming HOST (Moonlight-compatible + punktfunk/1)"
# NVENC + GPU EGL/CUDA come from the NVIDIA driver (nvidia-utils) — kept an optdepend, never a
# hard dep, exactly as the RPM (__requires_exclude libcuda) and deb (shlibdeps filter) do.
# The host captures the sink monitor through NATIVE PipeWire (audio/linux.rs) — it never
# opens a Pulse socket itself, so pipewire-pulse is an OPTdepend, not a depend: it exists
# for the GAMES, which commonly emit through the PulseAudio API. Hard-depending on it made
# the package uninstallable next to real `pulseaudio`, which serves those games just as well.
# ⚠ The libav* entries below are SONAME deps, not package names, and they are load-bearing.
# pacman is the only one of our packaging formats that does NOT derive dependencies from ELF
# DT_NEEDED (rpm auto-generates `libavcodec.so.62()(64bit)`; dpkg-shlibdeps emits `libavcodec62`;
# nix pins the closure). So a bare `depends=('ffmpeg')` let `pacman -Syu` walk the host across an
# FFmpeg soname bump with no warning and no conflict — which is exactly what FFmpeg 8 -> 9
# (2026-08-08, ffmpeg 2:9.0-5: libavutil .60->.61, libavcodec .62->.63, libavfilter .11->.12,
# libavdevice .62->.63, libswscale .9->.10) did to every Arch/CachyOS user: the dynamic loader
# cannot start the binary at all, so it is exit 127 *before* main() in a systemd restart loop,
# with nothing in the host's own log to explain it (`ldd /usr/bin/punktfunk-host | grep "not found"`
# is the one-line diagnosis). Arch's ffmpeg declares the matching
# `provides=(libavcodec.so=63-64 libavutil.so=61-64 ...)`, and makepkg rewrites each bare
# `libfoo.so` listed here into `libfoo.so=<soname>-<arch>` by reading the built binary's
# DT_NEEDED — so the bound tracks whatever FFmpeg the builder linked against, with nothing to
# hand-maintain across the next bump. pacman then REFUSES the ffmpeg upgrade instead of bricking
# the install, and the canary rebuilt on an ffmpeg-9 image installs cleanly on an ffmpeg-9 box.
# A hand-written `ffmpeg<2:9` would have to be edited (and would go stale) on every bump; this
# does not. NOT bundling FFmpeg the way the .deb does (BUNDLE_FFMPEG=1): that exists because
# Ubuntu 24.04 LTS is frozen on FFmpeg 6.1 and can never satisfy the dep, whereas rolling Arch
# always ships a current FFmpeg — and vendoring a second copy of a system library is against
# Arch packaging doctrine (and would not survive AUR review).
# All seven are listed even though --as-needed currently drops libavformat/libswresample from the
# link: an unlinked soname is left bare by makepkg and is satisfied by any ffmpeg, so listing it
# costs nothing, while a future link picking one up gets the version bound automatically.
depends=('ffmpeg' 'pipewire' 'wireplumber' 'opus' 'libei'
'mesa' 'libglvnd' 'libxkbcommon' 'wayland'
'libavcodec.so' 'libavutil.so' 'libavfilter.so' 'libavdevice.so'
'libavformat.so' 'libswscale.so' 'libswresample.so')
optdepends=('pipewire-pulse: PulseAudio-API audio from games/apps (real `pulseaudio` also works)'
'nvidia-utils: NVENC hardware encode + GPU EGL/CUDA zero-copy (REQUIRED to encode on NVIDIA)'
'gamescope: per-session nested compositor backend (no desktop login needed) — needs >=3.16.22'
'punktfunk-gamescope: HDR (10-bit BT.2020 PQ) streaming on the gamescope backend — attempted by default when installed'
'kwin: stream a KDE Plasma desktop (kwin VirtualDisplay backend)'
'mutter: stream a GNOME desktop (Mutter RecordVirtual backend)'
'sway: stream a wlroots desktop (Sway VirtualDisplay backend)'
'xdg-desktop-portal-kde: portal for the headless KDE session helper'
'xdg-desktop-portal-wlr: portal for the headless Sway session helper'
'punktfunk-web: browser management console (device pairing + status)'
'punktfunk-scripting: plugin/script runner for host automation (bun)')
install=punktfunk-host.install
# User-editable config: the headless game-mode drop-in (see below) — don't clobber local edits.
backup=('etc/gamescope-session-plus/sessions.d/steam')
local R; R="$(_repo)"; local T="$srcdir/target/release"
install -Dm0755 "$T/punktfunk-host" "$pkgdir/usr/bin/punktfunk-host"
# /dev/uinput + /dev/uhid -> input group (virtual gamepads + DualSense UHID)
install -Dm0644 "$R/scripts/60-punktfunk.rules" "$pkgdir/usr/lib/udev/rules.d/60-punktfunk.rules"
# Managed gamescope takeover on DM-autologin boxes: root helper + polkit action so the host can
# stop/restore the display manager for the stream. Arch has no /usr/libexec — install under
# /usr/lib/punktfunk and rewrite the policy's exec.path annotation to match (the host probes both).
install -Dm0755 "$R/scripts/pf-dm-helper" "$pkgdir/usr/lib/punktfunk/pf-dm-helper"
install -Dm0644 "$R/scripts/io.unom.punktfunk.dm-helper.policy" \
"$pkgdir/usr/share/polkit-1/actions/io.unom.punktfunk.dm-helper.policy"
sed -i 's#/usr/libexec/punktfunk/pf-dm-helper#/usr/lib/punktfunk/pf-dm-helper#' \
"$pkgdir/usr/share/polkit-1/actions/io.unom.punktfunk.dm-helper.policy"
# Web-console-triggered updates (host-update-from-web-console.md §7): root helper + oneshot
# unit + group-scoped polkit rule. Same no-libexec relocation as pf-dm-helper, with the
# unit's ExecStart rewritten to match. On pacman the helper additionally requires the
# explicit PACMAN_FULL_SYSUPGRADE opt-in (partial upgrades are against Arch doctrine).
install -Dm0755 "$T/pf-update" "$pkgdir/usr/lib/punktfunk/pf-update"
install -Dm0644 "$R/packaging/linux/punktfunk-update.service" \
"$pkgdir/usr/lib/systemd/system/punktfunk-update.service"
sed -i 's#/usr/libexec/punktfunk/pf-update#/usr/lib/punktfunk/pf-update#' \
"$pkgdir/usr/lib/systemd/system/punktfunk-update.service"
install -Dm0644 "$R/packaging/linux/49-punktfunk-update.rules" \
"$pkgdir/usr/share/polkit-1/rules.d/49-punktfunk-update.rules"
# vhci-hcd autoload — usbip transport for the virtual Steam Deck pad (Steam only adopts USB pads)
install -Dm0644 "$R/scripts/punktfunk-modules.conf" "$pkgdir/usr/lib/modules-load.d/punktfunk.conf"
# 32 MB UDP socket buffers (send-side headroom at high bitrate)
install -Dm0644 "$R/scripts/99-punktfunk-net.conf" "$pkgdir/usr/lib/sysctl.d/99-punktfunk-net.conf"
# systemd USER units (the host runs in the graphical session, not as root); repoint ExecStart.
install -Dm0644 "$R/scripts/punktfunk-host.service" "$pkgdir/usr/lib/systemd/user/punktfunk-host.service"
sed -i 's#%h/punktfunk/target/release/punktfunk-host#/usr/bin/punktfunk-host#' \
"$pkgdir/usr/lib/systemd/user/punktfunk-host.service"
# Optional drop-in for a DESKTOP-LOGIN host: binds the host to graphical-session.target so a
# Plasma/GNOME restart restarts it instead of leaving it on a dead compositor connection. Shipped
# under /usr/share (NOT as an active drop-in) because it is wrong for the appliance route — the
# operator copies it into ~/.config/systemd/user/punktfunk-host.service.d/ when they want it.
install -Dm0644 "$R/scripts/punktfunk-host-desktop-session.conf" \
"$pkgdir/usr/share/punktfunk/punktfunk-host-desktop-session.conf"
# Install-kind + channel marker, read by the host's update-check surface (planning:
# host-update-from-web-console.md §4.1). A canary build's pkgrel is `0.<zero-padded run>`.
local _pf_update_channel=stable
[[ $pkgrel == 0.* ]] && _pf_update_channel=canary
printf 'pacman %s\n' "$_pf_update_channel" | \
install -Dm0644 /dev/stdin "$pkgdir/usr/share/punktfunk/install-kind"
install -Dm0644 "$R/scripts/punktfunk-kde-session.service" "$pkgdir/usr/lib/systemd/user/punktfunk-kde-session.service"
sed -i 's#%h/punktfunk/scripts/headless/run-headless-kde.sh#/usr/share/punktfunk/headless/run-headless-kde.sh#' \
"$pkgdir/usr/lib/systemd/user/punktfunk-kde-session.service"
# KWin Desktop-mode authorization: non-launcher .desktop whose X-KDE-Wayland-Interfaces lets the
# host bind KWin's restricted zkde_screencast (virtual output) + fake_input globals on an
# interactive Plasma session. Must ship with the host (KWin caches the per-exe grant on first
# connect). See the file's header comment.
install -Dm0644 "$R/packaging/linux/io.unom.Punktfunk.Host.desktop" \
"$pkgdir/usr/share/applications/io.unom.Punktfunk.Host.desktop"
# Status tray: per-user SNI icon + XDG autostart entry (self-gating: --autostart exits silently
# for users who don't run a host) + the hicolor status icons it names.
install -Dm0755 "$T/punktfunk-tray" "$pkgdir/usr/bin/punktfunk-tray"
install -Dm0644 "$R/packaging/linux/io.unom.Punktfunk.Tray.desktop" \
"$pkgdir/etc/xdg/autostart/io.unom.Punktfunk.Tray.desktop"
local sz png
for sz in 22x22 48x48; do
for png in "$R"/packaging/linux/icons/hicolor/$sz/apps/*.png; do
install -Dm0644 "$png" "$pkgdir/usr/share/icons/hicolor/$sz/apps/$(basename "$png")"
done
done
# headless session helpers + env templates + OpenAPI doc
install -Dm0755 "$R/scripts/headless/run-headless-kde.sh" "$pkgdir/usr/share/punktfunk/headless/run-headless-kde.sh"
install -Dm0755 "$R/scripts/headless/run-headless-sway.sh" "$pkgdir/usr/share/punktfunk/headless/run-headless-sway.sh"
install -Dm0644 "$R/scripts/headless/kde-authorized" "$pkgdir/usr/share/punktfunk/headless/kde-authorized"
install -Dm0644 "$R/scripts/headless/punktfunk-sink.conf" "$pkgdir/usr/share/punktfunk/headless/punktfunk-sink.conf"
install -Dm0644 "$R/scripts/host.env.example" "$pkgdir/usr/share/punktfunk/host.env.example"
install -Dm0644 "$R/packaging/bazzite/host.env" "$pkgdir/usr/share/punktfunk/host.env.bazzite"
install -Dm0644 "$R/packaging/kde/host.env" "$pkgdir/usr/share/punktfunk/host.env.kde"
# Headless GAME-mode fix: gamescope-session-plus drop-in that uses the headless backend when no
# display is connected (so SteamOS/Bazzite "Switch to Game Mode" works on a display-less streaming
# host). No-op on display-attached boxes; sourced as /etc/gamescope-session-plus/sessions.d/steam.
install -Dm0644 "$R/packaging/bazzite/gamescope-headless-session" \
"$pkgdir/etc/gamescope-session-plus/sessions.d/steam"
install -Dm0644 "$R/api/openapi.json" "$pkgdir/usr/share/punktfunk/openapi.json"
# Firewall openers — NOT auto-enabled (an Arch package never touches the admin's running firewall).
# Stock Arch ships no firewall; CachyOS ships ufw; some spins (EndeavourOS) enable firewalld — so we
# install BOTH a ufw application profile and firewalld service definitions, and the one for whatever
# firewall you actually run is a one-liner. See README.md → Firewall.
# ufw: sudo ufw allow punktfunk-native (or punktfunk-gamestream)
# firewalld: sudo firewall-cmd --reload && sudo firewall-cmd --permanent --add-service=punktfunk-native && sudo firewall-cmd --reload
install -Dm0644 "$R/packaging/linux/punktfunk.ufw" \
"$pkgdir/etc/ufw/applications.d/punktfunk"
install -Dm0644 "$R/packaging/linux/punktfunk-gamestream.xml" \
"$pkgdir/usr/lib/firewalld/services/punktfunk-gamestream.xml"
install -Dm0644 "$R/packaging/linux/punktfunk-native.xml" \
"$pkgdir/usr/lib/firewalld/services/punktfunk-native.xml"
# Web console opener (TCP 47992) — only meaningful with the optional punktfunk-web package; opened
# deliberately (see README.md → Firewall). ufw's equivalent is the punktfunk-web profile above.
install -Dm0644 "$R/packaging/linux/punktfunk-web.xml" \
"$pkgdir/usr/lib/firewalld/services/punktfunk-web.xml"
install -Dm0644 "$R/LICENSE-MIT" "$pkgdir/usr/share/licenses/punktfunk-host/LICENSE-MIT"
install -Dm0644 "$R/LICENSE-APACHE" "$pkgdir/usr/share/licenses/punktfunk-host/LICENSE-APACHE"
install -Dm0644 "$R/README.md" "$pkgdir/usr/share/doc/punktfunk-host/README.md"
}
package_punktfunk-client() {
pkgdesc="Low-latency desktop/game streaming CLIENT — native GTK4/libadwaita Linux app"
# The GTK4/libadwaita shell + its Vulkan session streamer: SDL3 gamepads, native decode
# (Vulkan Video, VAAPI, and openh264 + rav1d in software), PipeWire audio/mic.
# No ffmpeg since M10: the client links no libav* at all, and its VAAPI rung dlopens libva
# rather than linking it, so the driver optdepends below are all it needs.
# vulkan-icd-loader: the session binary loads libvulkan at runtime (ash) for both its decoder
# and its ash/Skia presenter.
# NOT pipewire-pulse: the client speaks NATIVE PipeWire (audio.rs drives libpipewire-0.3
# directly for both playback and the mic uplink) and never opens a Pulse socket, so the
# compat shim buys it nothing — while `pipewire-pulse` CONFLICTS with `pulseaudio`, which
# made the package uninstallable for anyone keeping real PulseAudio. Matches the .deb
# (Recommends) and the RPM (Recommends, "degrade gracefully without it").
depends=('gtk4' 'libadwaita' 'sdl3' 'pipewire' 'wireplumber'
'opus' 'libglvnd' 'vulkan-icd-loader')
optdepends=('libva-mesa-driver: VAAPI hardware decode on AMD (incl. Steam Deck); software fallback otherwise'
'intel-media-driver: VAAPI hardware decode on Intel'
'vulkan-radeon: Vulkan Video decode + presenter on AMD'
'vulkan-nouveau: Vulkan Video decode + presenter on NVIDIA (nvidia-utils for the proprietary ICD)'
'gamescope: run the client fullscreen in a Gaming-Mode session')
install=punktfunk-client.install
local R; R="$(_repo)"; local T="$srcdir/target/release"
install -Dm0755 "$T/punktfunk-client" "$pkgdir/usr/bin/punktfunk-client"
# The session streamer the shell execs for a connect (resolved as its sibling in /usr/bin).
install -Dm0755 "$T/punktfunk-session" "$pkgdir/usr/bin/punktfunk-session"
# The headless CLI (design/client-architecture-split.md §4).
install -Dm0755 "$T/punktfunk" "$pkgdir/usr/bin/punktfunk"
# The app icon the desktop entry (and the About dialog) name.
install -Dm0644 "$R/packaging/linux/icons/hicolor/scalable/apps/io.unom.Punktfunk.svg" \
"$pkgdir/usr/share/icons/hicolor/scalable/apps/io.unom.Punktfunk.svg"
install -Dm0644 "$R/packaging/linux/io.unom.Punktfunk.desktop" \
"$pkgdir/usr/share/applications/io.unom.Punktfunk.desktop"
# DualSense hidraw access (full pad fidelity through SDL's HIDAPI driver).
install -Dm0644 "$R/scripts/70-punktfunk-client.rules" \
"$pkgdir/usr/lib/udev/rules.d/70-punktfunk-client.rules"
# 32 MB UDP recv buffer (so high-bitrate streams don't overflow the kernel socket buffer).
install -Dm0644 "$R/scripts/99-punktfunk-client-net.conf" \
"$pkgdir/usr/lib/sysctl.d/99-punktfunk-client-net.conf"
# One-tap client updates (`punktfunk-client --apply-update`, which is what the Decky plugin
# runs): the same root helper the host package ships, under the CLIENT's own paths — a
# client-only box (a Deck) has no host package, and two packages owning one path is a hard
# pacman conflict. Same no-libexec relocation as the host's copy, with the unit's ExecStart
# rewritten to match. On pacman the helper additionally requires the explicit
# PACMAN_FULL_SYSUPGRADE opt-in (partial upgrades are against Arch doctrine).
install -Dm0755 "$T/pf-update" "$pkgdir/usr/lib/punktfunk/pf-update-client"
install -Dm0644 "$R/packaging/linux/punktfunk-client-update.service" \
"$pkgdir/usr/lib/systemd/system/punktfunk-client-update.service"
sed -i 's#/usr/libexec/punktfunk/pf-update#/usr/lib/punktfunk/pf-update-client#' \
"$pkgdir/usr/lib/systemd/system/punktfunk-client-update.service"
install -Dm0644 "$R/packaging/linux/49-punktfunk-client-update.rules" \
"$pkgdir/usr/share/polkit-1/rules.d/49-punktfunk-client-update.rules"
# Install-kind + channel marker for the CLIENT, read by `punktfunk-client --check-update`
# (planning: host-update-from-web-console.md §4.1). Its own directory, matching the RPM (where
# the host subpackage's %{_datadir}/punktfunk/* glob would otherwise co-own a sibling file) —
# one path per product across every format, so the detector has one answer to remember.
local _pf_client_channel=stable
[[ $pkgrel == 0.* ]] && _pf_client_channel=canary
printf 'pacman %s\n' "$_pf_client_channel" | \
install -Dm0644 /dev/stdin "$pkgdir/usr/share/punktfunk-client/install-kind"
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"
}
package_punktfunk-web() {
pkgdesc="punktfunk management web console (Nitro SSR on bun, HTTPS/HTTP-1.1 over TLS) — pairing + status in the browser"
# bun is the runtime (Bun.serve), and it's a native binary we vendor, so this package is
# arch-specific (not 'any'). Auto-wired to the host's mgmt token + identity cert via the systemd
# --user units; enable with `systemctl --user enable --now punktfunk-web`. No nodejs/bun dependency.
local R; R="$(_repo)"
# Pre-built bun-preset 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"
# Vendor the build env's bun into a private dir so it never collides with a
# system-wide bun on PATH.
install -Dm0755 "$(command -v bun)" "$pkgdir/usr/lib/punktfunk-web/bun"
install -d "$pkgdir/usr/bin"
printf '%s\n' '#!/bin/sh' 'exec /usr/lib/punktfunk-web/bun /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"
}
package_punktfunk-scripting() {
pkgdesc="punktfunk plugin/script runner (Effect SDK on bun) — supervises host automation scripts + punktfunk-plugin-* packages"
# bun is the runtime (it import()s the operator's .ts plugins), a vendored native binary, so this
# package is arch-specific (not 'any'). OPT-IN: the systemd --user unit ships disabled (the runner
# is inert until you add scripts/plugins). No nodejs/bun dependency.
local R; R="$(_repo)"
# Pre-built self-contained bundle (from build()) + a PATH-stable launcher (matches the .deb/.rpm).
install -Dm0644 "$srcdir/runner-cli.js" "$pkgdir/usr/share/punktfunk-scripting/runner-cli.js"
# Vendor the build env's bun into a private dir so it never collides with a system-wide bun.
install -Dm0755 "$(command -v bun)" "$pkgdir/usr/lib/punktfunk-scripting/bun"
install -d "$pkgdir/usr/bin"
printf '%s\n' '#!/bin/sh' 'exec /usr/lib/punktfunk-scripting/bun /usr/share/punktfunk-scripting/runner-cli.js "$@"' \
> "$pkgdir/usr/bin/punktfunk-scripting"
chmod 0755 "$pkgdir/usr/bin/punktfunk-scripting"
# systemd USER unit — installed but NOT auto-enabled (opt-in). Enable once you have automation:
# systemctl --user enable --now punktfunk-scripting
install -Dm0644 "$R/scripts/punktfunk-scripting.service" "$pkgdir/usr/lib/systemd/user/punktfunk-scripting.service"
install -Dm0644 "$R/LICENSE-MIT" "$pkgdir/usr/share/licenses/punktfunk-scripting/LICENSE-MIT"
install -Dm0644 "$R/LICENSE-APACHE" "$pkgdir/usr/share/licenses/punktfunk-scripting/LICENSE-APACHE"
}