feat(packaging/flatpak,decky): Steam Deck client flatpak + plugin deploy + CI
apple / swift (push) Successful in 53s
android / android (push) Successful in 3m48s
ci / web (push) Successful in 29s
ci / docs-site (push) Successful in 34s
ci / rust (push) Successful in 2m21s
ci / bench (push) Successful in 1m36s
decky / build-publish (push) Successful in 31s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 6s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 3s
flatpak / build-publish (push) Failing after 4s
deb / build-publish (push) Successful in 2m38s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m9s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m42s
docker / deploy-docs (push) Successful in 16s

Ship the punktfunk Linux client to the Steam Deck as a Flatpak — the only viable
SteamOS install path, since /usr is read-only and lacks libadwaita/SDL3 — and
publish both it and the Decky plugin through Gitea. Built and validated live on a
Steam Deck (SteamOS 3.7): bundle installs user-scope, all libs resolve, libavcodec
resolves to the codecs-extra HEVC build, devices=all for DualSense hidraw.

packaging/flatpak (new):
- io.unom.Punktfunk.yml on GNOME 50 / freedesktop-sdk 25.08. rust-stable//25.08
  (rustc 1.96 — the GTK4 chain needs >=1.92; the EOL GNOME-48/24.08 rust-stable at
  1.89 could not build it) + llvm20 (libclang for bindgen in ffmpeg-sys-next/sdl3-sys).
  HEVC libavcodec comes from the runtime's auto codecs-extra extension point (no
  app-side codec declaration). Bundled SDL3 3.4.10 (matches sdl3-sys 0.6.6+SDL-3.4.10).
  finish-args: wayland/fallback-x11, --device=all (GPU/VAAPI + evdev + hidraw — flatpak
  cannot bind /dev/hidrawN char devices via --filesystem), pulseaudio, network,
  ~/.config/punktfunk.
- metainfo.xml, desktop, square SVG icon, build-flatpak.sh (offline cargo-sources;
  on-Deck org.flatpak.Builder or CI), README.

clients/decky:
- add LICENSE (MIT), fix package.json license (BSD-3-Clause -> Apache-2.0 OR MIT),
  add scripts/{package.sh,deploy.sh} (the plugins dir is root-owned: stage to /tmp,
  sudo install, restart plugin_loader), align the launcher fallback to the real
  flatpak app id io.unom.Punktfunk, rewrite the install section.

.gitea/workflows:
- flatpak.yml: privileged Fedora container builds the bundle and publishes to the
  Gitea generic registry (+ release attachment on tags).
- decky.yml: pnpm build -> store-layout zip -> registry (stable latest/ URL for
  Decky "install from URL").

docs: packaging/README + packaging/flatpak/README.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 01:43:35 +02:00
parent 79217eb93d
commit 8956bc14de
16 changed files with 882 additions and 19 deletions
+126
View File
@@ -0,0 +1,126 @@
# Build the punktfunk Decky Loader plugin (Gaming-Mode QAM launcher) into a distribution zip
# and publish it to Gitea's GENERIC package registry, giving Decky's "install from URL" a
# stable link. On tags the zip is ALSO attached to the Gitea release.
#
# PUT/GET https://git.unom.io/api/packages/unom/generic/punktfunk-decky/<version>/punktfunk.zip
#
# The plugin backend is PURE PYTHON (clients/decky/main.py — no compiled binary), so we do NOT
# need the Decky CLI (which requires Docker + rust-nightly only to compile native backends).
# We build the frontend with pnpm and assemble the store-layout zip by hand:
#
# punktfunk.zip
# punktfunk/ <- single top-level dir == plugin.json "name"
# plugin.json [required]
# package.json [required]
# main.py [required: python backend]
# dist/index.js [required: rollup output]
# README.md (recommended)
# LICENSE [required by the plugin store]
#
# REGISTRY_TOKEN: repo Actions secret, a PAT with write:package scope (shared with deb/rpm/docker).
name: decky
on:
push:
branches: [main]
tags: ['v*']
workflow_dispatch:
env:
REGISTRY: git.unom.io
OWNER: unom
PACKAGE: punktfunk-decky # generic-registry package name
PLUGIN: punktfunk # plugin.json "name" == zip top-level dir
jobs:
build-publish:
runs-on: ubuntu-24.04
timeout-minutes: 30
container:
image: node:22-bookworm # node + corepack(pnpm); matches the @decky toolchain
defaults:
run:
working-directory: clients/decky
steps:
- uses: actions/checkout@v4
- name: pnpm
run: |
corepack enable
# The repo's pnpm-lock.yaml + package.json devDeps target pnpm 9 (the version the
# @decky toolchain and the local build use). Pin it so --frozen-lockfile holds.
corepack prepare pnpm@9 --activate
- name: Build frontend
run: |
pnpm install --frozen-lockfile
pnpm run build # rollup -> clients/decky/dist/index.js
- name: Version
# Tag v1.2.3 -> 1.2.3; main push -> 0.0.1-ciN.g<sha>. Used only for the registry
# version path + the zip name (the plugin.json version is the source of truth Decky
# reads after install).
working-directory: ${{ gitea.workspace }}
run: |
SHORT=$(echo "$GITHUB_SHA" | cut -c1-8)
case "$GITHUB_REF" in
refs/tags/v*) V="${GITHUB_REF_NAME#v}" ;;
*) V="0.0.1-ci${GITHUB_RUN_NUMBER}.g${SHORT}" ;;
esac
echo "VERSION=$V" >> "$GITHUB_ENV"
echo "decky version $V"
- name: Assemble store-layout zip
working-directory: ${{ gitea.workspace }}
run: |
apt-get update && apt-get install -y --no-install-recommends zip >/dev/null
STAGE="$RUNNER_TEMP/decky"
DEST="$STAGE/$PLUGIN"
rm -rf "$STAGE"; mkdir -p "$DEST/dist"
cp clients/decky/plugin.json "$DEST/"
cp clients/decky/package.json "$DEST/"
cp clients/decky/main.py "$DEST/"
cp clients/decky/dist/index.js "$DEST/dist/"
cp clients/decky/README.md "$DEST/"
# Store requires a LICENSE in the plugin root; the project is MIT OR Apache-2.0.
cp LICENSE-MIT "$DEST/LICENSE"
( cd "$STAGE" && zip -r "$RUNNER_TEMP/punktfunk.zip" "$PLUGIN" )
ls -lh "$RUNNER_TEMP/punktfunk.zip"
unzip -l "$RUNNER_TEMP/punktfunk.zip"
- name: Publish to the Gitea generic registry
working-directory: ${{ gitea.workspace }}
env:
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
BASE="https://$REGISTRY/api/packages/$OWNER/generic/$PACKAGE"
# 1) Immutable, versioned URL.
curl -fsS --user "enricobuehler:$TOKEN" --upload-file "$RUNNER_TEMP/punktfunk.zip" \
"$BASE/$VERSION/punktfunk.zip"
echo "published $BASE/$VERSION/punktfunk.zip"
# 2) Stable `latest/punktfunk.zip` — this is the link to paste into Decky's
# "install from URL". The generic registry rejects re-uploading an existing
# version/file (409), so delete the prior `latest` first (ignore 404 on run #1).
curl -fsS -o /dev/null --user "enricobuehler:$TOKEN" -X DELETE \
"$BASE/latest/punktfunk.zip" || true
curl -fsS --user "enricobuehler:$TOKEN" --upload-file "$RUNNER_TEMP/punktfunk.zip" \
"$BASE/latest/punktfunk.zip"
echo "install-from-URL link: $BASE/latest/punktfunk.zip"
- name: Attach zip to the Gitea release (tags only)
if: startsWith(gitea.ref, 'refs/tags/')
working-directory: ${{ gitea.workspace }}
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
API="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
ID=$(curl -sf -X POST "$API/releases" \
-H "Authorization: token $TOKEN" -H 'Content-Type: application/json' \
-d "{\"tag_name\":\"$GITHUB_REF_NAME\",\"name\":\"$GITHUB_REF_NAME\"}" \
| python3 -c 'import json,sys;print(json.load(sys.stdin)["id"])' \
|| curl -sf "$API/releases/tags/$GITHUB_REF_NAME" -H "Authorization: token $TOKEN" \
| python3 -c 'import json,sys;print(json.load(sys.stdin)["id"])')
curl -sf -X POST "$API/releases/$ID/assets?name=punktfunk-${VERSION}.zip" \
-H "Authorization: token $TOKEN" \
-F "attachment=@$RUNNER_TEMP/punktfunk.zip" >/dev/null
echo "attached punktfunk-${VERSION}.zip to release $GITHUB_REF_NAME"
+129
View File
@@ -0,0 +1,129 @@
# Build the native punktfunk Linux CLIENT as a single-file Flatpak bundle and publish it to
# Gitea's GENERIC package registry, so the Steam Deck (and any flatpak distro) installs it
# the SteamOS-native, update-survivable way: `flatpak install --user <downloaded>.flatpak`.
# (The HOST stays an RPM/deb — it needs unsandboxed /dev/uinput + zero-copy NVENC; only the
# CLIENT is sandbox-friendly. See packaging/README.md and packaging/flatpak/README.md.)
#
# Gitea has NO flatpak/ostree registry, so the bundle lives in the generic registry:
# PUT https://git.unom.io/api/packages/unom/generic/punktfunk-client-flatpak/<version>/<file>
# GET https://git.unom.io/api/packages/unom/generic/punktfunk-client-flatpak/<version>/<file>
# On tags the bundle is ALSO attached to the Gitea release (mirrors release.yml's DMG).
#
# PRIVILEGED-BUILD CONSTRAINT: flatpak-builder runs bubblewrap, which needs user namespaces.
# In a Gitea/act_runner Docker executor that means the job container must be --privileged
# (the same runner already runs `docker build` in docker.yml, so its Docker daemon allows it).
# If your runner CANNOT grant --privileged, this job will fail at `flatpak-builder` with
# "Creating new namespace failed: Operation not permitted" — see the fallback in
# packaging/flatpak/README.md (build on the Deck via org.flatpak.Builder, or on a Linux box,
# then upload with the curl line below).
#
# REGISTRY_TOKEN: repo Actions secret, a PAT with write:package scope (shared with deb/rpm/docker).
name: flatpak
on:
push:
branches: [main]
tags: ['v*']
workflow_dispatch:
env:
REGISTRY: git.unom.io
OWNER: unom
APP_ID: io.unom.Punktfunk
MANIFEST: packaging/flatpak/io.unom.Punktfunk.yml
PACKAGE: punktfunk-client-flatpak # generic-registry package name
jobs:
build-publish:
runs-on: ubuntu-24.04
timeout-minutes: 120
container:
# Fedora ships a recent flatpak + flatpak-builder + the kernel userns support.
# --privileged is required for bubblewrap inside the Docker executor (see header).
image: fedora:43
options: --privileged
steps:
- uses: actions/checkout@v4
- name: Tooling
run: |
dnf -y install flatpak flatpak-builder git python3 python3-aiohttp python3-toml curl jq
# Flathub provides the GNOME runtime/SDK + the rust-stable + ffmpeg-full extensions.
flatpak remote-add --user --if-not-exists flathub \
https://dl.flathub.org/repo/flathub.flatpakrepo
git config --global --add safe.directory "$PWD"
- name: Version
# Tag v1.2.3 -> 1.2.3; a main push -> 0.0.1-ciN.g<sha> (sorts before a real release,
# increases by run number — newest main build always wins). The generic registry
# version string allows letters/dots/hyphens.
run: |
SHORT=$(echo "$GITHUB_SHA" | cut -c1-8)
case "$GITHUB_REF" in
refs/tags/v*) V="${GITHUB_REF_NAME#v}" ;;
*) V="0.0.1-ci${GITHUB_RUN_NUMBER}.g${SHORT}" ;;
esac
echo "VERSION=$V" >> "$GITHUB_ENV"
echo "BUNDLE=punktfunk-client-${V}.flatpak" >> "$GITHUB_ENV"
echo "flatpak version $V"
- name: Generate offline cargo sources
# flatpak builds with no network; vendor every crate from Cargo.lock into
# cargo-sources.json next to the manifest (referenced by the manifest's
# punktfunk-client module).
run: |
curl -fsSL -o /tmp/flatpak-cargo-generator.py \
https://raw.githubusercontent.com/flatpak/flatpak-builder-tools/master/cargo/flatpak-cargo-generator.py
python3 /tmp/flatpak-cargo-generator.py Cargo.lock \
-o packaging/flatpak/cargo-sources.json
- name: Build the flatpak (install deps from Flathub, offline build)
run: |
# --install-deps-from=flathub pulls everything the manifest declares: the GNOME 50
# runtime/SDK + the rust-stable (//25.08, rustc 1.96) and llvm20 SDK extensions, plus
# the runtime's auto codecs-extra (HEVC libavcodec). --disable-rofiles-fuse is the
# container-safe path (no FUSE).
flatpak-builder --user --force-clean --disable-rofiles-fuse \
--install-deps-from=flathub \
--repo="$PWD/repo" \
"$PWD/build-dir" "$MANIFEST"
- name: Export single-file bundle
run: |
flatpak build-bundle "$PWD/repo" "$BUNDLE" "$APP_ID"
ls -lh "$BUNDLE"
- name: Publish to the Gitea generic registry
env:
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
BASE="https://$REGISTRY/api/packages/$OWNER/generic/$PACKAGE"
# 1) Immutable, versioned URL.
curl -fsS --user "enricobuehler:$TOKEN" --upload-file "$BUNDLE" \
"$BASE/$VERSION/$BUNDLE"
echo "published $BASE/$VERSION/$BUNDLE"
# 2) Stable `latest/punktfunk-client.flatpak` alias for the Decky fallback + scripts.
# The generic registry rejects re-uploading an existing version/file (409), so
# delete the prior `latest` file first (ignore 404 on the first ever run).
curl -fsS -o /dev/null --user "enricobuehler:$TOKEN" -X DELETE \
"$BASE/latest/punktfunk-client.flatpak" || true
curl -fsS --user "enricobuehler:$TOKEN" --upload-file "$BUNDLE" \
"$BASE/latest/punktfunk-client.flatpak"
echo "published $BASE/latest/punktfunk-client.flatpak"
- name: Attach bundle to the Gitea release (tags only)
if: startsWith(gitea.ref, 'refs/tags/')
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
API="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
ID=$(curl -sf -X POST "$API/releases" \
-H "Authorization: token $TOKEN" -H 'Content-Type: application/json' \
-d "{\"tag_name\":\"$GITHUB_REF_NAME\",\"name\":\"$GITHUB_REF_NAME\"}" \
| python3 -c 'import json,sys;print(json.load(sys.stdin)["id"])' \
|| curl -sf "$API/releases/tags/$GITHUB_REF_NAME" -H "Authorization: token $TOKEN" \
| python3 -c 'import json,sys;print(json.load(sys.stdin)["id"])')
curl -sf -X POST "$API/releases/$ID/assets?name=$BUNDLE" \
-H "Authorization: token $TOKEN" \
-F "attachment=@$BUNDLE" >/dev/null
echo "attached $BUNDLE to release $GITHUB_REF_NAME"
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 unom
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+49 -11
View File
@@ -49,12 +49,14 @@ Shells out to **`avahi-browse -rpt _punktfunk._udp`** (SteamOS and Bazzite ship
### Client launch (`connect()`) ### Client launch (`connect()`)
The client binary `punktfunk-client` is resolved in order: `PATH``/usr/bin` The client binary `punktfunk-client` is resolved in order: `PATH``/usr/bin`
`/usr/local/bin``~/.local/bin` → a `flatpak run earth.buehler.punktfunk.Client` `/usr/local/bin``~/.local/bin` → a `flatpak run io.unom.Punktfunk` fallback. The resolved
fallback. The resolved argv and a clear `client-not-found` error surface to the UI. The argv and a clear `client-not-found` error surface to the UI. The child PID is tracked so
child PID is tracked so `disconnect()` (and plugin `_unload`) can terminate it. `disconnect()` (and plugin `_unload`) can terminate it.
> **TODO:** pin the canonical SteamOS install path once a Deck packaging story for > On the **Steam Deck** the client install is the flatpak `io.unom.Punktfunk`
> `punktfunk-client` is settled (likely a flatpak, since SteamOS `/usr` is read-only). > (`packaging/flatpak/`) — SteamOS `/usr` is read-only and lacks `libadwaita`/`libSDL3`, so
> the flatpak (which bundles them) is the canonical path; the resolver's flatpak fallback
> launches exactly that.
## Prerequisites ## Prerequisites
@@ -76,15 +78,51 @@ pnpm build # rollup → dist/index.js
## Install on the Deck ## Install on the Deck
Copy the built plugin directory to the Deck and restart Decky: ### Option A — Decky "install from URL" (recommended; published by CI)
```sh CI (`.gitea/workflows/decky.yml`) builds the plugin into a store-layout zip and publishes it to
# the dir must contain: dist/, main.py, plugin.json, package.json Gitea's **generic package registry** on every push to `main` and on `v*` tags, exposing a stable
rsync -a --exclude node_modules clients/decky/ deck@<deck-ip>:~/homebrew/plugins/punktfunk/ URL. In Decky's settings → **Developer Mode****Install Plugin from URL**, paste:
# then, on the Deck, restart Decky Loader (Settings → Developer → "Restart" / reboot)
```
https://git.unom.io/api/packages/unom/generic/punktfunk-decky/latest/punktfunk.zip
``` ```
The **punktfunk** panel then appears in the Quick Access Menu. (or a pinned version: `.../punktfunk-decky/<version>/punktfunk.zip`). On tags the same zip is
also attached to the Gitea release. The zip's layout is the store-required one — a single
top-level `punktfunk/` dir holding `plugin.json`, `package.json`, `main.py`, `dist/index.js`,
`README.md`, and `LICENSE`.
### Option B — manual dev copy (sideload)
Decky's `~/homebrew/plugins/` is **root-owned** (PluginLoader runs as root and manages it), so a
plain `rsync` into it fails — stage to a writable temp dir, then `sudo`-install and restart the
loader. The two helper scripts do exactly this:
```sh
cd clients/decky
pnpm install
pnpm run package # → out/punktfunk/ + out/punktfunk-v<ver>.zip
DECK=deck@<deck-ip> pnpm run deploy # rsync → /tmp, sudo cp into plugins/, chown root, restart
```
`deploy.sh` prompts for the Deck's sudo password interactively (via `ssh -t`); set `DECKPASS=…`
to run it non-interactively. Equivalent by hand:
```sh
cd clients/decky && pnpm build && bash scripts/package.sh
rsync -azp --delete out/punktfunk/ deck@<deck-ip>:/tmp/punktfunk/
ssh -t deck@<deck-ip> 'sudo sh -c "rm -rf ~deck/homebrew/plugins/punktfunk && \
cp -r /tmp/punktfunk ~deck/homebrew/plugins/punktfunk && \
chown -R root:root ~deck/homebrew/plugins/punktfunk && systemctl restart plugin_loader"'
```
A loader restart is required for an out-of-band install to appear. The **punktfunk** panel then
shows up in the Quick Access Menu.
> The plugin launches the client via the flatpak `io.unom.Punktfunk` (see
> [`../../packaging/flatpak/README.md`](../../packaging/flatpak/README.md)) — install that on
> the Deck too, or the panel's Connect surfaces a `client-not-found` error.
## Limitations / next steps ## Limitations / next steps
+9 -6
View File
@@ -28,8 +28,9 @@ import decky
# name and let the spawn fail loudly — install the client on the Deck (.deb / RPM / flatpak) # name and let the spawn fail loudly — install the client on the Deck (.deb / RPM / flatpak)
# or symlink it into ~/.local/bin. # or symlink it into ~/.local/bin.
# #
# TODO: once a Steam Deck / SteamOS install path for punktfunk-client is settled (likely a # On SteamOS (read-only /usr, image-based) the settled install path is the flatpak
# flatpak, since SteamOS is image-based and /usr is read-only), pin the canonical path here. # ``io.unom.Punktfunk`` (packaging/flatpak/), launched via ``flatpak run`` — see the flatpak
# fallback in :func:`_resolve_client`.
CLIENT_BINARY = "punktfunk-client" CLIENT_BINARY = "punktfunk-client"
# Service type advertised by punktfunk/1 hosts (matches NATIVE_SERVICE in the Rust host). # Service type advertised by punktfunk/1 hosts (matches NATIVE_SERVICE in the Rust host).
@@ -59,12 +60,14 @@ def _resolve_client() -> list[str]:
if Path(candidate).exists(): if Path(candidate).exists():
return [candidate] return [candidate]
# Flatpak fallback. The app id is a guess until a flatpak is actually published; # Flatpak fallback — the canonical install path on the Steam Deck (SteamOS /usr is
# `flatpak run <id>` is a no-op-ish failure if it is not installed, which surfaces as a # read-only; the flatpak bundles the libadwaita + SDL3 the system lacks). The app id is
# spawn error the user can act on. # the one the flatpak manifest publishes (packaging/flatpak/io.unom.Punktfunk.yml). If it
# is not installed, `flatpak run <id>` fails and surfaces as a spawn error the user can
# act on (install the bundle: `flatpak install --user punktfunk-client-*.flatpak`).
flatpak = shutil.which("flatpak") flatpak = shutil.which("flatpak")
if flatpak: if flatpak:
return [flatpak, "run", "earth.buehler.punktfunk.Client"] return [flatpak, "run", "io.unom.Punktfunk"]
decky.logger.warning( decky.logger.warning(
"punktfunk-client not found on PATH or in %s; falling back to bare name", "punktfunk-client not found on PATH or in %s; falling back to bare name",
+3 -1
View File
@@ -6,6 +6,8 @@
"scripts": { "scripts": {
"build": "rollup -c", "build": "rollup -c",
"watch": "rollup -c -w", "watch": "rollup -c -w",
"package": "pnpm build && bash scripts/package.sh",
"deploy": "bash scripts/deploy.sh",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"keywords": [ "keywords": [
@@ -15,7 +17,7 @@
"game-streaming" "game-streaming"
], ],
"author": "enrico", "author": "enrico",
"license": "BSD-3-Clause", "license": "Apache-2.0 OR MIT",
"dependencies": { "dependencies": {
"@decky/api": "^1.1.3", "@decky/api": "^1.1.3",
"react-icons": "^5.3.0", "react-icons": "^5.3.0",
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# Deploy the staged plugin tree (out/<name>, produced by scripts/package.sh) into a Steam
# Deck's ~/homebrew/plugins/, then restart Decky Loader.
#
# Decky's plugins dir is ROOT-OWNED (PluginLoader runs as root and manages it), so the install
# step needs sudo on the Deck. We rsync to a deck-writable /tmp first (no privilege), then
# sudo-copy into place. Out-of-band installs only appear after a loader restart.
#
# Usage:
# DECK=deck@192.168.1.235 bash scripts/deploy.sh # interactive sudo (prompts on the Deck)
# DECK=deck@192.168.1.235 DECKPASS=... bash scripts/deploy.sh # non-interactive (scripted/CI)
set -euo pipefail
HERE="$(cd "$(dirname "$0")/.." && pwd)"
DECK="${DECK:?set DECK=deck@<ip>}"
NAME="$(python3 -c 'import json;print(json.load(open("'"$HERE"'/plugin.json"))["name"])')"
STAGE_LOCAL="$HERE/out/$NAME"
[ -d "$STAGE_LOCAL" ] || { echo "$STAGE_LOCAL missing — run scripts/package.sh first" >&2; exit 1; }
# 1. push to a deck-writable temp dir (deck owns its $HOME)
rsync -azp --delete -e ssh "$STAGE_LOCAL/" "$DECK:/tmp/$NAME/"
# 2. sudo-install into the root-owned plugins dir, match Decky's root:root ownership, reload
INSTALL="rm -rf /home/deck/homebrew/plugins/$NAME \
&& cp -r /tmp/$NAME /home/deck/homebrew/plugins/$NAME \
&& chown -R root:root /home/deck/homebrew/plugins/$NAME \
&& rm -rf /tmp/$NAME \
&& systemctl restart plugin_loader"
if [ -n "${DECKPASS:-}" ]; then
ssh "$DECK" "echo '$DECKPASS' | sudo -S sh -c '$INSTALL'"
else
echo "==> sudo on the Deck will prompt for ${DECK}'s password:"
ssh -t "$DECK" "sudo sh -c '$INSTALL'"
fi
echo "deployed $NAME$DECK:~/homebrew/plugins/$NAME and restarted plugin_loader"
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# Assemble the Decky plugin into the canonical store/sideload layout:
#
# out/punktfunk-v<version>.zip -> punktfunk/{dist/index.js,main.py,plugin.json,
# package.json,decky.pyi,LICENSE,README.md}
# out/punktfunk/ (the same tree, unzipped — rsync this with scripts/deploy.sh)
#
# Decky extracts the zip with --strip-components=1, so the single top-level dir MUST equal
# plugin.json "name". Run after `pnpm build` (or use `pnpm run package`). Host-agnostic: needs
# only bash, python3 and zip.
set -euo pipefail
HERE="$(cd "$(dirname "$0")/.." && pwd)"
cd "$HERE"
[ -f dist/index.js ] || { echo "dist/index.js missing — run 'pnpm build' first" >&2; exit 1; }
[ -f LICENSE ] || { echo "LICENSE missing (required by the Decky store)" >&2; exit 1; }
NAME="$(python3 -c 'import json;print(json.load(open("plugin.json"))["name"])')"
VER="$(python3 -c 'import json;print(json.load(open("package.json"))["version"])')"
STAGE="$(mktemp -d)"
DEST="$STAGE/$NAME"
mkdir -p "$DEST/dist"
cp dist/index.js "$DEST/dist/index.js" # ship the bundle only, not the sourcemap
cp main.py plugin.json package.json LICENSE "$DEST/"
[ -f decky.pyi ] && cp decky.pyi "$DEST/"
[ -f README.md ] && cp README.md "$DEST/"
OUT="$HERE/out"
mkdir -p "$OUT"
ZIP="$OUT/${NAME}-v${VER}.zip"
rm -f "$ZIP"
( cd "$STAGE" && zip -r -X "$ZIP" "$NAME" >/dev/null )
# Leave an unzipped staging tree for the rsync/sudo deploy path (scripts/deploy.sh).
rm -rf "$OUT/$NAME" && cp -r "$DEST" "$OUT/$NAME"
rm -rf "$STAGE"
echo "built $ZIP"
echo "staged $OUT/$NAME (deploy with: DECK=deck@<ip> bash scripts/deploy.sh)"
+6 -1
View File
@@ -95,12 +95,17 @@ systemctl --user enable --now punktfunk-host
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.
## Why not Flatpak? ## Why not Flatpak (for the HOST)?
The host needs unsandboxed access the zero-copy NVENC path, `/dev/uinput`, the PipeWire The host needs unsandboxed access the zero-copy NVENC path, `/dev/uinput`, the PipeWire
graph and the compositor's privileged protocols — a Flatpak sandbox fights all of these. graph and the compositor's privileged protocols — a Flatpak sandbox fights all of these.
An RPM (or the bootc layer) installs into the host system where those just work. An RPM (or the bootc layer) installs into the host system where those just work.
> 👉 The **client** is a different story — it IS shipped as a Flatpak (the only viable
> Steam Deck install path: SteamOS `/usr` is read-only and lacks `libadwaita`/`libSDL3`). See
> [`flatpak/README.md`](flatpak/README.md). The client sandbox only needs the GPU render node,
> Wayland, PipeWire audio, the network and hidraw — all expressible as finish-args.
## Building the SRPM/RPM locally (Fedora only) ## Building the SRPM/RPM locally (Fedora only)
```sh ```sh
+9
View File
@@ -0,0 +1,9 @@
# Generated by flatpak-cargo-generator.py from Cargo.lock in CI (offline crate cache for the
# manifest's punktfunk-client module). Regenerated on every build — never commit it.
cargo-sources.json
# flatpak-builder scratch / output (when building locally in this dir).
.flatpak-builder/
build-dir/
repo/
*.flatpak
+122
View File
@@ -0,0 +1,122 @@
# punktfunk client — Flatpak (Steam Deck / SteamOS, and any flatpak distro)
The native Linux **client** (crate `punktfunk-client-linux`, binary `punktfunk-client`) is
published as a single-file **`.flatpak` bundle** to **Gitea's generic package registry** in
the public `unom` org. CI (`.gitea/workflows/flatpak.yml`) builds and publishes on every push
to `main` (a rolling `0.0.1-ciN.<sha>` build) and on `v*` tags (a clean `X.Y.Z`), and on tags
also attaches the bundle to the Gitea release.
> The **host** is NOT a flatpak (it needs unsandboxed `/dev/uinput` + zero-copy NVENC — see
> [`../README.md`](../README.md) "Why not Flatpak"). Only the **client** is sandbox-friendly.
## Why flatpak for the Steam Deck
SteamOS `/usr` is read-only and image-based, and the system is **missing `libadwaita` and
`libSDL3`** — so a bare `punktfunk-client` binary dropped into `~/.local/bin` won't run. Flatpak
is the Deck's native, update-survivable app path (the user already runs Moonlight and chiaki-ng
as flatpaks), and the bundle carries libadwaita (from `org.gnome.Platform//50`) + a bundled SDL3,
with HEVC-capable FFmpeg supplied automatically by the runtime's `codecs-extra` extension.
App id: **`io.unom.Punktfunk`** (matches the Apple bundle id family and the Decky plugin's
flatpak fallback).
## Install on the Deck (one-time)
The generic registry is a plain HTTP file store, so just download the bundle and install it
per-user (no root, survives SteamOS updates):
```sh
# Pick a version: a tag like 1.2.3, or the newest main build's 0.0.1-ciN.gSHA.
VER=1.2.3
URL="https://git.unom.io/api/packages/unom/generic/punktfunk-client-flatpak/$VER/punktfunk-client-$VER.flatpak"
# Flathub must be enabled (it is on the Deck) so the GNOME runtime + ffmpeg-full pull in:
flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
curl -fL -o /tmp/punktfunk-client.flatpak "$URL"
flatpak install --user --bundle /tmp/punktfunk-client.flatpak
```
Run it:
```sh
flatpak run io.unom.Punktfunk # GUI host list (mDNS)
flatpak run io.unom.Punktfunk --connect HOST:PORT
```
The **Decky plugin** launches exactly this (`flatpak run io.unom.Punktfunk --connect …`) once
installed — see [`../../clients/decky/README.md`](../../clients/decky/README.md).
## Updates
A bundle has no remote to track, so updates are "download the newer bundle and reinstall":
```sh
flatpak install --user --bundle /tmp/punktfunk-client.flatpak # same command, newer file
```
(If you want `flatpak update` to track new builds automatically you'd need a hosted OSTree
repo, which Gitea cannot serve — see "Alternatives" below. The bundle is the simplest path.)
## Build locally / the CI fallback
CI builds this in a **`--privileged`** Fedora container, because `flatpak-builder` runs
`bubblewrap`, which needs user namespaces the default Docker executor denies. **If the Gitea
runner can't grant `--privileged`** (the job fails at `flatpak-builder` with
*"Creating new namespace failed: Operation not permitted"*), build it out-of-band and upload
by hand. The easiest place is **on the Deck itself** (it can run `org.flatpak.Builder`
user-scope, no root):
```sh
# On the Deck (or any flatpak box), one-time:
flatpak install --user -y flathub org.flatpak.Builder
# build-flatpak.sh auto-detects org.flatpak.Builder, generates cargo-sources.json (or reuses an
# existing one — see below), builds, and exports dist/punktfunk-client-<version>.flatpak:
bash packaging/flatpak/build-flatpak.sh
# Upload to the generic registry (PAT with write:package):
curl -fsS --user "enricobuehler:$REGISTRY_TOKEN" \
--upload-file dist/punktfunk-client-*.flatpak \
"https://git.unom.io/api/packages/unom/generic/punktfunk-client-flatpak/0.0.1-manual/punktfunk-client.flatpak"
```
> `cargo-sources.json` generation needs `python3` + `aiohttp` + `tomlkit`, which the Deck lacks.
> Generate it on a dev box (`build-flatpak.sh` does it, or run the upstream
> `flatpak-cargo-generator.py Cargo.lock -o packaging/flatpak/cargo-sources.json`), rsync it next
> to the manifest, and `build-flatpak.sh` reuses it (it only regenerates when the file is absent
> or `FORCE_GEN=1`).
> The Mac build host **cannot** build a Linux flatpak (no flatpak-builder for macOS), and
> home-worker-2 has no flatpak and no passwordless sudo to install it — so the Deck or the
> privileged CI container are the only two viable build sites.
## Manifest
[`io.unom.Punktfunk.yml`](io.unom.Punktfunk.yml). Runtime `org.gnome.Platform//50`
(GTK 4.20 + libadwaita 1.8 ≥ the crate floors of v4_16 / v1_5), built on freedesktop-sdk 25.08,
with two build-time SDK extensions: `org.freedesktop.Sdk.Extension.rust-stable` (→ //25.08,
**rustc 1.96** — the GTK4 dep chain, e.g. pango-sys 0.22, needs ≥ 1.92, which the EOL GNOME-48 /
24.08 rust-stable at 1.89 could not provide) and `org.freedesktop.Sdk.Extension.llvm20` (libclang,
needed by bindgen in ffmpeg-sys-next / sdl3-sys). HEVC-capable libavcodec (soname 61, accepted by
ffmpeg-next 8.x) is supplied **automatically at runtime** by the freedesktop `codecs-extra`
extension point (auto-downloaded with the runtime; no app-side codec declaration). A bundled
**SDL3 3.4.10** module (pinned to match `sdl3-sys 0.6.6+SDL-3.4.10`), and finish-args for Wayland +
`--device=all` (GPU/VAAPI render node + evdev + the hidraw char-devices SDL3 needs for DualSense)
+ `--socket=pulseaudio` (PipeWire-pulse: playback + mic) + `--share=network`. Alongside it:
`io.unom.Punktfunk.desktop`, `io.unom.Punktfunk.metainfo.xml`, `io.unom.Punktfunk.svg` (all
installed by the manifest). `cargo-sources.json` (the offline crate cache) is a pure function of
`Cargo.lock`; CI regenerates it each build and it is **gitignored** — generate it on any box with
network + `python3`/`aiohttp`/`tomlkit` (`build-flatpak.sh` does this automatically) and, for a
build host that lacks those (the Deck), rsync the generated file in alongside the manifest.
## Alternatives considered (and why the bundle wins)
- **Generic registry bundle (chosen):** one curl to publish, one `flatpak install --bundle` to
consume; mirrors the existing deb/rpm curl-upload pattern exactly. No auto-update.
- **Release attachment:** also done on tags (the bundle is attached to the Gitea release), good
for a human-facing download page; the generic registry gives the stable per-version URL the
Decky fallback and scripts use.
- **Self-hosted OSTree repo (rejected):** would enable `flatpak update`, but Gitea has no
flatpak/ostree registry, so it would mean serving a static OSTree tree over Gitea raw/Pages —
more moving parts than the appliance needs today.
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
# Build the punktfunk Linux client as a single-file `.flatpak` bundle.
#
# Works on the Steam Deck (org.flatpak.Builder from Flathub, user-scope, NO root) and on any
# Linux box with flatpak + flatpak-builder. The CI does the same steps (.gitea/workflows/flatpak.yml).
#
# On the Deck (one-time):
# flatpak install --user -y flathub org.flatpak.Builder
# Then run this script from the repo root:
# bash packaging/flatpak/build-flatpak.sh
# Output: dist/punktfunk-client-<version>.flatpak (install with `flatpak install --user <file>`)
#
# Env knobs:
# VERSION=... version string for the bundle name (default: git describe / 0.0.1-dev)
# ONLINE=1 skip offline cargo-sources.json; build with --share=network (fast local
# iteration, non-reproducible). Default: offline (regenerates cargo-sources).
# BUILDER=... override the flatpak-builder invocation (default: auto-detect host
# flatpak-builder, else `flatpak run org.flatpak.Builder`).
set -euo pipefail
ROOTDIR="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$ROOTDIR"
APP_ID="io.unom.Punktfunk"
MANIFEST="packaging/flatpak/io.unom.Punktfunk.yml"
VERSION="${VERSION:-$(git describe --tags --always --dirty 2>/dev/null || echo 0.0.1-dev)}"
VERSION="${VERSION#v}"
BUNDLE="dist/punktfunk-client-${VERSION}.flatpak"
# --- pick a flatpak-builder (host binary, or the org.flatpak.Builder flatpak on the Deck) ---
if [ -n "${BUILDER:-}" ]; then
FPB=($BUILDER)
elif command -v flatpak-builder >/dev/null 2>&1; then
FPB=(flatpak-builder)
elif flatpak info org.flatpak.Builder >/dev/null 2>&1; then
FPB=(flatpak run org.flatpak.Builder)
else
echo "error: need flatpak-builder. On the Deck: flatpak install --user -y flathub org.flatpak.Builder" >&2
exit 1
fi
# --- ensure Flathub is available for the runtime/SDK/extensions ---
flatpak remote-add --user --if-not-exists flathub \
https://dl.flathub.org/repo/flathub.flatpakrepo
# --- offline crate cache (skip with ONLINE=1) -------------------------------------------
EXTRA_ARGS=()
if [ "${ONLINE:-0}" = "1" ]; then
echo "==> ONLINE build (cargo fetches from crates.io; non-reproducible)"
EXTRA_ARGS+=(--build-args=--share=network)
# The manifest references cargo-sources.json; provide an empty list so it stays valid.
[ -f packaging/flatpak/cargo-sources.json ] || echo '[]' > packaging/flatpak/cargo-sources.json
elif [ -f packaging/flatpak/cargo-sources.json ] && [ "${FORCE_GEN:-0}" != "1" ]; then
# Reuse a cargo-sources.json that was generated elsewhere (e.g. on a dev box with network +
# python aiohttp/toml, then rsynced to a build host that lacks them — like the Deck). The
# offline crate cache is a pure function of Cargo.lock, so this is reproducible. FORCE_GEN=1
# to regenerate anyway.
echo "==> reusing existing packaging/flatpak/cargo-sources.json (FORCE_GEN=1 to regenerate)"
else
echo "==> generating offline cargo-sources.json from Cargo.lock"
GEN=/tmp/flatpak-cargo-generator.py
if [ ! -f "$GEN" ]; then
curl -fsSL -o "$GEN" \
https://raw.githubusercontent.com/flatpak/flatpak-builder-tools/master/cargo/flatpak-cargo-generator.py
fi
# Needs python3 + aiohttp + toml. On a host that lacks them (e.g. the Deck), generate on the
# Mac / a dev box instead and rsync the result next to the manifest (reused by the branch above).
python3 "$GEN" Cargo.lock -o packaging/flatpak/cargo-sources.json
fi
# --- build into a local ostree repo, then export a single-file bundle --------------------
echo "==> flatpak-builder ($APP_ID, version $VERSION)"
"${FPB[@]}" --user --force-clean --disable-rofiles-fuse \
--install-deps-from=flathub \
"${EXTRA_ARGS[@]}" \
--repo="$ROOTDIR/.flatpak-repo" \
"$ROOTDIR/.flatpak-build" "$MANIFEST"
mkdir -p dist
flatpak build-bundle "$ROOTDIR/.flatpak-repo" "$BUNDLE" "$APP_ID"
echo "built $BUNDLE"
ls -lh "$BUNDLE"
echo
echo "install: flatpak install --user -y $BUNDLE"
echo "run: flatpak run $APP_ID (or: flatpak run $APP_ID --connect host:port)"
@@ -0,0 +1,10 @@
[Desktop Entry]
Type=Application
Name=Punktfunk
Comment=Stream a remote punktfunk host
Exec=punktfunk-client
Icon=io.unom.Punktfunk
Terminal=false
Categories=Network;Game;
Keywords=streaming;remote;game;moonlight;
StartupNotify=true
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- AppStream metainfo for the punktfunk Linux client flatpak. Validate with:
appstreamcli validate packaging/flatpak/io.unom.Punktfunk.metainfo.xml
The component id MUST equal the flatpak app-id; the <launchable> MUST name the installed
desktop file (io.unom.Punktfunk.desktop). -->
<component type="desktop-application">
<id>io.unom.Punktfunk</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>MIT OR Apache-2.0</project_license>
<name>Punktfunk</name>
<summary>Low-latency desktop and game streaming client</summary>
<description>
<p>
Punktfunk is the native Linux client for the punktfunk low-latency desktop and game
streaming stack. It discovers hosts on the LAN over mDNS, trusts them via certificate
pinning with a SPAKE2 PIN pairing ceremony, and streams HEVC video (GF(2^16) Leopard
FEC plus AES-GCM over UDP, with a QUIC control plane) at exactly the requested
resolution and refresh rate — no scaling.
</p>
<p>Features:</p>
<ul>
<li>Hardware-accelerated HEVC decode (VAAPI zero-copy on AMD and Intel, software fallback)</li>
<li>PipeWire audio playback and microphone uplink</li>
<li>Full gamepad support including DualSense touchpad, motion, adaptive triggers and lightbar (SDL3)</li>
<li>LAN host discovery, TOFU fingerprint pinning and PIN pairing</li>
</ul>
</description>
<launchable type="desktop-id">io.unom.Punktfunk.desktop</launchable>
<categories>
<category>Network</category>
<category>Game</category>
<category>RemoteAccess</category>
</categories>
<keywords>
<keyword>streaming</keyword>
<keyword>remote</keyword>
<keyword>game</keyword>
<keyword>moonlight</keyword>
</keywords>
<url type="homepage">https://git.unom.io/unom/punktfunk</url>
<url type="bugtracker">https://git.unom.io/unom/punktfunk/issues</url>
<url type="vcs-browser">https://git.unom.io/unom/punktfunk</url>
<developer id="io.unom">
<name>unom</name>
</developer>
<content_rating type="oars-1.1"/>
<branding>
<color type="primary" scheme_preference="light">#a79ff8</color>
<color type="primary" scheme_preference="dark">#6c5bf3</color>
</branding>
<!-- Bump on each release; the version/date should track the published bundle. -->
<releases>
<release version="0.0.1" date="2026-06-15">
<description>
<p>Initial flatpak packaging of the native Linux client for the Steam Deck and Wayland desktops.</p>
</description>
</release>
</releases>
</component>
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="1000" height="1000" viewBox="0 0 1000 1000" version="1.1"
xmlns="http://www.w3.org/2000/svg"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<!-- Brand mark: three overlapping circles (flattened from clients/apple punktfunk_Logo.icon).
Order back-to-front: large violet circle (layer 1), deep-purple circle (layer 2),
light lens highlight (layer 3). -->
<rect x="0" y="0" width="1000" height="1000" rx="180" ry="180" fill="#1c1530"/>
<path d="M403.037,791.672c107.586,0 194.41,-86.824 194.41,-194.41c0,-107.586 -86.824,-194.41 -194.41,-194.41c-107.586,0 -194.41,86.824 -194.41,194.41c0,107.586 86.824,194.41 194.41,194.41Z" fill="#a79ff8"/>
<path d="M735.276,540.321c76.075,-76.075 76.075,-198.862 0,-274.937c-76.075,-76.075 -198.862,-76.075 -274.937,0c-76.075,76.075 -76.075,198.862 0,274.937c76.075,76.075 198.862,76.075 274.937,0Z" fill="#6c5bf3"/>
<path d="M647.84,590.737c-64.853,17.403 -136.871,0.597 -187.885,-50.416c-51.013,-51.013 -67.819,-123.032 -50.416,-187.885c64.853,-17.403 136.871,-0.597 187.885,50.416c51.013,51.013 67.819,123.032 50.416,187.885Z" fill="#d2c9fb"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+159
View File
@@ -0,0 +1,159 @@
# Flatpak manifest for the native punktfunk Linux client (crate punktfunk-client-linux,
# binary `punktfunk-client`). Built into a single-file `.flatpak` bundle and published to
# Gitea's generic package registry (see .gitea/workflows/flatpak.yml + packaging/flatpak/README.md).
#
# Why flatpak for the CLIENT (the host stays an RPM/deb — see packaging/README.md "Why not
# Flatpak"): on SteamOS the Steam Deck's /usr is read-only and image-based, so a bare
# `punktfunk-client` binary in ~/.local/bin can't bring its own libadwaita / SDL3 (both
# MISSING from the SteamOS system) — but flatpak is the Deck's native, update-survivable app
# path (the user already runs Moonlight + chiaki-ng as flatpaks). Unlike the host, the client
# is sandbox-friendly: it only needs the GPU render node, the host PipeWire socket, the
# network, Wayland, hidraw for DualSense, and its config dir — all expressible as finish-args.
#
# Runtime: GNOME 50 ships GTK 4.20 and libadwaita 1.8 — both far exceed the crate floors
# (gtk4 0.11 "v4_16", libadwaita 0.9 "v1_5"). GNOME 50 is built on freedesktop-sdk 25.08, so
# `org.freedesktop.Sdk.Extension.rust-stable` resolves to //25.08 (rustc 1.96 — the GTK4 dep
# chain, e.g. pango-sys 0.22, needs >= 1.92, which the older GNOME-48/24.08 rust-stable at 1.89
# could NOT satisfy). GNOME 50 is also a *supported* runtime (GNOME 48 went EOL in March 2026).
# libopus and the PipeWire client lib are in the freedesktop base; SDL3 is NOT, so it is built
# from source as a bundled module.
#
# HEVC decode: the base runtime's libavcodec is a stripped build (no encumbered codecs). The
# freedesktop runtime declares `org.freedesktop.Platform.codecs-extra` as a built-in extension
# point (directory lib/x86_64-linux-gnu/codecs-extra, add-ld-path lib, auto-downloaded with the
# runtime), whose full libavcodec.so.61 transparently shadows the base one at runtime. So HEVC
# (software + VAAPI) works with NO app-side codec extension to declare — we just build against
# the SDK's linkable libavcodec.so.61 and let the runtime swap in the capable build.
app-id: io.unom.Punktfunk
runtime: org.gnome.Platform
runtime-version: '50'
sdk: org.gnome.Sdk
# Build-time SDK extensions:
# - rust-stable: cargo/rustc 1.96 + the bundled mold linker (/usr/lib/sdk/rust-stable/bin).
# - llvm20: provides libclang (/usr/lib/sdk/llvm20/lib), which bindgen needs — ffmpeg-sys-next
# and sdl3-sys generate their FFI bindings via bindgen at build time. The base SDK ships no
# clang/libclang, so without this the build panics ("Unable to find libclang").
# Both are added to PATH / LIBCLANG_PATH in build-options below.
sdk-extensions:
- org.freedesktop.Sdk.Extension.rust-stable
- org.freedesktop.Sdk.Extension.llvm20
command: punktfunk-client
cleanup:
- /include
- /lib/pkgconfig
- /lib/cmake
- /share/aclocal
- /man
- /share/man
- '*.a'
- '*.la'
finish-args:
# --- display ---
- --socket=wayland # GTK4 native Wayland window (the client is Wayland-first)
- --socket=fallback-x11 # Xwayland fallback when no Wayland socket is exposed
- --share=ipc # required alongside X11 for shared-memory surfaces
# --- GPU + all input devices ---
# --device=all (not just --device=dri): covers the GPU render node (VAAPI HEVC decode + GL),
# evdev joysticks, AND the hidraw CHAR devices SDL3's HIDAPI needs for DualSense touchpad/
# motion/adaptive-triggers/lightbar. flatpak cannot bind individual /dev/hidrawN via
# --filesystem (they are char devices — "unsupported type 0o20000"), and there is no granular
# --device=hidraw; --device=all is what game/emulator flatpaks (RetroArch, Dolphin) use. We
# self-host via the Gitea generic registry — NOT Flathub — so its --device=all review rule
# does not apply.
- --device=all
- --filesystem=/run/udev:ro # SDL/HIDAPI enumerates devices via udev
# --- audio: PipeWire via its PulseAudio shim — covers playback AND mic uplink. SteamOS
# exposes PipeWire-pulse here; --socket=pulseaudio is the portable arg Moonlight/chiaki
# also use on the Deck (a bare --socket=pipewire would also need the camera/portal dance
# for capture; the pulse shim gives mic + speaker in one grant). ---
- --socket=pulseaudio
# --- network: QUIC control + UDP data plane + mDNS discovery (_punktfunk._udp) ---
- --share=network
# --- persistent client identity / pairing store (shared with punktfunk-client-rs) ---
- --filesystem=~/.config/punktfunk:create # client-{cert,key}.pem, known-hosts, settings
build-options:
append-path: /usr/lib/sdk/rust-stable/bin:/usr/lib/sdk/llvm20/bin
# The rust build resolves everything via pkg-config: gtk4/libadwaita/pipewire/opus AND a
# linkable libavcodec.so.61 from org.gnome.Sdk//50 (the multiarch /usr dir), plus the bundled
# SDL3's .pc from /app. (At runtime the codecs-extra extension swaps in the HEVC-capable
# libavcodec — see the header.)
env:
PKG_CONFIG_PATH: /app/lib/pkgconfig:/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/lib/pkgconfig
# bindgen (ffmpeg-sys-next / sdl3-sys) loads libclang from the llvm20 extension.
LIBCLANG_PATH: /usr/lib/sdk/llvm20/lib
# mold (shipped in rust-stable) speeds the ~450-crate link on the Deck APU.
RUSTFLAGS: -C link-arg=-fuse-ld=mold
modules:
# ---------------------------------------------------------------------------------------
# SDL3 — NOT provided as a linkable libSDL3.so.0 by org.gnome.Platform/freedesktop-sdk
# 25.08, and there is no SDL3 recipe in flathub/shared-modules. Build it from source.
# Pinned to 3.4.10 to match the crate exactly: sdl3-sys is `0.6.6+SDL-3.4.10`, i.e. its
# bindings target SDL 3.4.10 — building an older SDL risks missing symbols at link time.
# HIDAPI is enabled (DualSense touchpad/motion/triggers/lightbar over hidraw).
# ---------------------------------------------------------------------------------------
- name: sdl3
buildsystem: cmake-ninja
config-opts:
- -DCMAKE_BUILD_TYPE=Release
- -DSDL_SHARED=ON
- -DSDL_STATIC=OFF
- -DSDL_HIDAPI=ON # DualSense full fidelity over hidraw
- -DSDL_TEST_LIBRARY=OFF
- -DSDL_EXAMPLES=OFF
sources:
- type: archive
url: https://github.com/libsdl-org/SDL/releases/download/release-3.4.10/SDL3-3.4.10.tar.gz
# `sha256sum SDL3-3.4.10.tar.gz` (verified 2026-06-15). Bump url + sha together.
sha256: 12b34280415ec8418c864408b93d008a20a6530687ee613d60bfbd20411f2785
x-checker-data:
type: anitya
project-id: 4974
stable-only: true
url-template: https://github.com/libsdl-org/SDL/releases/download/release-$version/SDL3-$version.tar.gz
cleanup:
- /bin
- /include
- /lib/cmake
- /lib/pkgconfig
# ---------------------------------------------------------------------------------------
# The client. cargo-sources.json is the GENERATED offline crate cache:
# python3 flatpak-cargo-generator.py Cargo.lock -o packaging/flatpak/cargo-sources.json
# (run from the repo root; the CI step does exactly this). With it present the build is fully
# offline (CARGO_NET_OFFLINE). For quick LOCAL iteration WITHOUT regenerating it, drop the
# cargo-sources.json source and pass --build-args=--share=network to flatpak-builder
# (non-reproducible; cargo fetches from crates.io during the build).
# ---------------------------------------------------------------------------------------
- name: punktfunk-client
buildsystem: simple
build-options:
env:
CARGO_HOME: /run/build/punktfunk-client/cargo
CARGO_NET_OFFLINE: 'true'
build-commands:
- cargo --offline build --release --locked -p punktfunk-client-linux
- install -Dm0755 target/release/punktfunk-client ${FLATPAK_DEST}/bin/punktfunk-client
# Desktop entry (renamed to the app id; Exec is the in-sandbox binary).
- install -Dm0644 packaging/flatpak/io.unom.Punktfunk.desktop
${FLATPAK_DEST}/share/applications/io.unom.Punktfunk.desktop
# AppStream metainfo (required for a well-formed flatpak / Software listings).
- install -Dm0644 packaging/flatpak/io.unom.Punktfunk.metainfo.xml
${FLATPAK_DEST}/share/metainfo/io.unom.Punktfunk.metainfo.xml
# Scalable icon named for the app id (GNOME runtime renders SVG via librsvg).
- install -Dm0644 packaging/flatpak/io.unom.Punktfunk.svg
${FLATPAK_DEST}/share/icons/hicolor/scalable/apps/io.unom.Punktfunk.svg
sources:
# The repo checkout. For a Flathub/published build, replace with a pinned git source:
# - type: git
# url: https://git.unom.io/unom/punktfunk
# tag: vX.Y.Z
# commit: <sha>
# For ON-DECK / CI builds we build the checked-out working tree in place:
- type: dir
path: ../..
# Generated offline crate cache (see the comment block above). Remove for --share=network.
- cargo-sources.json