Compare commits

...
Author SHA1 Message Date
enricobuehler 85980b425e docs: state the real host floor — Ubuntu 26.04+ / Debian 13+ — and name Debian everywhere
ci / docs-site (pull_request) Successful in 1m17s
ci / rust-arm64 (pull_request) Successful in 1m29s
ci / bun-nix (pull_request) Successful in 1m33s
apple / swift (pull_request) Successful in 1m44s
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Successful in 3m56s
android / android (pull_request) Successful in 9m55s
ci / rust (pull_request) Successful in 15m59s
The floor that matters is the DESKTOP, not the package: 24.04 installs
punktfunk-host and then has no compositor over the version floors and no
gamescope, which reads as a bug rather than an unsupported base. The
requirements page now leads with a per-release table separating 'package
installs' from 'can actually host', and install.md carries the same
caveat next to the apt row.

Debian was already a supported target after the previous commits but was
still invisible at the entry points — README's install table, the docs
index cards, and the 'what you need' list all said Ubuntu only. All three
now name Debian and carry the version floor.
2026-08-13 12:20:33 +02:00
enricobuehler 107fa3472d docs: Linux Mint 22.x cannot host, and 24.04 gives the host no compositor
Measured on a real linuxmintd/mint22-amd64 image and on Ubuntu 24.04. The
package installs on both, which is exactly what makes this easy to miss —
nothing on the box can then produce a stream:

  * Cinnamon cannot host a virtual display (Muffin has no RecordVirtual).
  * gamescope is absent from 24.04 and cannot be built for it: the tree needs
    wayland >= 1.23.1 (has 1.22.0), libinput >= 1.26 (1.25), libavif >= 1.2.1
    (1.0.4), pixman >= 0.44 (0.42), plus libdisplay-info2 and libxcb-errors0,
    neither of which 24.04 packages at all.
  * Switching desktop does not rescue it — 24.04 has KWin 5.27 (floor 6.5.6)
    and GNOME Shell 46 (floor 48). Only sway 1.9 is even a candidate.

So the gamescope route documented for Cinnamon holds for LMDE 7 (Debian 13,
verified end to end) but NOT for Linux Mint 22.x — which is every mainstream
Mint until Mint 23 lands on a 26.04 base in December 2026. Both the Debian and
Ubuntu pages now say so, and the Debian page carries a per-edition table.

Also states what Debian 13 itself can drive: GNOME 48.7 and sway 1.10 are above
the floors; its KWin 6.3.6 is below.
2026-08-13 12:12:52 +02:00
enricobuehler 5d8682d7b7 feat(vdisplay): explain a cinnamon/muffin compositor pin instead of listing backends
PUNKTFUNK_COMPOSITOR=cinnamon is the first thing a Mint or LMDE user
reaches for, and the bare list of accepted values invites the
next-closest guess — 'mutter', since Muffin is a Mutter fork — which
starts a session that fails deep inside an org.gnome.Mutter.ScreenCast
call Muffin does not serve. No value of the variable can work, so say
that and name headless gamescope, which needs no desktop compositor.
2026-08-13 11:50:05 +02:00
enricobuehler 030bc8a1c2 docs(debian): use deb822 sources for non-free, not apt-add-repository
software-properties-common is not available in Debian 13, so the
apt-add-repository line could not have worked. Debian 13 keeps its
sources in deb822 format; edit Components there instead (verified in a
trixie container — the NVIDIA driver it then offers is 550, above our
535 floor).
2026-08-13 11:47:20 +02:00
enricobuehler 346385bad8 fix(deb): ship punktfunk-gamescope on apt at last, and support Debian 13
`punktfunk-gamescope` had never been published to the apt registry — not in any
release. It was built inside the host job's Ubuntu 24.04 image, where it cannot
build: our pin vendors wlroots 0.19.3, which floors `wayland-server` at 1.23.1,
and noble ships 1.22.0 (it also lacks libxcb-errors-dev and has only
libdisplay-info 0.1.1). Every rung of that path was a `::warning::` returning 0
and the one hard gate ran last by design, so v0.26.0 and v0.27.0 both released
with the package missing while docs-site told apt users to install it. The same
tags shipped it fine for Arch, Fedora 44 and Bazzite.

It now builds in its own job on Debian 13 (ci/gamescope-trixie.Dockerfile), the
oldest apt base the tree configures on. One package serves Debian 13 AND Ubuntu
26.04 — measured by installing and running it on both — because the build also
vendors libdisplay-info via the new `--extra-fallback` option: linked against
the distro copy it demands `libdisplay-info2` on trixie, which Ubuntu 26.04 does
not have (it carries libdisplay-info3). The option is opt-in, so the
Arch/Fedora/nix outputs are byte-for-byte unchanged. Ubuntu 24.04 gets no
gamescope package and cannot — its wayland is too old to run one however built.

Debian 13 is now a documented host target. That needed no packaging change at
all: the host .deb's glibc-2.39 floor and bundled FFmpeg already made it
installable, and it had been working for a long time while docs-site said Debian
was unsupported and unverified. Verified by installing: host, web console and
plugin runner install, resolve every soname and run. The desktop client stays
Ubuntu-26.04-only (built there, floors at `libc6 >= 2.43`; Debian 13 has 2.41).

Compositor detection now answers Cinnamon (Mint, LMDE) with the route that works
instead of advice that cannot help. Muffin forked from Mutter 3.36:
`org.cinnamon.Muffin.ScreenCast` has only RecordMonitor/RecordWindow, never
RecordVirtual, and xdg-desktop-portal-xapp implements no ScreenCast — so no
value of PUNKTFUNK_COMPOSITOR makes a Cinnamon desktop host a virtual display.
The error names headless gamescope, which needs no desktop compositor. The XDG
sniff moved into a pure function so those branches are testable; Cinnamon is
matched before GNOME, since it is a GNOME derivative and the generic arm would
otherwise hand it the Mutter backend (caught by the new test).

New `smoke-install` job installs every published package from the registry in
pristine ubuntu:24.04, ubuntu:26.04 and debian:trixie images, asserts each
binary resolves its libraries and runs, and insists the version served is the
one this run built. Nothing in deb.yml had ever installed a package it produced,
which is how both of the above survived unnoticed.

⚠ Bootstrap: seed `punktfunk-gamescope-trixie:latest` into the LAN registry once
(docker.yml builds it thereafter) or the new job cannot start.
2026-08-13 11:43:54 +02:00
enricobuehler 124cb66324 Merge pull request 'Wake-on-LAN: support WoWLAN so Wi-Fi hosts wake like wired ones' (#187) from worktree-wowlan-support into main
apple / swift (push) Successful in 1m40s
ci / rust-arm64 (push) Successful in 1m41s
ci / bun-nix (push) Successful in 2m2s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 1m45s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 1m1s
deb / build-publish-client-arm64 (push) Successful in 3m24s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 45s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 31s
ci / rust (push) Successful in 5m55s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 22s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 23s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 3m59s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m17s
apple / screenshots (push) Successful in 6m3s
arch / build-publish (push) Successful in 8m55s
ci / web (push) Successful in 1m3s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 2m42s
ci / docs-site (push) Successful in 1m12s
docker / builders-arm64cross (push) Successful in 20s
docker / deploy-docs (push) Successful in 48s
deb / build-publish (push) Failing after 6m34s
deb / build-publish-host (push) Successful in 14m19s
android / android (push) Successful in 10m58s
release / apple (push) Successful in 10m0s
flatpak / build-publish (push) Successful in 12m38s
windows-host / package (push) Successful in 16m30s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 46s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m19s
windows-msix / package (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m53s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 20m2s
windows-msix / package (x64, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m6s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m20s
2026-08-12 22:51:57 +00:00
17 changed files with 897 additions and 131 deletions
+170 -99
View File
@@ -1,6 +1,6 @@
# Build the punktfunk .debs and publish them to Gitea's Debian package registry, so Ubuntu
# boxes get new builds via `apt update && apt upgrade`. Three jobs, all publishing to the same
# apt distribution/component:
# Build the punktfunk .debs and publish them to Gitea's Debian package registry, so Debian and
# Ubuntu boxes get new builds via `apt update && apt upgrade`. Five jobs; the four builders all
# publish to the same apt distribution/component, and the fifth checks the result:
#
# build-publish — client + web + scripting, on the Ubuntu 26.04 rust-ci image (the client
# needs 24.04-absent libs: SDL3, GTK4 ≥ 4.20).
@@ -11,8 +11,17 @@
# build-publish-host — the HOST, on the Ubuntu 24.04 rust-ci-noble image with a from-source
# FFmpeg 8 BUNDLED into the .deb. This lowers the host's glibc floor to 2.39
# and removes the hard `Depends: libavcodec62`, so the ONE host .deb installs
# on Ubuntu 24.04 LTS through 26.04. (A 26.04-built host .deb is uninstallable
# on 24.04 — the reason this job exists; see packaging/debian/README.md.)
# on Ubuntu 24.04 LTS through 26.04 — and, for free, on Debian 13.
# (A 26.04-built host .deb is uninstallable on 24.04 — the reason this job
# exists; see packaging/debian/README.md.)
# build-publish-gamescope
# — the patched `punktfunk-gamescope`, on DEBIAN 13. It lived in the host job
# until 2026-08 and never once succeeded there: noble's wayland is 1.22.0
# and the vendored wlroots floors it at 1.23.1, so v0.26.0 and v0.27.0 both
# shipped without the package while the docs told apt users to install it.
# smoke-install — installs what was just published, from the registry, in pristine
# ubuntu:24.04 / ubuntu:26.04 / debian:trixie images. Nothing here used to
# install a package it built, which is how both of the above survived.
#
# Both compute VERSION identically (scripts/ci/pf-version.sh is deterministic per commit), so the
# host and client packages always share a version line. The release-attach helpers are race-safe
@@ -337,82 +346,6 @@ jobs:
bash scripts/ci/assert-cap-matrix.sh --self-test
bash scripts/ci/assert-cap-matrix.sh dist/punktfunk-host_*.deb
# punktfunk-gamescope for apt. Same reasoning as the RPM leg in rpm.yml: without a packaged
# build, a Debian/Ubuntu box has no route to the patched gamescope except compiling it, and a
# stock gamescope streams SDR, cursorless, and tells every game its display is 60 Hz.
#
# CACHED on packaging/gamescope/** alone — it depends on nothing else in this repo, so a
# normal push restores a binary instead of spending ~10 minutes on someone else's tree.
- uses: actions/cache@v4
id: gamescope
with:
path: gs-cache
key: punktfunk-gamescope-noble-${{ hashFiles('packaging/gamescope/**') }}
- name: Build the patched gamescope
if: steps.gamescope.outputs.cache-hit != 'true'
# Best-effort, exactly like rpm.yml: the host packages above are the primary delivery and
# work without this binary, so a hiccup building an unrelated tree must not fail the job.
# `build-dep gamescope` resolves the distro's much older packaged version, so it can come up
# short — that is what the `|| true`s absorb, and the marker check downstream is what makes
# a half-built result impossible to ship.
run: |
set -x
apt-get update
apt-get install -y --no-install-recommends meson ninja-build glslc git || true
apt-get build-dep -y gamescope || true
# NOT best-effort. `build-dep gamescope` resolves the distro's much older packaged
# gamescope — where noble has one at all — so it misses what the master tree needs, and
# wayland-protocols is the gap that actually stops the build: meson dies in
# protocol/meson.build with "Neither a subproject directory nor a wayland-protocols.wrap
# file was found", because the tree has no wrap fallback for it. That is what happened on
# the v0.26.0 tag: the step warned and skipped, the job stayed green, and the release
# shipped with no gamescope .deb while the notes said it had one.
apt-get install -y --no-install-recommends wayland-protocols
# The remaining Arch makedepends the older packaged gamescope does not necessarily pull.
# Best-effort: meson falls back or does without, and a name that moves between Ubuntu
# releases should not fail the job. (No libstdc++ static package is needed here — g++
# ships libstdc++.a, which is why only Fedora tripped the sanity check.)
# `build-dep gamescope` gives noble almost nothing — the distro has no comparable package
# — so the tree's real dependency set has to be named outright. One `apt-get` per name on
# purpose: a single transaction aborts wholesale on one unknown package, which would
# install NOTHING and hide the real gap behind a name typo. Best-effort per package, with
# the missing one named; the end-of-job gate below is what actually decides.
for p in libxdamage-dev libxcomposite-dev libxrender-dev libxext-dev libxxf86vm-dev \
libxtst-dev libx11-dev libxres-dev libxmu-dev libxcursor-dev libxi-dev \
libxfixes-dev libxkbcommon-dev libxkbcommon-x11-dev libcap-dev libdrm-dev \
libinput-dev libudev-dev libpipewire-0.3-dev libseat-dev libsdl2-dev \
libluajit-5.1-dev libavif-dev libdecor-0-dev hwdata libglm-dev libbenchmark-dev \
glslang-tools libvulkan-dev libwayland-dev libxcb1-dev libxcb-composite0-dev \
libxcb-xfixes0-dev libxcb-res0-dev libxcb-ewmh-dev libxcb-icccm4-dev \
libxcb-errors-dev libpixman-1-dev libdisplay-info-dev libgbm-dev libegl-dev \
cmake xwayland; do
apt-get install -y --no-install-recommends "$p" \
|| echo "::warning::no such noble package: $p (gamescope may still build without it)"
done
if bash packaging/gamescope/build-punktfunk-gamescope.sh \
--destdir "$PWD/gs-stage" --prefix /usr --jobs "$(nproc)"; then
install -Dm0755 gs-stage/usr/bin/punktfunk-gamescope gs-cache/punktfunk-gamescope
else
# Warn only, even on a tag. The hard gate moved to the END of this job: failing HERE
# skips the host .deb's own publish + release-attach steps below, which is how the
# v0.26.0 release ended up still carrying the pre-CAP_SYS_NICE host .deb from an
# earlier tag commit — a KDE-breaking artifact withheld from replacement by a gate
# meant to protect the release. Never let a missing EXTRA stop a good artifact
# shipping; go red afterwards instead.
echo "::warning::punktfunk-gamescope failed to build on noble — no .deb this run (gamescope sessions stay SDR)"
fi
- name: Build punktfunk-gamescope .deb
# Picked up by the publish loop below, which globs dist/*.deb.
run: |
if [ -x gs-cache/punktfunk-gamescope ] && gs-cache/punktfunk-gamescope --version >/dev/null 2>&1; then
bash packaging/debian/build-gamescope-deb.sh --binary gs-cache/punktfunk-gamescope
else
# Warn only — see the note on the build step. The gate is the last step of this job.
echo "::warning::no usable punktfunk-gamescope — skipping its .deb"
fi
- name: Publish to the Gitea apt registry
env:
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
@@ -440,25 +373,109 @@ jobs:
upsert_asset "$RID" "$DEB"
done
# A release must not be able to make a claim its own CI silently dropped: v0.26.0's notes and
# docs-site said the patched gamescope was apt-installable while no .deb had ever been built,
# because every failure on this path was a `::warning::` that returned 0.
#
# ⚠ LAST step on purpose. The first version of this gate failed at the build step instead, and
# that skipped the host .deb's own publish + attach below — so the release kept the PREVIOUS
# tag commit's host .deb, which still carried the CAP_SYS_NICE postinst that breaks KDE. A
# gate protecting the release withheld the fix for it. Everything good ships first; the job
# goes red afterwards.
- name: A stable tag must ship the gamescope .deb
if: startsWith(gitea.ref, 'refs/tags/v')
# ---------------------------------------------------------------------------------------------
# `punktfunk-gamescope` for apt — the patched build that gives a gamescope session HDR, a cursor,
# and the client's real refresh rate instead of a hardcoded 60 Hz. Same reasoning as the RPM leg
# in rpm.yml: without a package, a Debian/Ubuntu box has no route to it except compiling by hand.
#
# ⚠ THIS IS ITS OWN JOB, ON DEBIAN 13, BECAUSE THE NOBLE BUILD COULD NEVER HAVE WORKED.
# It used to be a few best-effort steps inside build-publish-host (Ubuntu 24.04), where it failed
# every single time:
# wlroots| Dependency wayland-server found: NO found 1.22.0 but need: '>=1.23.1'
# Our pin vendors wlroots 0.19.3, which floors wayland-server at 1.23.1; noble ships 1.22.0 and
# always will. v0.26.0 AND v0.27.0 both shipped with no gamescope .deb — while the release notes
# and docs-site told apt users to install it — because every rung of that path was a `::warning::`
# that returned 0, and the one hard gate ran last by design (so good artifacts still publish) and
# was simply never acted on. Moving it to trixie makes the build possible; making it a job of its
# own makes its failure visible instead of a footnote in someone else's log.
#
# Debian 13 is the OLDEST apt distro the tree configures on (wayland 1.23.1, libxcb-errors,
# libdisplay-info 0.2). The binary it produces needs GLIBC_2.38 and links no libstdc++, so what
# actually bounds it is wayland: Debian 13 and Ubuntu 26.04 YES, Ubuntu 24.04 NO — and 24.04
# could not run this binary however it was built, so nothing was lost by leaving noble.
build-publish-gamescope:
runs-on: ubuntu-24.04
container:
image: 192.168.1.58:5010/punktfunk-gamescope-trixie:latest
timeout-minutes: 90
steps:
- uses: actions/checkout@v4
# Byte-identical to the other jobs' version step (pf-version.sh is deterministic per commit)
# — but only DISTRIBUTION is used here. The package version is the gamescope upstream
# version + our patch level, which build-gamescope-deb.sh derives itself; it deliberately
# does NOT follow the punktfunk version line, because this package moves on its own cadence.
- name: Channel
run: |
shopt -s nullglob
built=(dist/punktfunk-gamescope_*.deb)
if [ ${#built[@]} -eq 0 ]; then
echo "::error::no punktfunk-gamescope .deb was built — a stable tag must not ship without it (the release notes and docs-site say it is apt-installable). Everything else in this job published normally; see the gamescope build step above for the meson error."
exit 1
fi
echo "gamescope .deb present: ${built[*]}"
git config --global --add safe.directory "$PWD"
case "$GITHUB_REF" in
refs/tags/v*) DIST=stable ;;
*) DIST=canary ;;
esac
echo "DISTRIBUTION=$DIST" >> "$GITHUB_ENV"
echo "gamescope -> apt distribution '$DIST'"
# CACHED on packaging/gamescope/** alone — it depends on nothing else in this repo, so a
# normal push restores a binary instead of spending ~10 minutes on someone else's tree.
# Keyed `-trixie-` so the noble cache entries (which only ever held misses) can't be hit.
- uses: actions/cache@v4
id: gamescope
with:
path: gs-cache
key: punktfunk-gamescope-trixie-${{ hashFiles('packaging/gamescope/**') }}
# NOT best-effort, unlike the noble version of this step. Every dependency now comes from the
# image (which asserts the wayland floor at build time), so a failure here is a real
# regression in the tree or the pin — exactly the thing the previous arrangement hid.
- name: Build the patched gamescope
if: steps.gamescope.outputs.cache-hit != 'true'
# `--extra-fallback libdisplay-info` is what makes ONE .deb serve both Debian 13 and
# Ubuntu 26.04. Built against the distro's copy, the package picks up
# `Depends: libdisplay-info2 (>= 0.2.0)` on trixie — and Ubuntu 26.04 carries
# libdisplay-info **3** (0.3.0), so apt refuses it there ("Depends libdisplay-info2 …" —
# measured, not predicted). gamescope vendors the library as a submodule, so linking the
# vendored copy drops the dependency entirely. Same reasoning the build script already
# applies to wlroots: a binary we SHIP must not follow the build host's shared libraries.
run: |
bash packaging/gamescope/build-punktfunk-gamescope.sh \
--destdir "$PWD/gs-stage" --prefix /usr --jobs "$(nproc)" \
--extra-fallback libdisplay-info
install -Dm0755 gs-stage/usr/bin/punktfunk-gamescope gs-cache/punktfunk-gamescope
# The binary must RUN, not merely link: `--version` is what the old job used as its ship
# gate, and it is the cheapest proof that the static-libstdc++ trick and the vendored wlroots
# actually produced a working compositor.
- name: Build the .deb
run: |
gs-cache/punktfunk-gamescope --version
bash packaging/debian/build-gamescope-deb.sh --binary gs-cache/punktfunk-gamescope
- name: Publish to the Gitea apt registry
env:
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
for DEB in dist/punktfunk-gamescope_*.deb; do
echo "uploading $DEB"
NAME=$(dpkg-deb -f "$DEB" Package)
VER=$(dpkg-deb -f "$DEB" Version)
ARCH=$(dpkg-deb -f "$DEB" Architecture)
curl -fsS -o /dev/null --user "enricobuehler:$TOKEN" -X DELETE \
"https://$REGISTRY/api/packages/$OWNER/debian/pool/$DISTRIBUTION/$COMPONENT/$NAME/$VER/$ARCH" || true
curl -fsS --user "enricobuehler:$TOKEN" --upload-file "$DEB" \
"https://$REGISTRY/api/packages/$OWNER/debian/pool/$DISTRIBUTION/$COMPONENT/upload"
done
echo "published gamescope to $OWNER/debian $DISTRIBUTION/$COMPONENT"
- name: Attach the gamescope .deb to the Gitea release (stable tags only)
if: startsWith(gitea.ref, 'refs/tags/v')
env:
GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
. scripts/ci/gitea-release.sh
RID=$(ensure_release "$GITHUB_REF_NAME" "$GITHUB_REF_NAME" auto)
for DEB in dist/punktfunk-gamescope_*.deb; do
upsert_asset "$RID" "$DEB"
done
# ---------------------------------------------------------------------------------------------
# The aarch64 CLIENT .deb. Cross-compiled on the ordinary amd64 runner in the
@@ -560,3 +577,57 @@ jobs:
for DEB in dist/*.deb; do
upsert_asset "$RID" "$DEB"
done
# ---------------------------------------------------------------------------------------------
# Does the thing we just published actually INSTALL? Nothing in this repo asked that before, and
# the cost of not asking was two independent, long-lived facts nobody knew:
# * `punktfunk-host` has installed cleanly on Debian 13 for a long time — while docs-site said
# "Debian isn't a supported target … nobody has verified it".
# * `punktfunk-gamescope` was missing from apt entirely across two releases.
# Both are exactly what a five-minute install check catches, so it is now a job.
#
# It runs on the RUNNER (no `container:`) and drives docker directly — the same access
# docker.yml's image builds use — because the check must happen in a pristine distro image, not
# in a builder image with the deps already present.
#
# It installs FROM THE REGISTRY, after the publish jobs, rather than from a local .deb: that
# exercises the real path a user takes (repo key, apt distribution, dependency resolution against
# the distro's own package set), and it matches this workflow's established order — publish the
# good artifacts first, go red afterwards, never let a gate withhold a shipping fix.
smoke-install:
needs: [build-publish, build-publish-host, build-publish-gamescope]
# `needs` for ORDER only — this must still run when a builder went red, or the failure that
# matters most (a package that did not publish) is exactly the one that skips its own check and
# leaves the run looking merely "partly red" instead of saying what a user would hit.
if: ${{ !cancelled() }}
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
# Recomputes the SAME version string the builder jobs stamped — pf-version.sh is
# deterministic per commit and GITHUB_RUN_NUMBER is shared across a run's jobs — so the check
# below can insist the registry is serving THIS run's build. Without that, a smoke job that
# beats the index regeneration installs the previous build, passes, and proves nothing.
- name: Channel + expected version
run: |
git config --global --add safe.directory "$PWD"
eval "$(bash scripts/ci/pf-version.sh)"
SHORT=$(echo "$GITHUB_SHA" | cut -c1-8)
case "$GITHUB_REF" in
refs/tags/v*) V="${GITHUB_REF_NAME#v}"; DIST=stable ;;
*) V="${PF_BASE}~ci${GITHUB_RUN_NUMBER}.g${SHORT}"; DIST=canary ;;
esac
echo "DISTRIBUTION=$DIST" >> "$GITHUB_ENV"
echo "EXPECT_VERSION=$V" >> "$GITHUB_ENV"
echo "expecting $V in apt distribution '$DIST'"
# The support matrix, asserted rather than asserted-in-prose. Each row names the packages
# that MUST install on that distro; anything absent from the row is expected not to and is
# not checked here (the client's glibc 2.43 floor keeps it off 24.04 and Debian 13 —
# see docs-site/content/docs/debian.md).
- name: Install from the apt registry on every supported distro
run: bash scripts/ci/deb-install-smoke.sh
env:
PF_APT_DISTRIBUTION: ${{ env.DISTRIBUTION }}
PF_EXPECT_VERSION: ${{ env.EXPECT_VERSION }}
+6
View File
@@ -96,6 +96,12 @@ jobs:
# (rust-ci's 26.04 build is uninstallable there). Consumed by deb.yml's build-publish-host job.
- image: punktfunk-rust-ci-noble
dockerfile: ci/rust-ci-noble.Dockerfile
# Debian 13 gamescope builder. The gamescope .deb used to be built in the noble image
# and NEVER once succeeded there — noble's wayland is 1.22.0 and the vendored wlroots
# 0.19.3 floors it at 1.23.1, so two releases shipped without the package. trixie is the
# oldest apt distro the tree configures on. Consumed by deb.yml's build-publish-gamescope.
- image: punktfunk-gamescope-trixie
dockerfile: ci/gamescope-trixie.Dockerfile
- image: punktfunk-fedora-rpm
dockerfile: ci/fedora-rpm.Dockerfile
# Fedora 44 builder (Fedora KDE spin): same Dockerfile, newer base → libavcodec.so.62.
+36
View File
@@ -220,6 +220,42 @@ Helldivers 2 at 1% lows of 25 FPS, cured by uninstalling). Two mechanisms, bo
is now refcounted across the hot stream threads and reverts when the last one exits
(= session teardown), the same lifetime the per-thread MMCSS effects already ride.
### Debian 13 is a supported target, and `punktfunk-gamescope` reaches apt for the first time
🛑 **The `punktfunk-gamescope` .deb had never been published — not once, in any release.** It was
built inside the host job's Ubuntu 24.04 image, where it cannot build: our pin vendors wlroots
0.19.3, which floors `wayland-server` at 1.23.1, and noble ships 1.22.0 (it also has no
`libxcb-errors-dev` and only libdisplay-info 0.1.1). Every rung of that path was a `::warning::`
returning 0, and the one hard gate ran last by design so good artifacts still shipped — so
**v0.26.0 and v0.27.0 both released with the package missing** while the release notes and
docs-site told Debian/Ubuntu users to `apt install` it. The same tag shipped it fine for Arch,
Fedora 44 and Bazzite; apt was the only platform affected.
It now has its own job on **Debian 13** (`ci/gamescope-trixie.Dockerfile`), the oldest apt base the
tree configures on. One package serves Debian 13 **and** Ubuntu 26.04 — verified by installing and
running it on both — because the build additionally vendors libdisplay-info
(`build-punktfunk-gamescope.sh --extra-fallback libdisplay-info`, opt-in so the Arch/Fedora/nix
outputs are unchanged): linked against the distro copy it would demand `libdisplay-info2` on trixie,
which Ubuntu 26.04 does not have (it carries `libdisplay-info3`). **Ubuntu 24.04 gets no gamescope
package** — its wayland is too old to run one, however it is built.
**Debian 13 is now a documented, CI-tested host target** ([docs](https://docs.punktfunk.unom.io/docs/debian)).
It required no packaging change: the host .deb's glibc-2.39 floor and bundled FFmpeg already made
it installable, and it had been working for a long time while docs-site said Debian was unsupported
and unverified. The desktop **client** remains Ubuntu-26.04-only (built there, floors at
`libc6 >= 2.43`; Debian 13 has 2.41).
**Cinnamon (Linux Mint, LMDE) cannot host a virtual display**, and compositor detection now says
so instead of advising a `PUNKTFUNK_COMPOSITOR` value that cannot help. Muffin forked from Mutter
3.36: `org.cinnamon.Muffin.ScreenCast` has only `RecordMonitor`/`RecordWindow`, never
`RecordVirtual`, and `xdg-desktop-portal-xapp` implements no ScreenCast at all. The error names the
route that does work on those boxes — a headless gamescope, which needs no desktop compositor.
New CI job **`smoke-install`** installs every published package from the registry in pristine
`ubuntu:24.04`, `ubuntu:26.04` and `debian:trixie` images and asserts the version served is the one
the run just built. Nothing in `deb.yml` had ever installed a package it produced, which is how
both facts above survived for so long.
## v0.27.0
87 commits since v0.26.0.
+1 -1
View File
@@ -100,7 +100,7 @@ installer (all-vendor: NVIDIA, AMD, Intel).
| Platform | Install | Guide |
|--------|---------|-------|
| **Ubuntu / Debian** (apt) | `sudo apt install punktfunk-host` *(after adding the repo)* | [Ubuntu / Debian](https://docs.punktfunk.unom.io/docs/ubuntu) · [packaging/debian](packaging/debian/README.md) |
| **Ubuntu 26.04+ / Debian 13+** (apt) | `sudo apt install punktfunk-host` *(after adding the repo)* | [Ubuntu](https://docs.punktfunk.unom.io/docs/ubuntu) · [Debian](https://docs.punktfunk.unom.io/docs/debian) · [packaging/debian](packaging/debian/README.md) |
| **Bazzite / Fedora Atomic** (systemd-sysext) | `curl -fsSLO https://git.unom.io/unom/punktfunk/raw/branch/main/packaging/bazzite/punktfunk-sysext.sh && sudo bash punktfunk-sysext.sh install` *(no layering, no reboot; rpm-ostree + bootc also supported)* | [Bazzite](https://docs.punktfunk.unom.io/docs/bazzite) |
| **Fedora** (dnf) | `sudo dnf install punktfunk` *(after adding the repo; the console comes with it)* | [Fedora](https://docs.punktfunk.unom.io/docs/fedora) · [packaging/rpm](packaging/rpm/README.md) |
| **Arch / CachyOS** (pacman) | `sudo pacman -Syu punktfunk-host` *(binary repo — always a full `-Syu`)* | [Arch Linux](https://docs.punktfunk.unom.io/docs/arch) · [packaging/arch](packaging/arch/README.md) |
+68
View File
@@ -0,0 +1,68 @@
# Builder for the `punktfunk-gamescope` .deb — Debian 13 (trixie).
#
# WHY THIS EXISTS, AND WHY IT IS NOT THE NOBLE IMAGE:
# The gamescope .deb was built in the host job's Ubuntu 24.04 (noble) image, and it has NEVER once
# succeeded there — v0.26.0 and v0.27.0 both shipped with no gamescope package while the release
# notes and docs-site said it was apt-installable. The failure is structural, not a flaky dep:
#
# wlroots| Dependency wayland-server found: NO found 1.22.0 but need: '>=1.23.1'
# subprojects/wlroots/meson.build:96:17: ERROR: Dependency 'wayland-server' is required but not found
#
# Our gamescope pin vendors wlroots 0.19.3, which floors wayland-server at 1.23.1. Noble ships
# 1.22.0 and will never ship more — so no amount of `apt-get install` in that image can fix it.
# Noble also has no `libxcb-errors-dev` at all and only libdisplay-info 0.1.1 (the tree wants 0.2).
#
# Debian 13 ships wayland 1.23.1 exactly, libxcb-errors 1.0.1 and libdisplay-info 0.2.0 — the
# oldest apt distro the tree actually builds on. Building HERE rather than on Ubuntu 26.04
# (wayland 1.24, libdisplay-info 0.3) is deliberate twice over: it keeps the glibc floor low, and
# it stays on the libdisplay-info 0.2 line the pin was developed against.
#
# WHAT THE RESULTING BINARY RUNS ON — verified by building it and reading the ELF:
# * glibc floor GLIBC_2.38 (the C++ runtime is linked statically by
# build-punktfunk-gamescope.sh, so libstdc++ never enters the NEEDED list)
# * NEEDED libwayland-server.so.0 / libwayland-client.so.0 — wlroots 0.19 calls symbols
# added in 1.23.1, so THAT, not glibc, is the real floor.
# ⇒ Debian 13 (1.23.1) and Ubuntu 26.04 (1.24.0) YES; Ubuntu 24.04 (1.22.0) NO — and 24.04
# could not run this binary however it was built, so nothing is lost by moving off noble.
#
# Rebuilt+pushed by .gitea/workflows/docker.yml (matrix: punktfunk-gamescope-trixie); consumed by
# the `build-publish-gamescope` job in .gitea/workflows/deb.yml. Bootstrap: like rust-ci-noble, the
# first deb.yml run after this image is added needs the image to already exist — seed it once by
# hand (docker build -f ci/gamescope-trixie.Dockerfile -t <registry>/punktfunk-gamescope-trixie:latest ci
# && docker push …) before that job can run.
FROM debian:trixie
ENV DEBIAN_FRONTEND=noninteractive
# nodejs is not optional: the Gitea runner executes the JS actions (checkout/cache) INSIDE this
# container, so an image without it fails before the first `run:` step ever starts.
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential pkg-config cmake meson ninja-build git curl ca-certificates nodejs \
# .deb assembly (dpkg-shlibdeps computes the runtime Depends from the built binary)
dpkg-dev \
# shader compilers gamescope's meson looks for
glslc glslang-tools \
# wayland + protocols. libwayland-dev 1.23.1 is the whole reason this image is Debian.
libwayland-dev wayland-protocols \
# gamescope's own dependency set. `apt-get build-dep gamescope` is useless here — Debian has
# no gamescope package to derive it from — so the tree's needs are named outright, exactly as
# the noble job had to. Kept as ONE transaction on purpose: in an image build a missing name
# SHOULD fail loudly at build time, unlike the workflow's per-package best-effort loop where a
# rename would have silently dropped a dep into a warning nobody reads.
libxdamage-dev libxcomposite-dev libxrender-dev libxext-dev libxxf86vm-dev \
libxtst-dev libx11-dev libxres-dev libxmu-dev libxcursor-dev libxi-dev \
libxfixes-dev libxkbcommon-dev libxkbcommon-x11-dev libcap-dev libdrm-dev \
libinput-dev libudev-dev libpipewire-0.3-dev libseat-dev libsdl2-dev \
libluajit-5.1-dev libavif-dev libdecor-0-dev hwdata libglm-dev libbenchmark-dev \
libvulkan-dev libxcb1-dev libxcb-composite0-dev libxcb-xfixes0-dev libxcb-res0-dev \
libxcb-ewmh-dev libxcb-icccm4-dev libxcb-errors-dev libxcb-shape0-dev \
libpixman-1-dev libdisplay-info-dev libgbm-dev libegl-dev xwayland \
&& rm -rf /var/lib/apt/lists/*
# Assert the ONE version that decides whether this image can do its job, so a future Debian base
# bump that regressed it fails HERE (loudly, at image build) instead of in a deb.yml run whose
# gamescope failure has historically been a `::warning::` nobody saw.
RUN set -eux; \
have="$(pkg-config --modversion wayland-server)"; \
pkg-config --atleast-version=1.23.1 wayland-server \
|| { echo "wayland-server $have < 1.23.1 — the vendored wlroots will not configure" >&2; exit 1; }; \
echo "wayland-server $have — OK"
+141 -19
View File
@@ -321,11 +321,7 @@ pub fn detect() -> Result<Compositor> {
#[cfg(target_os = "linux")]
{
if let Some(v) = pf_host_config::config().compositor.as_deref() {
return compositor_from_pin(v).ok_or_else(|| {
anyhow::anyhow!(
"unknown PUNKTFUNK_COMPOSITOR '{v}' (kwin|wlroots|hyprland|mutter|gamescope)"
)
});
return compositor_from_pin(v).ok_or_else(|| unknown_pin_error(v));
}
if let Some(c) = compositor_for_kind(detect_active_session().kind) {
return Ok(c);
@@ -338,20 +334,78 @@ pub fn detect() -> Result<Compositor> {
let desktop = with_env_lock(|| std::env::var("XDG_CURRENT_DESKTOP"))
.unwrap_or_default()
.to_ascii_uppercase();
if desktop.contains("KDE") {
Ok(Compositor::Kwin)
} else if desktop.contains("GNOME") {
Ok(Compositor::Mutter)
} else if desktop.contains("HYPRLAND") {
Ok(Compositor::Hyprland)
} else if desktop.contains("SWAY") || desktop.contains("WLROOTS") {
Ok(Compositor::Wlroots)
} else {
anyhow::bail!(
"could not detect compositor: no live graphical session for this uid and \
XDG_CURRENT_DESKTOP='{desktop}'; set PUNKTFUNK_COMPOSITOR"
)
}
compositor_from_xdg(&desktop)
}
}
/// The error for a `PUNKTFUNK_COMPOSITOR` value that names no backend.
///
/// `cinnamon`/`muffin` get their own answer rather than the bare list: it is the value a Mint or
/// LMDE user reaches for first, and the plain list invites them to try the next-closest name
/// (`mutter` — Muffin *is* a Mutter fork), which starts a session that then fails deep inside a
/// `org.gnome.Mutter.ScreenCast` call Muffin does not serve. There is no working value; say so, and
/// name the route that does work.
#[cfg(target_os = "linux")]
fn unknown_pin_error(v: &str) -> anyhow::Error {
const ACCEPTED: &str = "kwin|wlroots|hyprland|mutter|gamescope";
if matches!(
v.trim().to_ascii_lowercase().as_str(),
"cinnamon" | "muffin"
) {
return anyhow::anyhow!(
"PUNKTFUNK_COMPOSITOR='{v}' is not a backend and cannot become one: Cinnamon's \
compositor Muffin has no virtual-output API (no `RecordVirtual`), so it cannot make a \
screen for a client. Do NOT substitute 'mutter' Muffin is a Mutter fork but serves \
none of that interface. Use PUNKTFUNK_COMPOSITOR=gamescope to stream games through a \
headless gamescope, which needs no desktop compositor. See \
https://docs.punktfunk.unom.io/docs/debian#cinnamon-linux-mint-and-lmde"
);
}
anyhow::anyhow!("unknown PUNKTFUNK_COMPOSITOR '{v}' ({ACCEPTED})")
}
/// The last-resort `XDG_CURRENT_DESKTOP` sniff, as a **pure function of the (uppercased) value** so
/// its branches — including the two that only ever produce an error — are testable without mutating
/// process-global env. Called only by [`detect`], after both the operator pin and live-session
/// detection have come up empty.
#[cfg(target_os = "linux")]
fn compositor_from_xdg(desktop: &str) -> Result<Compositor> {
// CINNAMON is tested FIRST, ahead of GNOME, and the order is load-bearing rather than
// stylistic: Cinnamon is a GNOME derivative, so a session that advertises both (`X-Cinnamon`
// alongside a GNOME-compatibility token) would otherwise match the GNOME arm and be handed the
// Mutter backend — which then fails deep in a `org.gnome.Mutter.ScreenCast` call that Muffin
// does not serve, i.e. an obscure D-Bus error instead of the explanation below. The more
// specific desktop wins.
if desktop.contains("CINNAMON") {
// Linux Mint / LMDE report `X-Cinnamon`. Cinnamon is NOT a missing backend we could add —
// its compositor (Muffin) exposes no virtual-output API at all: the fork base is Mutter
// 3.36, and `org.cinnamon.Muffin.ScreenCast` carries only `RecordMonitor` / `RecordWindow`,
// never Mutter 42+'s `RecordVirtual`. Its portal backend (xdg-desktop-portal-xapp)
// implements no ScreenCast either, so the sway/Hyprland portal route is closed too. The
// generic message below would send a Cinnamon user hunting for the setting that turns it
// on; there isn't one. Name the ONE route that does work on that box — a headless
// gamescope, which needs no desktop compositor at all — instead of a dead end.
anyhow::bail!(
"Cinnamon (XDG_CURRENT_DESKTOP='{desktop}') cannot host a virtual display: its \
compositor Muffin has no virtual-output API, so Punktfunk cannot create a screen \
for a client on it. Stream games instead by setting PUNKTFUNK_COMPOSITOR=gamescope \
in host.env the host then spawns its own headless gamescope per connect and needs \
no desktop session. See \
https://docs.punktfunk.unom.io/docs/debian#cinnamon-linux-mint-and-lmde"
)
} else if desktop.contains("KDE") {
Ok(Compositor::Kwin)
} else if desktop.contains("GNOME") {
Ok(Compositor::Mutter)
} else if desktop.contains("HYPRLAND") {
Ok(Compositor::Hyprland)
} else if desktop.contains("SWAY") || desktop.contains("WLROOTS") {
Ok(Compositor::Wlroots)
} else {
anyhow::bail!(
"could not detect compositor: no live graphical session for this uid and \
XDG_CURRENT_DESKTOP='{desktop}'; set PUNKTFUNK_COMPOSITOR"
)
}
}
@@ -811,6 +865,74 @@ mod wlroots;
mod tests {
use super::*;
/// The XDG sniff is the last thing standing between an unrecognized desktop and a useless
/// error, and `mgmt/display.rs` puts that error VERBATIM in the console's `/display/monitors`
/// response — so its exact wording is a user-facing surface, tested as one.
#[cfg(target_os = "linux")]
#[test]
fn xdg_sniff_maps_known_desktops() {
// Real-world values, uppercased the way `detect` hands them over.
assert_eq!(compositor_from_xdg("KDE").unwrap(), Compositor::Kwin);
assert_eq!(compositor_from_xdg("GNOME").unwrap(), Compositor::Mutter);
assert_eq!(
compositor_from_xdg("UBUNTU:GNOME").unwrap(),
Compositor::Mutter
);
assert_eq!(
compositor_from_xdg("HYPRLAND").unwrap(),
Compositor::Hyprland
);
assert_eq!(compositor_from_xdg("SWAY").unwrap(), Compositor::Wlroots);
}
/// Cinnamon must NOT fall into the generic "set PUNKTFUNK_COMPOSITOR" arm: Muffin has no
/// virtual-output API, so there is no value of that variable which makes a Cinnamon desktop
/// host a virtual display. The error has to name gamescope — the one route that works on an
/// LMDE/Mint box — or the user is sent hunting for a setting that does not exist.
#[cfg(target_os = "linux")]
#[test]
fn cinnamon_is_told_to_use_gamescope_not_to_pick_a_backend() {
// `X-Cinnamon` is what Mint and LMDE actually set.
for v in ["X-CINNAMON", "CINNAMON", "X-CINNAMON:GNOME-FLASHBACK"] {
let err = compositor_from_xdg(v)
.expect_err("Cinnamon cannot host a virtual display")
.to_string();
assert!(err.contains("gamescope"), "no gamescope route named: {err}");
assert!(err.contains("Muffin"), "does not say why: {err}");
}
}
/// Pinning `cinnamon` explicitly must not answer with the plain list of accepted values: the
/// next thing a Mint user tries is `mutter` (Muffin is a Mutter fork), which fails much later
/// and much less clearly. A typo'd pin still gets the ordinary list.
#[cfg(target_os = "linux")]
#[test]
fn pinning_cinnamon_explains_instead_of_listing_backends() {
for v in ["cinnamon", "Cinnamon", "muffin", " MUFFIN "] {
let err = unknown_pin_error(v).to_string();
assert!(err.contains("gamescope"), "no working route named: {err}");
assert!(
err.contains("Muffin"),
"does not explain why it cannot work: {err}"
);
}
let typo = unknown_pin_error("kwim").to_string();
assert!(
typo.contains("kwin|wlroots|hyprland|mutter|gamescope"),
"{typo}"
);
assert!(!typo.contains("Muffin"), "{typo}");
}
/// An unknown desktop keeps the generic advice — the Cinnamon arm must not swallow it.
#[cfg(target_os = "linux")]
#[test]
fn unknown_desktop_keeps_the_generic_error() {
let err = compositor_from_xdg("XFCE").unwrap_err().to_string();
assert!(err.contains("PUNKTFUNK_COMPOSITOR"), "{err}");
assert!(!err.contains("Muffin"), "{err}");
}
#[test]
fn active_kind_maps_to_its_backend() {
assert_eq!(
+227
View File
@@ -0,0 +1,227 @@
---
title: Debian
description: Install the Punktfunk host on Debian 13 with apt — including LMDE and Linux Mint.
---
Install a Punktfunk host on **Debian 13 ("trixie") or newer** from the apt registry. This page
covers the distro-level setup — GPU driver, package, gamepad access. How the host creates its
virtual display and injects input is desktop-specific, so pick your desktop on the
[configure pages](#configure-your-desktop) afterward rather than here.
> New here? Read [Security & Safe Use](/docs/security) first — a streaming host is remote control of
> the machine, so keep it on a trusted LAN or VPN and require pairing.
> **Which releases.** The host package needs **glibc 2.39 or newer**; Debian 13 has 2.41, so it
> installs and runs there. **Debian 12 (bookworm) has glibc 2.36 and cannot install it** — build
> from source ([Ubuntu appendix](/docs/ubuntu#appendix--build-from-source), which applies here too)
> or upgrade. Check yours with `ldd --version`.
> **The desktop client is not packaged for Debian yet.** `punktfunk-client` is built on Ubuntu 26.04
> and floors at `libc6 >= 2.43` (Debian 13 has 2.41), on top of needing GTK4 ≥ 4.20. On a Debian
> box, stream *to* it with a [different client](/docs/install-client) — the Flatpak, or a build from
> source. The **host**, the **web console** and the **plugin runner** all install normally.
## What works on Debian 13
| Package | Debian 13 | What it is |
|---|---|---|
| `punktfunk-host` | ✅ | The streaming host |
| `punktfunk-web` | ✅ | The browser management console |
| `punktfunk-scripting` | ✅ | The plugin/script runner |
| `punktfunk-gamescope` | ✅ | The patched gamescope (HDR + cursor + real refresh) |
| `punktfunk-client` | ❌ | Desktop client — `libc6 >= 2.43`, see above |
## 1. GPU driver
On **NVIDIA**, the driver lives in Debian's `contrib` / `non-free` / `non-free-firmware`
components, which a default install does not enable. Debian 13 keeps its sources in the deb822
format, so add them there and refresh:
```sh
sudo sed -i 's/^Components: .*/Components: main contrib non-free non-free-firmware/' \
/etc/apt/sources.list.d/debian.sources
sudo apt update
sudo apt install nvidia-driver firmware-misc-nonfree
```
Debian 13 ships driver 550, comfortably above the [535 floor](/docs/requirements).
Reboot, then confirm the driver and KMS modeset — Wayland on NVIDIA needs `modeset=1`:
```sh
nvidia-smi
cat /sys/module/nvidia_drm/parameters/modeset # should print Y
```
If modeset is not `Y`:
```sh
echo 'options nvidia-drm modeset=1' | sudo tee /etc/modprobe.d/nvidia-drm.conf
sudo update-initramfs -u && sudo reboot
```
> **Secure Boot:** with Secure Boot enabled, Debian's DKMS-built NVIDIA module must be signed and
> its key enrolled before it will load. If `nvidia-smi` can't talk to the driver, enrol the MOK
> (`sudo mokutil --import /var/lib/dkms/mok.pub`, reboot, choose **Enrol MOK**) or disable Secure
> Boot in firmware.
On **AMD/Intel** none of the NVIDIA steps apply. Encode runs on the Mesa stack: **Vulkan Video** for
HEVC and AV1 (`mesa-vulkan-drivers`), with **VAAPI** for H.264 and as the fallback —
`mesa-va-drivers` on AMD, `intel-media-va-driver` on Intel (the latter is in `non-free`).
## 2. Install the host (apt)
The registry is public — no auth needed, just trust its signing key:
```sh
sudo install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://git.unom.io/api/packages/unom/debian/repository.key \
| sudo tee /etc/apt/keyrings/punktfunk.asc >/dev/null
echo "deb [signed-by=/etc/apt/keyrings/punktfunk.asc] https://git.unom.io/api/packages/unom/debian stable main" \
| sudo tee /etc/apt/sources.list.d/punktfunk.list
sudo apt update
sudo apt install punktfunk-host
```
`punktfunk-host` `Recommends` the browser console (`punktfunk-web`), so apt pulls it in by default.
The NVIDIA driver is **not** a dependency — you installed it out of band in step 1. Later updates
are `sudo apt update && sudo apt upgrade`; restart the running host afterwards so it picks up the
new binary:
```sh
systemctl --user restart punktfunk-host
```
The `stable` component above is the stable channel. To track pre-release builds instead, see
[Release Channels](/docs/channels).
## 3. Grant gamepad access
Virtual gamepads inject through `/dev/uinput`, gated by the `input` group. Add yourself and re-login:
```sh
sudo usermod -aG input "$USER" # re-login to apply
```
Also join `punktfunk` if you want the **virtual Steam Deck controller** (paddles, trackpads, gyro) —
it reaches games as a real USB device over usbip, which is what makes Steam Input adopt it. Join it
only on a machine you trust: writing the usbip `attach` file can materialise arbitrary emulated USB
hardware.
```sh
sudo usermod -aG punktfunk "$USER" # re-login to apply
```
## 4. Check it installed
```sh
punktfunk-host --version # the binary is on PATH
punktfunk-host detect-conflicts # exits 1 if Sunshine/Apollo is also installed
```
Two hosts on one machine is the most common reason a clean install never streams — see
[Troubleshooting](/docs/troubleshooting#another-streaming-host-sunshine-apollo--is-installed).
## 5. Open the firewall (if you have one)
**Debian ships no firewall enabled by default**, so out of the box there is nothing to open. If you
run one, the package installs the openers:
```sh
# ufw:
sudo ufw allow punktfunk-native
# firewalld:
sudo firewall-cmd --reload # load the installed definitions
sudo firewall-cmd --permanent --add-service=punktfunk-native
sudo firewall-cmd --reload
```
Add `punktfunk-gamestream` for Moonlight compat and `punktfunk-web` (TCP 47992) to reach the console
from another device. Full port lists are in
[`packaging/debian/README.md`](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/debian/README.md#firewall).
## Cinnamon, Linux Mint and LMDE
**A Cinnamon desktop cannot host a virtual display, and no setting changes that.** Punktfunk gives
each client its own screen at that device's exact resolution by asking the compositor to create a
virtual output. Cinnamon's compositor, **Muffin**, has no such API: it forked from Mutter 3.36, and
its `org.cinnamon.Muffin.ScreenCast` interface offers only `RecordMonitor` and `RecordWindow`
never the `RecordVirtual` that Mutter gained in 42. Its portal backend
(`xdg-desktop-portal-xapp`) implements no ScreenCast either, so the route that serves Sway and
Hyprland is closed too. This is upstream's to fix, not a Punktfunk setting.
**Which Mint you run decides whether there is any route at all:**
| Edition | Base | Can it host? |
|---|---|---|
| **LMDE 7 "Gigi"** | Debian 13 | ✅ Yes — via gamescope (below) |
| **Linux Mint 22.x** ("Wilma"…"Zena") | Ubuntu 24.04 | ❌ No — see [below](#linux-mint-22x-cannot-host-yet) |
| **Linux Mint 23** | Ubuntu 26.04 | ✅ Expected — due December 2026 |
On **LMDE 7**, what works is **gamescope**: the host starts its own headless gamescope for each
connecting client and runs the game inside it, so it needs no desktop compositor at all. Your
Cinnamon session keeps running untouched; the stream is the game, not the desktop.
```sh
sudo apt install punktfunk-gamescope # LMDE 7 / Debian 13 — not available on Mint 22.x
echo 'PUNKTFUNK_COMPOSITOR=gamescope' >> ~/.config/punktfunk/host.env
systemctl --user restart punktfunk-host
```
The pin is required: auto-detection reads the live session, finds Cinnamon, and stops with an error
rather than guessing. Set a game to launch with
[`PUNKTFUNK_GAMESCOPE_APP`](/docs/gamescope) or per-session launch commands, then see
[Steam / gamescope](/docs/gamescope) for the rest.
> **Install `punktfunk-gamescope`, not Debian's.** Debian ships **no** `gamescope` package at all,
> and the patched build is what gives the stream HDR, a visible cursor, and the client's real
> refresh rate instead of a hardcoded 60 Hz.
If you want to stream the **desktop** from an LMDE box, the answer today is to log into a GNOME or
Sway session instead — Debian 13 ships GNOME 48.7 and sway 1.10, both above the
[floors](/docs/requirements). (Debian 13's KDE is KWin **6.3.6**, below the 6.5.6 floor, so Plasma
is not an option there yet.)
### Linux Mint 22.x cannot host yet
**On Linux Mint 22.x — the current mainstream release, and every version until Mint 23 in December
2026 — there is no working configuration.** `punktfunk-host` will install, which makes this easy to
miss, but nothing on the box can produce a stream:
- **Cinnamon** cannot host a virtual display (above).
- **gamescope is not available and cannot be made available.** Ubuntu 24.04 packages no gamescope,
and the patched `punktfunk-gamescope` cannot run there either: 24.04 is short of what the build
needs on *five* libraries — wayland 1.22.0 (needs ≥ 1.23.1), libinput 1.25 (≥ 1.26), libavif
1.0.4 (≥ 1.2.1), pixman 0.42 (≥ 0.44), and no `libdisplay-info2` or `libxcb-errors0` at all.
- **Switching desktop does not rescue it.** Ubuntu 24.04 ships KWin **5.27** (floor 6.5.6) and GNOME
Shell **46** (floor 48). Only `sway` 1.9 is even a candidate, and that means giving up Cinnamon.
If you want to run a host on Mint hardware today, use **LMDE 7** — it is the same desktop on a
Debian 13 base, where gamescope works. Otherwise wait for **Mint 23** (Ubuntu 26.04 base), where
both the patched gamescope and the newer compositors are available.
## Configure your desktop
How the host creates its virtual display and injects input depends on your desktop, not your distro:
- [KDE Plasma (KWin)](/docs/kde)
- [GNOME (Mutter)](/docs/gnome)
- [Steam / gamescope](/docs/gamescope)
- [Hyprland](/docs/hyprland)
- [Sway / wlroots](/docs/sway)
Then bring up [The Web Console](/docs/web-console) to arm pairing and connect your first
[client](/docs/clients). To run the host at boot — including fully **headless** — see
[Running as a Service](/docs/running-as-a-service).
## Next steps
- **Keep it current** — [Updating the Host](/docs/updating).
- **Remove it again** — [Uninstalling](/docs/uninstall).
- **Something not working?** — [Troubleshooting](/docs/troubleshooting).
- **Build from source** (Debian 12, or tracking `main`) — the
[Ubuntu appendix](/docs/ubuntu#appendix--build-from-source) applies unchanged; Debian 13's
`libavcodec-dev` is new enough to build against.
+5 -1
View File
@@ -189,7 +189,11 @@ own name and does **not** replace your system gamescope — your Gaming Mode kee
- **Bazzite / Fedora Atomic** — included in the Punktfunk sysext; `punktfunk-sysext update` gets it.
- **Fedora, Nobara and other RPM boxes**`sudo dnf install punktfunk-gamescope` from the same
Punktfunk repo the host comes from.
- **Debian / Ubuntu**`sudo apt install punktfunk-gamescope` from the Punktfunk apt repo.
- **Debian 13 and Ubuntu 26.04**`sudo apt install punktfunk-gamescope` from the Punktfunk apt
repo. It is the only gamescope those two have: Debian packages none at all, and Ubuntu 26.04's
is 3.16.20 — below the [3.16.22 floor](/docs/requirements), so headless capture deadlocks
against the PipeWire 1.6 that same release ships. **Ubuntu 24.04 cannot run it**: the build needs
wayland ≥ 1.23.1 and noble has 1.22.0, so build from source or move to a newer release.
- **Arch** — the `punktfunk-gamescope` package.
- **SteamOS (Steam Deck installer)** — built and wired automatically by
`scripts/steamdeck/install.sh` / `update.sh`.
+2 -2
View File
@@ -36,7 +36,7 @@ stream and links out to the detail as you need it. The rest of these are for whe
<Card title="Quick Start" href="/docs/quickstart" description="From nothing to streaming: set up a host and connect your first client." />
<Card title="How It Works" href="/docs/how-it-works" description="The ideas behind Punktfunk in a few minutes — virtual displays, the two protocols, pairing." />
<Card title="Support Matrix" href="/docs/support-matrix" description="What works where — every host desktop, GPU and client app, each cell read out of the code that decides it." />
<Card title="Install the Host" href="/docs/install" description="Add the repo and install the package — Ubuntu, Fedora, Arch, Bazzite, SteamOS, NixOS, or Windows." />
<Card title="Install the Host" href="/docs/install" description="Add the repo and install the package — Ubuntu, Debian, Fedora, Arch, Bazzite, SteamOS, NixOS, or Windows." />
<Card title="Connect a Client" href="/docs/clients" description="Stream with the native app for your device — macOS, Linux, Windows, Android — or any Moonlight client." />
<Card title="Your Game Library" href="/docs/game-library" description="The host finds your installed games by itself — browse a paired host and launch a title straight into the stream." />
<Card title="API Reference" href="/api" description="Interactive OpenAPI reference for the host's management REST API — status, devices, pairing, library." />
@@ -46,7 +46,7 @@ stream and links out to the detail as you need it. The rest of these are for whe
- A **host** with an NVIDIA, AMD, or Intel GPU (there's a software encoder to fall back on without
one) — either a **Linux** machine running one of the [supported setups](/docs/requirements)
(**Ubuntu**, **Fedora**, **Arch**, **Bazzite**, or **SteamOS**), or a
(**Ubuntu**, **Debian**, **Fedora**, **Arch**, **Bazzite**, or **SteamOS**), or a
**[Windows](/docs/windows-host) PC**. What each combination can actually do — codecs, HDR, 4:4:4,
cursor, input — is in the [Support matrix](/docs/support-matrix).
- A **client device** to stream to — there are native apps for **macOS, iOS/iPadOS, tvOS, Linux,
+8 -1
View File
@@ -16,13 +16,20 @@ On **Windows**, the host ships as a signed installer instead — see [Windows](#
| Distro | Package manager | One-command happy path | Guide |
|--------|-----------------|------------------------|-------|
| **Ubuntu** | apt | `sudo apt install punktfunk-host` | [Ubuntu](/docs/ubuntu) · [packaging/debian](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/debian/README.md) |
| **Ubuntu 26.04+** ¹ | apt | `sudo apt install punktfunk-host` | [Ubuntu](/docs/ubuntu) · [packaging/debian](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/debian/README.md) |
| **Debian 13+** (incl. LMDE) | apt | `sudo apt install punktfunk-host` | [Debian](/docs/debian) · [packaging/debian](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/debian/README.md) |
| **Bazzite / Fedora Atomic** | systemd-sysext | `curl -fsSLO https://git.unom.io/unom/punktfunk/raw/branch/main/packaging/bazzite/punktfunk-sysext.sh && sudo bash punktfunk-sysext.sh install` (no layering, no reboot) | [Bazzite](/docs/bazzite) · [packaging/bazzite](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/bazzite/README.md) |
| **Fedora (dnf)** | dnf / rpm-ostree | `sudo dnf install punktfunk` | [Fedora](/docs/fedora) · [packaging/rpm](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/rpm/README.md) |
| **Arch** | pacman | `sudo pacman -Syu punktfunk-host` (binary repo — always a full `-Syu`, never `-Sy`) | [Arch Linux](/docs/arch) · [packaging/arch](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/arch/README.md) |
| **SteamOS (host)** | on-device script | clone the repo, then `bash ~/punktfunk/scripts/steamdeck/install.sh` (builds on-device) | [SteamOS (Host)](/docs/steamos-host) |
| **NixOS / Nix** | nix flake | `nix run git+https://git.unom.io/unom/punktfunk#punktfunk-host -- serve --gamestream` | [NixOS](#nixos) · [packaging/nix](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/nix/README.md) |
> ¹ **Ubuntu 24.04 LTS installs the package but cannot host.** It ships no compositor that meets
> the [version floors](/docs/requirements) — KWin 5.27 against 6.5.6, GNOME Shell 46 against 48 —
> and no `gamescope`. Use 26.04 or newer. This is also why
> [Linux Mint 22.x cannot host](/docs/debian#linux-mint-22x-cannot-host-yet); LMDE 7 (Debian 13)
> can.
Each registry is public — no auth, you just trust the repo's signing key. Adding the repo is a
one-time step covered in the linked guide; after that, normal `apt upgrade` / `dnf upgrade` /
`pacman -Syu` (or `sudo punktfunk-sysext update` on Bazzite) tracks new builds. On **NixOS** there
+1
View File
@@ -10,6 +10,7 @@
"requirements",
"install",
"ubuntu",
"debian",
"fedora",
"arch",
"bazzite",
+26 -1
View File
@@ -13,9 +13,34 @@ is also available. Setup splits along two axes: you **install** the package per
> New here? Read [Security & Safe Use](/docs/security) first — a streaming host is remote control of
> the machine, so keep it on a trusted LAN or VPN and require pairing.
## The floor for a working host
**On apt distros that means Ubuntu 26.04 or newer, or Debian 13 or newer.** Both are supported
targets and both install from the same repository.
This floor is about the **desktop**, not the package. A host needs a compositor that can create a
virtual display, and those have version floors of their own ([below](#desktop-session)). Older
releases will happily install `punktfunk-host` and then have nothing that can produce a stream —
so read this as the real requirement, not the package's:
| Release | Package installs | Can actually host |
|---|---|---|
| **Ubuntu 26.04+** | ✅ | ✅ KWin 6.5+, GNOME 48+, gamescope |
| **Debian 13+** | ✅ | ✅ GNOME 48.7, sway 1.10, gamescope (its KWin 6.3.6 is below the floor) |
| Ubuntu 24.04 LTS | ✅ | ❌ KWin 5.27 (floor 6.5.6), GNOME 46 (floor 48), no gamescope available |
| Debian 12 | ❌ glibc 2.36 | ❌ |
Ubuntu 24.04 is called out because the package *does* install there — it is built on 24.04 precisely
so one package spans the range — which makes the gap easy to mistake for a bug. It is not: 24.04
ships no compositor new enough, and no `gamescope` (the patched
[`punktfunk-gamescope`](/docs/gamescope) cannot run there either — 24.04 is too old on wayland,
libinput, libavif and pixman). The same gap is why
[Linux Mint 22.x cannot host](/docs/debian#linux-mint-22x-cannot-host-yet).
**Distros — install the package:**
- [Ubuntu](/docs/ubuntu)
- [Ubuntu](/docs/ubuntu) — 26.04 or newer for a working host
- [Debian](/docs/debian) — 13 or newer, including LMDE
- [Fedora](/docs/fedora)
- [Arch](/docs/arch)
- [Bazzite](/docs/bazzite)
+7
View File
@@ -71,6 +71,7 @@ mechanics. The one exception is a gamescope the host only *attaches* to, which k
| gamescope (SteamOS · Bazzite) | ✅ ⁴ | ⚠️ ⁵ | ✅ ⁶ |
| sway ⁹ | ✅ | ✅ | ⚠️ ⁷ |
| Hyprland | ⚠️ ⁸ | ✅ | ⚠️ ⁷ |
| Cinnamon (Mint · LMDE) | ❌ ¹⁰ | ❌ ¹⁰ | ❌ ¹⁰ |
| macOS / anything else | ❌ | ❌ | ❌ |
1. Punktfunk's own IddCx display driver. It requires **Windows 11 22H2 (build 22621) or newer**
@@ -108,6 +109,12 @@ mechanics. The one exception is a gamescope the host only *attaches* to, which k
(River, dwl, …) cannot host — the session fails at `swaymsg get_outputs`. Their input would work
(they do have the wlroots virtual pointer and keyboard protocols), but with no video there is no
stream. See [Sway / wlroots](/docs/sway).
10. Cinnamon's compositor **Muffin** exposes no virtual-output API and no monitor-capture route we
can reach: it forked from Mutter 3.36, so `org.cinnamon.Muffin.ScreenCast` has only
`RecordMonitor` / `RecordWindow` and never Mutter 42's `RecordVirtual`, and its portal backend
(`xdg-desktop-portal-xapp`) implements no ScreenCast at all. Nothing in Punktfunk can change
this. A Mint or LMDE box can still stream **games** through a headless gamescope, which needs no
desktop compositor — see [Debian → Cinnamon](/docs/debian#cinnamon-linux-mint-and-lmde).
### Input, cursor and HDR
+12 -5
View File
@@ -18,11 +18,18 @@ desktop on the [configure pages](#configure-your-desktop) afterward rather than
> and needs GTK4 ≥ 4.20 and SDL3, so it installs on **26.04 or newer** only — the host has no such
> limit.
> **Debian isn't a supported target.** The packages are built on Ubuntu images and their dependencies
> are resolved against Ubuntu's package names, and nothing in CI builds or tests on Debian. Debian 12
> (bookworm) is below the glibc floor and cannot install them at all. A newer Debian may work, but
> it's untested — build from source ([appendix](#appendix--build-from-source)) if you want to try.
> The `debian` in the repository URL below is the *package format*, not a supported distro.
> **On 24.04 LTS, the package installs but the distro gives it no compositor to drive.** The
> host `.deb` is built for 24.04 and installs cleanly — but streaming needs a compositor that meets
> the [version floors](/docs/requirements), and stock 24.04 meets none of them: KWin **5.27** (floor
> 6.5.6), GNOME Shell **46** (floor 48), and no `gamescope` package at all (nor can the patched one
> run there — 24.04 is too old on wayland, libinput, libavif and pixman). `sway` 1.9 is the only
> candidate. **For a working host, use 26.04**, where the patched gamescope and current KDE/GNOME
> are all available. This is why [Linux Mint 22.x cannot host](/docs/debian#linux-mint-22x-cannot-host-yet).
> **On Debian**, see [Debian](/docs/debian) — the host, console and plugin runner are supported and
> CI-tested on **Debian 13**; the desktop client is not packaged for it yet. Debian 12 (bookworm) is
> below the glibc floor and cannot install anything here. Note that the `debian` in the repository
> URL below is the *package format*, and is the same URL for both distros.
## 1. GPU driver
+44
View File
@@ -1,5 +1,18 @@
# punktfunk-host — Debian/Ubuntu package (apt)
> **Which distros the published packages install on** — measured by installing them, not inferred
> from the build image (`scripts/ci/deb-install-smoke.sh` asserts this on every run):
>
> | | Ubuntu 24.04 | Ubuntu 26.04 | Debian 13 | Debian 12 |
> |---|---|---|---|---|
> | `punktfunk-host` | ✅ | ✅ | ✅ | ❌ glibc 2.36 < 2.39 |
> | `punktfunk-web` / `punktfunk-scripting` | ✅ | ✅ | ✅ | ✅ |
> | `punktfunk-gamescope` | ❌ wayland 1.22 | ✅ | ✅ | ❌ |
> | `punktfunk-client` | ❌ `libc6 >= 2.43` | ✅ | ❌ `libc6 >= 2.43` | ❌ |
>
> Debian 13 is a supported host target ([docs](https://docs.punktfunk.unom.io/docs/debian)); the
> client is the one gap, since it is built on 26.04 and floors at that release's glibc.
`punktfunk-host` is published as a `.deb` to **Gitea's Debian package registry** in the public
`unom` org, so the Ubuntu hosts update with plain `apt`. CI (`.gitea/workflows/deb.yml`) builds
and publishes on every push to `main` (a rolling `<next-minor>~ciN.g<sha>` build — the base is
@@ -36,6 +49,37 @@ dropped from `Depends`). The result is **one** host `.deb` that installs on **Ub
on 26.04 (the native client needs SDL3 / GTK4 ≥ 4.20, absent on 24.04) — install the client on the box
you stream *to*, which is independent of the host's distro.
## `punktfunk-gamescope` is built on Debian 13, not Ubuntu
The patched gamescope has its own job (`build-publish-gamescope`) in a **Debian 13** image
(`ci/gamescope-trixie.Dockerfile`), and that is not a preference — it is the only apt distro the
tree configures on. Built in the noble host image, as it was until 2026-08, it failed every single
run:
```
wlroots| Dependency wayland-server found: NO found 1.22.0 but need: '>=1.23.1'
subprojects/wlroots/meson.build:96:17: ERROR: Dependency 'wayland-server' is required but not found
```
Our pin vendors wlroots 0.19.3, which floors wayland-server at 1.23.1; noble ships 1.22.0 (and has
no `libxcb-errors-dev`, and only libdisplay-info 0.1.1). Because every rung of that path was a
`::warning::` returning 0, **v0.26.0 and v0.27.0 both shipped with no gamescope .deb** while the
release notes and docs-site said it was apt-installable. Debian 13 has wayland 1.23.1 exactly —
the oldest apt base that works.
Two things make the one package serve both Debian 13 and Ubuntu 26.04:
- **`--extra-fallback libdisplay-info`** (see `packaging/gamescope/build-punktfunk-gamescope.sh`).
Linked against the distro copy, the package picks up `Depends: libdisplay-info2 (>= 0.2.0)` on
trixie — and Ubuntu 26.04 carries libdisplay-info **3** (0.3.0), so apt refuses it there.
gamescope vendors the library as a submodule, so the vendored build drops the dependency. Same
reasoning the script already applies to wlroots: a binary we ship must not follow the build
host's shared libraries.
- The **static C++ runtime** the build script already forces, so `libstdc++` never appears in
`NEEDED`. The binary asks only for `GLIBC_2.38`.
**Ubuntu 24.04 gets no gamescope package** and cannot: the wayland floor is a runtime one too.
## Install on a host (one-time)
The registry is public, so no apt auth is needed — just trust the repo's signing key:
@@ -28,7 +28,7 @@ set -euo pipefail
GAMESCOPE_REV="5fb8dce4a09d0a68d097b9faf9513782106bc843"
GAMESCOPE_REPO="https://github.com/ValveSoftware/gamescope.git"
REV="$GAMESCOPE_REV" PREFIX=/usr DESTDIR="" SRCDIR="" JOBS="" SETCAP=1
REV="$GAMESCOPE_REV" PREFIX=/usr DESTDIR="" SRCDIR="" JOBS="" SETCAP=1 EXTRA_FALLBACK=""
while [ $# -gt 0 ]; do
case "$1" in
--rev) REV="${2:?}"; shift 2 ;;
@@ -37,6 +37,15 @@ while [ $# -gt 0 ]; do
--srcdir) SRCDIR="${2:?}"; shift 2 ;;
--jobs) JOBS="${2:?}"; shift 2 ;;
--no-setcap) SETCAP=0; shift ;;
# Extra `force_fallback_for` entries, comma-separated, appended to the mandatory three below.
# Exists for ONE package family: the .deb has to install on both Debian 13 and Ubuntu 26.04,
# and those two disagree on the libdisplay-info SONAME (0.2.0 -> libdisplay-info2 vs 0.3.0 ->
# libdisplay-info3), so a package built against either one is uninstallable on the other.
# Vendoring it makes ONE .deb serve both. Opt-in rather than baked in, so the Arch/Fedora/nix
# packages — which have no such split and are shipping fine — keep producing exactly the binary
# they produce today. Its only caller is the `build-publish-gamescope` job in
# .gitea/workflows/deb.yml, which passes `--extra-fallback libdisplay-info`.
--extra-fallback) EXTRA_FALLBACK="${2:?}"; shift 2 ;;
*) echo "unknown argument: $1" >&2; exit 1 ;;
esac
done
@@ -111,7 +120,7 @@ export LDFLAGS="${LDFLAGS:-} -static-libstdc++ -static-libgcc"
meson setup "$BUILD" "$SRCDIR" \
--prefix="$PREFIX" \
--buildtype=release \
-Dforce_fallback_for=libliftoff,vkroots,wlroots \
-Dforce_fallback_for="libliftoff,vkroots,wlroots${EXTRA_FALLBACK:+,$EXTRA_FALLBACK}" \
-Dpipewire=enabled \
-Denable_tests=false \
-Denable_openvr_support=false \
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env bash
# Install the published .debs on every supported apt distro, in a pristine image, the way a user
# would — repo key, sources.list line, `apt-get install`.
#
# WHY THIS EXISTS: nothing in deb.yml ever installed a package it built. Two facts survived a long
# time in that blind spot, in opposite directions:
# * `punktfunk-host` installed cleanly on Debian 13 for months while docs-site said Debian was
# unsupported and unverified.
# * `punktfunk-gamescope` was missing from the apt registry across two releases, while the docs
# told Debian/Ubuntu users to `apt install` it.
# Both are what a five-minute install check catches.
#
# Usage: bash scripts/ci/deb-install-smoke.sh
# PF_APT_DISTRIBUTION stable | canary (default: stable)
# PF_EXPECT_VERSION if set, `punktfunk-host --version` must contain it — proves the run is
# testing the artifact THIS run published and not a leftover from an
# earlier one (the failure mode that makes a green check meaningless).
# PF_SMOKE_IMAGES override the image list, space-separated (local runs / bisecting).
set -euo pipefail
DIST="${PF_APT_DISTRIBUTION:-stable}"
EXPECT="${PF_EXPECT_VERSION:-}"
REPO_URL="https://git.unom.io/api/packages/unom/debian"
# THE SUPPORT MATRIX, as an assertion instead of a paragraph. Each row is
# "<image>|<packages that must install>". A package absent from a row is one we do NOT claim works
# there, and it is deliberately NOT asserted absent — that would turn every future improvement into
# a red build. The omissions and their reasons:
# ubuntu:24.04 no client — built on 26.04, floors at libc6 >= 2.43 (noble has 2.39)
# no gamescope — noble's wayland is 1.22.0; the vendored wlroots needs >= 1.23.1,
# so the binary cannot even load there
# debian:trixie no client — same libc6 >= 2.43 floor (trixie has 2.41), plus GTK4 >= 4.20
# Debian 12 (bookworm) is absent entirely: glibc 2.36 is below the host's 2.39 floor, so nothing
# we ship installs there and there is nothing to smoke-test.
MATRIX=(
"ubuntu:24.04|punktfunk-host punktfunk-web punktfunk-scripting"
"ubuntu:26.04|punktfunk-host punktfunk-web punktfunk-scripting punktfunk-client punktfunk-gamescope"
"debian:trixie|punktfunk-host punktfunk-web punktfunk-scripting punktfunk-gamescope"
)
if [ -n "${PF_SMOKE_IMAGES:-}" ]; then
FILTERED=()
for row in "${MATRIX[@]}"; do
for want in $PF_SMOKE_IMAGES; do
[ "${row%%|*}" = "$want" ] && FILTERED+=("$row")
done
done
MATRIX=("${FILTERED[@]}")
fi
echo "smoke-installing from '$DIST'${EXPECT:+ (expecting host version $EXPECT)}"
FAILED=()
for row in "${MATRIX[@]}"; do
IMAGE="${row%%|*}"
PACKAGES="${row#*|}"
echo
echo "==================== $IMAGE ===================="
echo "packages: $PACKAGES"
# `docker run` inherits nothing from this shell — every value the container needs is passed
# explicitly, so a typo here is a hard failure rather than a silently empty variable.
if docker run --rm --platform linux/amd64 \
-e DEBIAN_FRONTEND=noninteractive \
-e DIST="$DIST" -e PACKAGES="$PACKAGES" -e EXPECT="$EXPECT" -e REPO_URL="$REPO_URL" \
"$IMAGE" bash -euxc '
apt-get update -qq
apt-get install -y -qq --no-install-recommends curl ca-certificates
install -d -m 0755 /etc/apt/keyrings
curl -fsSL --max-time 60 "$REPO_URL/repository.key" -o /etc/apt/keyrings/punktfunk.asc
echo "deb [signed-by=/etc/apt/keyrings/punktfunk.asc] $REPO_URL $DIST main" \
> /etc/apt/sources.list.d/punktfunk.list
apt-get update -qq
# Wait for the index to actually carry the version this run published. Gitea regenerates
# the apt Packages file after an upload, so a smoke job that starts immediately can see
# the PREVIOUS build — install it, pass, and prove nothing about the new one. Bounded:
# if it never appears, that is a real publishing failure and the job should say so.
if [ -n "$EXPECT" ]; then
for i in $(seq 1 10); do
apt-cache policy punktfunk-host | grep -qF "$EXPECT" && break
echo "index does not carry $EXPECT yet (attempt $i) — waiting"
sleep 15
apt-get update -qq
done
apt-cache policy punktfunk-host | grep -qF "$EXPECT" || {
echo "the apt index never served $EXPECT — publish did not land"
apt-cache policy punktfunk-host
exit 1
}
fi
# The real thing: unpack + run every maintainer script, exactly as a user would.
apt-get install -y $PACKAGES
# Installed is not the same as working. Assert every shipped binary RESOLVED its shared
# libraries and, where it is safe to run headless, that it executes — an unsatisfied
# soname is invisible to dpkg but fatal to the user, and it is exactly what a
# distro-mismatched build produces.
# (`if ldd | grep; then fail` rather than `grep && exit 1`: the latter leaves the block
# returning grep NOT-found = 1, which under `set -e` fails the container on success.)
for pkg in $PACKAGES; do
for bin in $(dpkg -L "$pkg" | grep "^/usr/bin/" || true); do
if ldd "$bin" 2>/dev/null | grep -F "not found"; then
echo "UNRESOLVED SONAME in $bin (from $pkg)"
exit 1
fi
done
done
# --version is the cheapest proof of "actually runs". Only for binaries that answer it
# without a session/GPU: the client opens GTK, the console is a bun bundle.
if echo "$PACKAGES" | grep -q punktfunk-host; then
punktfunk-host --version
[ -z "$EXPECT" ] || punktfunk-host --version | grep -F "$EXPECT"
fi
if echo "$PACKAGES" | grep -q punktfunk-gamescope; then
punktfunk-gamescope --version
fi
'; then
echo "PASS: $IMAGE"
else
echo "::error::$IMAGE — the published packages do not install ($PACKAGES)"
FAILED+=("$IMAGE")
fi
done
echo
if [ ${#FAILED[@]} -gt 0 ]; then
echo "install smoke FAILED on: ${FAILED[*]}"
exit 1
fi
echo "install smoke passed on every supported distro"