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
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:
@@ -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
@@ -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@<deck-ip>:~/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/<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
|
||||
|
||||
|
||||
@@ -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 <id>` 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 <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")
|
||||
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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Executable
+35
@@ -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"
|
||||
Executable
+39
@@ -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)"
|
||||
Reference in New Issue
Block a user