diff --git a/.gitea/workflows/deb.yml b/.gitea/workflows/deb.yml index 58ab0f13..0697c8f1 100644 --- a/.gitea/workflows/deb.yml +++ b/.gitea/workflows/deb.yml @@ -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 }} diff --git a/.gitea/workflows/docker.yml b/.gitea/workflows/docker.yml index 003f4491..5bdf3f81 100644 --- a/.gitea/workflows/docker.yml +++ b/.gitea/workflows/docker.yml @@ -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. diff --git a/CHANGELOG.md b/CHANGELOG.md index c21baabc..34ac78cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -220,6 +220,42 @@ Helldivers 2 at 1% lows of 2–5 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. diff --git a/README.md b/README.md index 8ce46f51..7a70385e 100644 --- a/README.md +++ b/README.md @@ -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) | diff --git a/ci/gamescope-trixie.Dockerfile b/ci/gamescope-trixie.Dockerfile new file mode 100644 index 00000000..113cf817 --- /dev/null +++ b/ci/gamescope-trixie.Dockerfile @@ -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 /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" diff --git a/crates/pf-vdisplay/src/lib.rs b/crates/pf-vdisplay/src/lib.rs index f27baa20..11779ab4 100644 --- a/crates/pf-vdisplay/src/lib.rs +++ b/crates/pf-vdisplay/src/lib.rs @@ -321,11 +321,7 @@ pub fn detect() -> Result { #[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 { 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 { + // 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!( diff --git a/docs-site/content/docs/debian.md b/docs-site/content/docs/debian.md new file mode 100644 index 00000000..54a39c98 --- /dev/null +++ b/docs-site/content/docs/debian.md @@ -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. diff --git a/docs-site/content/docs/gamescope.md b/docs-site/content/docs/gamescope.md index 4109fa91..6db58768 100644 --- a/docs-site/content/docs/gamescope.md +++ b/docs-site/content/docs/gamescope.md @@ -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`. diff --git a/docs-site/content/docs/index.mdx b/docs-site/content/docs/index.mdx index cc511b63..f5ade04b 100644 --- a/docs-site/content/docs/index.mdx +++ b/docs-site/content/docs/index.mdx @@ -36,7 +36,7 @@ stream and links out to the detail as you need it. The rest of these are for whe - + @@ -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, diff --git a/docs-site/content/docs/install.md b/docs-site/content/docs/install.md index 8dcb467e..88879ab6 100644 --- a/docs-site/content/docs/install.md +++ b/docs-site/content/docs/install.md @@ -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 diff --git a/docs-site/content/docs/meta.json b/docs-site/content/docs/meta.json index 548edd9a..5ef24668 100644 --- a/docs-site/content/docs/meta.json +++ b/docs-site/content/docs/meta.json @@ -10,6 +10,7 @@ "requirements", "install", "ubuntu", + "debian", "fedora", "arch", "bazzite", diff --git a/docs-site/content/docs/requirements.md b/docs-site/content/docs/requirements.md index aaf0c575..d4e2ae73 100644 --- a/docs-site/content/docs/requirements.md +++ b/docs-site/content/docs/requirements.md @@ -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) diff --git a/docs-site/content/docs/support-matrix.md b/docs-site/content/docs/support-matrix.md index 362ec95a..2f7c4358 100644 --- a/docs-site/content/docs/support-matrix.md +++ b/docs-site/content/docs/support-matrix.md @@ -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 diff --git a/docs-site/content/docs/ubuntu.md b/docs-site/content/docs/ubuntu.md index 2fbbde51..bda0c980 100644 --- a/docs-site/content/docs/ubuntu.md +++ b/docs-site/content/docs/ubuntu.md @@ -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 diff --git a/packaging/debian/README.md b/packaging/debian/README.md index 12ddae5b..e83c5599 100644 --- a/packaging/debian/README.md +++ b/packaging/debian/README.md @@ -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 `~ciN.g` 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: diff --git a/packaging/gamescope/build-punktfunk-gamescope.sh b/packaging/gamescope/build-punktfunk-gamescope.sh index f2ed3075..9b7bf070 100755 --- a/packaging/gamescope/build-punktfunk-gamescope.sh +++ b/packaging/gamescope/build-punktfunk-gamescope.sh @@ -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 \ diff --git a/scripts/ci/deb-install-smoke.sh b/scripts/ci/deb-install-smoke.sh new file mode 100755 index 00000000..31301471 --- /dev/null +++ b/scripts/ci/deb-install-smoke.sh @@ -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 +# "|". 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"