From 8956bc14de9c85bee3d165453bf7eafb46a3cbc1 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Mon, 15 Jun 2026 01:43:35 +0200 Subject: [PATCH] feat(packaging/flatpak,decky): Steam Deck client flatpak + plugin deploy + CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .gitea/workflows/decky.yml | 126 ++++++++++++++ .gitea/workflows/flatpak.yml | 129 ++++++++++++++ clients/decky/LICENSE | 21 +++ clients/decky/README.md | 60 +++++-- clients/decky/main.py | 15 +- clients/decky/package.json | 4 +- clients/decky/scripts/deploy.sh | 35 ++++ clients/decky/scripts/package.sh | 39 +++++ packaging/README.md | 7 +- packaging/flatpak/.gitignore | 9 + packaging/flatpak/README.md | 122 ++++++++++++++ packaging/flatpak/build-flatpak.sh | 85 ++++++++++ packaging/flatpak/io.unom.Punktfunk.desktop | 10 ++ .../flatpak/io.unom.Punktfunk.metainfo.xml | 68 ++++++++ packaging/flatpak/io.unom.Punktfunk.svg | 12 ++ packaging/flatpak/io.unom.Punktfunk.yml | 159 ++++++++++++++++++ 16 files changed, 882 insertions(+), 19 deletions(-) create mode 100644 .gitea/workflows/decky.yml create mode 100644 .gitea/workflows/flatpak.yml create mode 100644 clients/decky/LICENSE create mode 100755 clients/decky/scripts/deploy.sh create mode 100755 clients/decky/scripts/package.sh create mode 100644 packaging/flatpak/.gitignore create mode 100644 packaging/flatpak/README.md create mode 100755 packaging/flatpak/build-flatpak.sh create mode 100644 packaging/flatpak/io.unom.Punktfunk.desktop create mode 100644 packaging/flatpak/io.unom.Punktfunk.metainfo.xml create mode 100644 packaging/flatpak/io.unom.Punktfunk.svg create mode 100644 packaging/flatpak/io.unom.Punktfunk.yml diff --git a/.gitea/workflows/decky.yml b/.gitea/workflows/decky.yml new file mode 100644 index 0000000..842a5b2 --- /dev/null +++ b/.gitea/workflows/decky.yml @@ -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//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. 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" diff --git a/.gitea/workflows/flatpak.yml b/.gitea/workflows/flatpak.yml new file mode 100644 index 0000000..211a113 --- /dev/null +++ b/.gitea/workflows/flatpak.yml @@ -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 .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// +# GET https://git.unom.io/api/packages/unom/generic/punktfunk-client-flatpak// +# 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 (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" diff --git a/clients/decky/LICENSE b/clients/decky/LICENSE new file mode 100644 index 0000000..f42d1f9 --- /dev/null +++ b/clients/decky/LICENSE @@ -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. diff --git a/clients/decky/README.md b/clients/decky/README.md index b966551..78d6474 100644 --- a/clients/decky/README.md +++ b/clients/decky/README.md @@ -49,12 +49,14 @@ Shells out to **`avahi-browse -rpt _punktfunk._udp`** (SteamOS and Bazzite ship ### Client launch (`connect()`) The client binary `punktfunk-client` is resolved in order: `PATH` → `/usr/bin` → -`/usr/local/bin` → `~/.local/bin` → a `flatpak run earth.buehler.punktfunk.Client` -fallback. The resolved argv and a clear `client-not-found` error surface to the UI. The -child PID is tracked so `disconnect()` (and plugin `_unload`) can terminate it. +`/usr/local/bin` → `~/.local/bin` → a `flatpak run io.unom.Punktfunk` fallback. The resolved +argv and a clear `client-not-found` error surface to the UI. The child PID is tracked so +`disconnect()` (and plugin `_unload`) can terminate it. -> **TODO:** pin the canonical SteamOS install path once a Deck packaging story for -> `punktfunk-client` is settled (likely a flatpak, since SteamOS `/usr` is read-only). +> On the **Steam Deck** the client install is the flatpak `io.unom.Punktfunk` +> (`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 @@ -76,15 +78,51 @@ pnpm build # rollup → dist/index.js ## 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 -# the dir must contain: dist/, main.py, plugin.json, package.json -rsync -a --exclude node_modules clients/decky/ deck@:~/homebrew/plugins/punktfunk/ -# then, on the Deck, restart Decky Loader (Settings → Developer → "Restart" / reboot) +CI (`.gitea/workflows/decky.yml`) builds the plugin into a store-layout zip and publishes it to +Gitea's **generic package registry** on every push to `main` and on `v*` tags, exposing a stable +URL. In Decky's settings → **Developer Mode** → **Install Plugin from URL**, paste: + +``` +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//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.zip +DECK=deck@ 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@:/tmp/punktfunk/ +ssh -t deck@ '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 diff --git a/clients/decky/main.py b/clients/decky/main.py index 7def47f..56001c8 100644 --- a/clients/decky/main.py +++ b/clients/decky/main.py @@ -28,8 +28,9 @@ import decky # name and let the spawn fail loudly — install the client on the Deck (.deb / RPM / flatpak) # or symlink it into ~/.local/bin. # -# TODO: once a Steam Deck / SteamOS install path for punktfunk-client is settled (likely a -# flatpak, since SteamOS is image-based and /usr is read-only), pin the canonical path here. +# On SteamOS (read-only /usr, image-based) the settled install path is the flatpak +# ``io.unom.Punktfunk`` (packaging/flatpak/), launched via ``flatpak run`` — see the flatpak +# fallback in :func:`_resolve_client`. CLIENT_BINARY = "punktfunk-client" # 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(): return [candidate] - # Flatpak fallback. The app id is a guess until a flatpak is actually published; - # `flatpak run ` is a no-op-ish failure if it is not installed, which surfaces as a - # spawn error the user can act on. + # Flatpak fallback — the canonical install path on the Steam Deck (SteamOS /usr is + # read-only; the flatpak bundles the libadwaita + SDL3 the system lacks). The app id is + # the one the flatpak manifest publishes (packaging/flatpak/io.unom.Punktfunk.yml). If it + # is not installed, `flatpak run ` 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") if flatpak: - return [flatpak, "run", "earth.buehler.punktfunk.Client"] + return [flatpak, "run", "io.unom.Punktfunk"] decky.logger.warning( "punktfunk-client not found on PATH or in %s; falling back to bare name", diff --git a/clients/decky/package.json b/clients/decky/package.json index 6ecf13c..f7db3bb 100644 --- a/clients/decky/package.json +++ b/clients/decky/package.json @@ -6,6 +6,8 @@ "scripts": { "build": "rollup -c", "watch": "rollup -c -w", + "package": "pnpm build && bash scripts/package.sh", + "deploy": "bash scripts/deploy.sh", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [ @@ -15,7 +17,7 @@ "game-streaming" ], "author": "enrico", - "license": "BSD-3-Clause", + "license": "Apache-2.0 OR MIT", "dependencies": { "@decky/api": "^1.1.3", "react-icons": "^5.3.0", diff --git a/clients/decky/scripts/deploy.sh b/clients/decky/scripts/deploy.sh new file mode 100755 index 0000000..2d3eb20 --- /dev/null +++ b/clients/decky/scripts/deploy.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Deploy the staged plugin tree (out/, 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@}" +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" diff --git a/clients/decky/scripts/package.sh b/clients/decky/scripts/package.sh new file mode 100755 index 0000000..7c258e7 --- /dev/null +++ b/clients/decky/scripts/package.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Assemble the Decky plugin into the canonical store/sideload layout: +# +# out/punktfunk-v.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@ bash scripts/deploy.sh)" diff --git a/packaging/README.md b/packaging/README.md index 7169249..55dceb1 100644 --- a/packaging/README.md +++ b/packaging/README.md @@ -95,12 +95,17 @@ systemctl --user enable --now punktfunk-host 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 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. +> 👉 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) ```sh diff --git a/packaging/flatpak/.gitignore b/packaging/flatpak/.gitignore new file mode 100644 index 0000000..da71017 --- /dev/null +++ b/packaging/flatpak/.gitignore @@ -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 diff --git a/packaging/flatpak/README.md b/packaging/flatpak/README.md new file mode 100644 index 0000000..77bdfeb --- /dev/null +++ b/packaging/flatpak/README.md @@ -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.` 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-.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. diff --git a/packaging/flatpak/build-flatpak.sh b/packaging/flatpak/build-flatpak.sh new file mode 100755 index 0000000..72459ce --- /dev/null +++ b/packaging/flatpak/build-flatpak.sh @@ -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-.flatpak (install with `flatpak install --user `) +# +# 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)" diff --git a/packaging/flatpak/io.unom.Punktfunk.desktop b/packaging/flatpak/io.unom.Punktfunk.desktop new file mode 100644 index 0000000..cf40580 --- /dev/null +++ b/packaging/flatpak/io.unom.Punktfunk.desktop @@ -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 diff --git a/packaging/flatpak/io.unom.Punktfunk.metainfo.xml b/packaging/flatpak/io.unom.Punktfunk.metainfo.xml new file mode 100644 index 0000000..06b9f18 --- /dev/null +++ b/packaging/flatpak/io.unom.Punktfunk.metainfo.xml @@ -0,0 +1,68 @@ + + + + io.unom.Punktfunk + CC0-1.0 + MIT OR Apache-2.0 + Punktfunk + Low-latency desktop and game streaming client + + +

+ 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. +

+

Features:

+
    +
  • Hardware-accelerated HEVC decode (VAAPI zero-copy on AMD and Intel, software fallback)
  • +
  • PipeWire audio playback and microphone uplink
  • +
  • Full gamepad support including DualSense touchpad, motion, adaptive triggers and lightbar (SDL3)
  • +
  • LAN host discovery, TOFU fingerprint pinning and PIN pairing
  • +
+
+ + io.unom.Punktfunk.desktop + + + Network + Game + RemoteAccess + + + + streaming + remote + game + moonlight + + + https://git.unom.io/unom/punktfunk + https://git.unom.io/unom/punktfunk/issues + https://git.unom.io/unom/punktfunk + + + unom + + + + + + #a79ff8 + #6c5bf3 + + + + + + +

Initial flatpak packaging of the native Linux client for the Steam Deck and Wayland desktops.

+
+
+
+
diff --git a/packaging/flatpak/io.unom.Punktfunk.svg b/packaging/flatpak/io.unom.Punktfunk.svg new file mode 100644 index 0000000..472cf62 --- /dev/null +++ b/packaging/flatpak/io.unom.Punktfunk.svg @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packaging/flatpak/io.unom.Punktfunk.yml b/packaging/flatpak/io.unom.Punktfunk.yml new file mode 100644 index 0000000..b8f989b --- /dev/null +++ b/packaging/flatpak/io.unom.Punktfunk.yml @@ -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: + # 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