Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85980b425e | ||
|
|
107fa3472d | ||
|
|
5d8682d7b7 | ||
|
|
030bc8a1c2 | ||
|
|
346385bad8 | ||
|
|
124cb66324 | ||
|
|
6774c4e7a2 |
+170
-99
@@ -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 }}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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
@@ -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!(
|
||||
|
||||
@@ -7,14 +7,22 @@
|
||||
//!
|
||||
//! Reliability (this is the whole point — a sleeping host has no ARP entry, so a plain unicast
|
||||
//! can't wake it, and `255.255.255.255` alone leaves only via the default route). For each
|
||||
//! known host MAC we send the 102-byte packet to:
|
||||
//! * every non-loopback IPv4 interface's **subnet-directed broadcast** (routes to that NIC's
|
||||
//! segment — this is what covers multi-homed clients on VPN/docker/multiple LANs), and
|
||||
//! * the **limited broadcast** `255.255.255.255`, and
|
||||
//! * optionally a **unicast** to the host's last-known IP (covers the brief window where the
|
||||
//! host is reachable but hasn't re-advertised, and NICs that wake on a directed unicast),
|
||||
//! known host MAC we send the 102-byte packet:
|
||||
//! * **out of every non-loopback IPv4 interface**, from a socket bound to that interface's own
|
||||
//! address, to both that NIC's **subnet-directed broadcast** and the **limited broadcast**
|
||||
//! `255.255.255.255` — binding the source is what forces the datagram onto that segment
|
||||
//! instead of whatever the default route happens to be (a VPN/mesh interface, typically), and
|
||||
//! * from an unbound socket to `255.255.255.255` and, when known, a **unicast** to the host's
|
||||
//! last-known IP (covers the brief window where the host is reachable but hasn't
|
||||
//! re-advertised, and NICs that wake on a directed unicast),
|
||||
//!
|
||||
//! on the two conventional WoL ports (9 and 7), repeated a few times to survive UDP loss.
|
||||
//!
|
||||
//! **Wi-Fi hosts (WoWLAN) ride the same path**, and the per-interface egress above is what makes
|
||||
//! them work: a station in WoWLAN sleep stays associated, and the AP buffers broadcast frames for
|
||||
//! its sleeping stations and flushes them on the next DTIM beacon — so the broadcast does reach
|
||||
//! the sleeping NIC, but only if the datagram actually leaves via the wireless interface. The
|
||||
//! host end of it (arming the NIC's magic-packet trigger) is `punktfunk-host`'s `wol` module.
|
||||
|
||||
use std::io;
|
||||
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, UdpSocket};
|
||||
@@ -64,41 +72,63 @@ pub fn build_magic_packet(mac: Mac) -> [u8; 102] {
|
||||
/// directed broadcast with no route) doesn't fail the whole wake. Errors only if no socket
|
||||
/// could be opened or nothing could be sent at all.
|
||||
pub fn send_magic_packet(macs: &[Mac], last_known_ip: Option<Ipv4Addr>) -> io::Result<()> {
|
||||
send_magic_packet_on(macs, last_known_ip, &WOL_PORTS)
|
||||
}
|
||||
|
||||
/// [`send_magic_packet`] with the destination ports spelled out. Private because the ports are
|
||||
/// not a caller's business — it exists so the tests can aim a real send at a port they're allowed
|
||||
/// to bind (9 and 7 are privileged) and assert the bytes that come off the wire.
|
||||
fn send_magic_packet_on(
|
||||
macs: &[Mac],
|
||||
last_known_ip: Option<Ipv4Addr>,
|
||||
ports: &[u16],
|
||||
) -> io::Result<()> {
|
||||
if macs.is_empty() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"no MAC addresses",
|
||||
));
|
||||
}
|
||||
let packets: Vec<[u8; 102]> = macs.iter().map(|m| build_magic_packet(*m)).collect();
|
||||
|
||||
// Build the target IP set: each interface's directed broadcast, the limited broadcast, and
|
||||
// the optional last-known unicast. Dedup so a single-NIC client doesn't send twice.
|
||||
let mut targets = broadcast_addrs();
|
||||
targets.push(Ipv4Addr::BROADCAST); // 255.255.255.255
|
||||
// Targets that go out the default route (or wherever the routing table sends them): the
|
||||
// limited broadcast as a baseline, plus the optional unicast — destination routing picks the
|
||||
// right NIC for a unicast, so it doesn't need per-interface treatment.
|
||||
let mut routed: Vec<Ipv4Addr> = vec![Ipv4Addr::BROADCAST];
|
||||
if let Some(ip) = last_known_ip {
|
||||
targets.push(ip);
|
||||
routed.push(ip);
|
||||
}
|
||||
targets.sort_unstable();
|
||||
targets.dedup();
|
||||
|
||||
// One broadcast-enabled socket bound to all interfaces. Directed broadcasts route to the
|
||||
// matching NIC via the routing table; the limited broadcast leaves via the default route.
|
||||
let sock = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))?;
|
||||
sock.set_broadcast(true)?;
|
||||
|
||||
let mut sent_any = false;
|
||||
for _ in 0..BURST {
|
||||
for mac in macs {
|
||||
let pkt = build_magic_packet(*mac);
|
||||
for ip in &targets {
|
||||
for port in WOL_PORTS {
|
||||
let dst = SocketAddr::V4(SocketAddrV4::new(*ip, port));
|
||||
if sock.send_to(&pkt, dst).is_ok() {
|
||||
sent_any = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Per-interface pass. One socket per non-loopback IPv4 address, bound to that address so the
|
||||
// datagram leaves on THAT segment: without this, `255.255.255.255` follows the default route
|
||||
// only (a VPN/mesh NIC on most of these machines) and never touches the LAN — or the Wi-Fi
|
||||
// segment the sleeping WoWLAN station is associated to.
|
||||
for (local, bcast) in local_v4_segments() {
|
||||
let Ok(sock) = UdpSocket::bind(SocketAddrV4::new(local, 0)) else {
|
||||
// Bind failed (address just went away, or the OS refuses it) — fall back to the
|
||||
// routed socket below, which still reaches this segment's directed broadcast.
|
||||
routed.push(bcast);
|
||||
continue;
|
||||
};
|
||||
if sock.set_broadcast(true).is_err() {
|
||||
routed.push(bcast);
|
||||
continue;
|
||||
}
|
||||
sent_any |= blast(&sock, &packets, &[bcast, Ipv4Addr::BROADCAST], ports);
|
||||
}
|
||||
|
||||
// Routed pass, and the only pass on a machine whose interfaces can't be enumerated.
|
||||
if let Ok(sock) = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0)) {
|
||||
// A refused SO_BROADCAST doesn't abort the pass: the unicast target still goes out, and
|
||||
// the per-interface sockets above may already have carried the broadcast.
|
||||
let _ = sock.set_broadcast(true);
|
||||
routed.sort_unstable();
|
||||
routed.dedup();
|
||||
sent_any |= blast(&sock, &packets, &routed, ports);
|
||||
} else if !sent_any {
|
||||
return Err(io::Error::other("no socket could be opened for the wake"));
|
||||
}
|
||||
|
||||
if sent_any {
|
||||
@@ -108,10 +138,33 @@ pub fn send_magic_packet(macs: &[Mac], last_known_ip: Option<Ipv4Addr>) -> io::R
|
||||
}
|
||||
}
|
||||
|
||||
/// Subnet-directed broadcast address of every non-loopback IPv4 interface (`ip | !netmask`,
|
||||
/// or the OS-provided broadcast when present). Best-effort: interface enumeration failing
|
||||
/// (permissions, exotic platform) yields an empty list, and the limited broadcast still fires.
|
||||
fn broadcast_addrs() -> Vec<Ipv4Addr> {
|
||||
/// Send every packet to every target, on every port, [`BURST`] times. Returns whether any
|
||||
/// single datagram made it out — an unroutable target is expected and never fails the wake.
|
||||
fn blast(sock: &UdpSocket, packets: &[[u8; 102]], targets: &[Ipv4Addr], ports: &[u16]) -> bool {
|
||||
let mut sent_any = false;
|
||||
for _ in 0..BURST {
|
||||
for pkt in packets {
|
||||
for ip in targets {
|
||||
// A degenerate 0.0.0.0 (unconfigured NIC) is not a destination.
|
||||
if ip.is_unspecified() {
|
||||
continue;
|
||||
}
|
||||
for port in ports {
|
||||
let dst = SocketAddr::V4(SocketAddrV4::new(*ip, *port));
|
||||
if sock.send_to(pkt, dst).is_ok() {
|
||||
sent_any = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sent_any
|
||||
}
|
||||
|
||||
/// Every non-loopback IPv4 interface as `(its own address, its subnet-directed broadcast)`. The
|
||||
/// broadcast is the OS-provided one where present, else `ip | !netmask`. Best-effort: enumeration
|
||||
/// failing (permissions, exotic platform) yields an empty list and the routed pass still fires.
|
||||
fn local_v4_segments() -> Vec<(Ipv4Addr, Ipv4Addr)> {
|
||||
let mut out = Vec::new();
|
||||
let ifaces = match if_addrs::get_if_addrs() {
|
||||
Ok(i) => i,
|
||||
@@ -122,14 +175,13 @@ fn broadcast_addrs() -> Vec<Ipv4Addr> {
|
||||
continue;
|
||||
}
|
||||
if let if_addrs::IfAddr::V4(v4) = iface.addr {
|
||||
if v4.ip.is_unspecified() {
|
||||
continue; // nothing to bind to
|
||||
}
|
||||
let bcast = v4
|
||||
.broadcast
|
||||
.unwrap_or_else(|| Ipv4Addr::from(u32::from(v4.ip) | !u32::from(v4.netmask)));
|
||||
// Skip a degenerate 0.0.0.0 (unconfigured) and the all-ones limited broadcast we
|
||||
// already add unconditionally.
|
||||
if !bcast.is_unspecified() && bcast != Ipv4Addr::BROADCAST {
|
||||
out.push(bcast);
|
||||
}
|
||||
out.push((v4.ip, bcast));
|
||||
}
|
||||
}
|
||||
out
|
||||
@@ -183,10 +235,47 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn broadcast_addrs_never_contains_limited_or_unspecified() {
|
||||
for b in broadcast_addrs() {
|
||||
assert_ne!(b, Ipv4Addr::BROADCAST);
|
||||
assert!(!b.is_unspecified());
|
||||
fn local_segments_are_bindable_and_have_a_broadcast() {
|
||||
for (local, bcast) in local_v4_segments() {
|
||||
// The local address is what we bind the per-interface socket to, so it must be a
|
||||
// real address — and it must never be the loopback (filtered) or unspecified.
|
||||
assert!(!local.is_unspecified());
|
||||
assert!(!local.is_loopback());
|
||||
assert!(!bcast.is_unspecified());
|
||||
// Binding to an address the OS just reported must work; a failure here would mean
|
||||
// the per-interface pass silently degrades to the routed one.
|
||||
assert!(UdpSocket::bind(SocketAddrV4::new(local, 0)).is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blast_reports_nothing_sent_for_an_empty_target_list() {
|
||||
let sock = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).expect("bind loopback");
|
||||
let pkt = [build_magic_packet([1, 2, 3, 4, 5, 6])];
|
||||
assert!(!blast(&sock, &pkt, &[], &WOL_PORTS));
|
||||
// An unconfigured 0.0.0.0 target is skipped rather than sent to.
|
||||
assert!(!blast(&sock, &pkt, &[Ipv4Addr::UNSPECIFIED], &WOL_PORTS));
|
||||
// Loopback is a real destination — this one must go out.
|
||||
assert!(blast(&sock, &pkt, &[Ipv4Addr::LOCALHOST], &[9999]));
|
||||
}
|
||||
|
||||
/// The whole send path, end to end: a real receiver gets a real magic packet with the right
|
||||
/// bytes. Aimed at loopback on an unprivileged port (WoL's own 9 and 7 need root to bind),
|
||||
/// which exercises the routed pass's unicast leg — the one a WoWLAN host is woken by when
|
||||
/// the AP filters broadcast to sleeping stations.
|
||||
#[test]
|
||||
fn send_delivers_the_magic_packet_to_a_listener() {
|
||||
let rx = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).expect("bind receiver");
|
||||
let port = rx.local_addr().expect("local addr").port();
|
||||
rx.set_read_timeout(Some(std::time::Duration::from_secs(5)))
|
||||
.expect("read timeout");
|
||||
|
||||
let mac: Mac = [0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02];
|
||||
send_magic_packet_on(&[mac], Some(Ipv4Addr::LOCALHOST), &[port]).expect("send");
|
||||
|
||||
let mut buf = [0u8; 256];
|
||||
let (n, _from) = rx.recv_from(&mut buf).expect("a magic packet must arrive");
|
||||
assert_eq!(n, 102);
|
||||
assert_eq!(buf[..102], build_magic_packet(mac));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
//! Host-side Wake-on-LAN support.
|
||||
//! Host-side Wake-on-LAN / Wake-on-Wireless-LAN support.
|
||||
//!
|
||||
//! Two jobs, both best-effort (a failure here never affects streaming):
|
||||
//! 1. [`wake_macs`] — report the host's wake-capable NIC MAC(s) so a client can persist them
|
||||
//! (from the mDNS `mac` TXT record, [`crate::discovery`]) and wake this host later, once it's
|
||||
//! asleep and no longer advertising.
|
||||
//! asleep and no longer advertising. Wired and Wi-Fi NICs alike: a magic packet is the same
|
||||
//! packet either way, and an associated station in WoWLAN sleep receives the broadcast the
|
||||
//! AP buffers for it.
|
||||
//! 2. [`warn_if_not_armed`] — *detect & warn only* whether the NIC is actually armed to wake on a
|
||||
//! magic packet. We never change NIC settings (that's the user's call); we just surface the
|
||||
//! single most common reason WoL silently fails.
|
||||
//!
|
||||
//! Wired and wireless are armed through completely different interfaces, so the check follows the
|
||||
//! NIC: `ethtool <iface>` reports the wired `Wake-on: g` bit, while a Wi-Fi NIC's magic-packet
|
||||
//! trigger lives in nl80211's WoWLAN state and is read with `iw phy <phy> wowlan show`. Asking
|
||||
//! ethtool about a Wi-Fi NIC is what the previous version did, and it is actively misleading:
|
||||
//! most wireless drivers print `Wake-on: d` whether or not WoWLAN is armed, so an armed host got
|
||||
//! warned that it wasn't — with a fix command (`ethtool -s wlan0 wol g`) that its driver rejects.
|
||||
|
||||
use std::net::IpAddr;
|
||||
|
||||
@@ -61,8 +70,8 @@ pub fn wake_macs(primary_ip: IpAddr) -> Vec<String> {
|
||||
}
|
||||
|
||||
/// Log whether the host NIC bearing `primary_ip` is armed to wake on a magic packet. Detect &
|
||||
/// warn only — never modifies settings. Linux-only (reads `ethtool <iface>`); a no-op elsewhere
|
||||
/// and silent when it can't tell (no `ethtool`, insufficient privilege).
|
||||
/// warn only — never modifies settings. Linux-only (shells out to `iw`/`ethtool`); a no-op
|
||||
/// elsewhere and silent when it can't tell (tool missing, insufficient privilege).
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn warn_if_not_armed(primary_ip: IpAddr) {
|
||||
let ifaces = if_addrs::get_if_addrs().unwrap_or_default();
|
||||
@@ -73,6 +82,41 @@ pub fn warn_if_not_armed(primary_ip: IpAddr) {
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
// A NIC with an nl80211 phy is wireless: ask nl80211 about WoWLAN, not ethtool about WoL.
|
||||
if let Some(phy) = wireless_phy(&iface) {
|
||||
match wowlan_has_magic(phy.as_deref(), &iface) {
|
||||
Some(true) => tracing::info!(
|
||||
iface = %iface,
|
||||
phy = phy.as_deref().unwrap_or("?"),
|
||||
"Wake-on-WLAN armed (magic packet) on host Wi-Fi NIC"
|
||||
),
|
||||
Some(false) => {
|
||||
let phy = phy.as_deref().unwrap_or("phy0");
|
||||
// A device the kernel won't arm can't wake on anything, so name that separately
|
||||
// — enabling a WoWLAN trigger alone would not fix it.
|
||||
let extra = if device_wakeup_enabled(&iface) == Some(false) {
|
||||
" The kernel also has wake-up switched off for this device \
|
||||
(/sys/class/net/<iface>/device/power/wakeup reads `disabled`), which blocks \
|
||||
a network wake by itself."
|
||||
} else {
|
||||
""
|
||||
};
|
||||
tracing::warn!(
|
||||
iface = %iface,
|
||||
"Wake-on-WLAN is NOT armed on this host's Wi-Fi NIC — clients cannot wake it \
|
||||
from sleep. Enable it with: sudo iw phy {phy} wowlan enable magic-packet \
|
||||
(NetworkManager resets that on every re-connect; make it stick with: sudo \
|
||||
nmcli connection modify <connection> 802-11-wireless.wake-on-wlan magic). \
|
||||
The adapter must also stay powered and associated while the host sleeps, and \
|
||||
be allowed to wake the machine in BIOS/UEFI.{extra}",
|
||||
)
|
||||
}
|
||||
None => {} // couldn't determine — stay quiet rather than cry wolf
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
match ethtool_wol_has_magic(&iface) {
|
||||
Some(true) => {
|
||||
tracing::info!(iface = %iface, "Wake-on-LAN armed (magic packet) on host NIC")
|
||||
@@ -81,7 +125,7 @@ pub fn warn_if_not_armed(primary_ip: IpAddr) {
|
||||
iface = %iface,
|
||||
"Wake-on-LAN is NOT armed on this host's NIC — clients cannot wake it from sleep. \
|
||||
Enable it with: sudo ethtool -s {iface} wol g (and turn on 'Wake on LAN'/'Wake on \
|
||||
PCIe' in BIOS). Wired Ethernet is required; Wi-Fi wake is unreliable.",
|
||||
PCIe' in BIOS).",
|
||||
),
|
||||
None => {} // couldn't determine — stay quiet rather than cry wolf
|
||||
}
|
||||
@@ -90,6 +134,80 @@ pub fn warn_if_not_armed(primary_ip: IpAddr) {
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub fn warn_if_not_armed(_primary_ip: IpAddr) {}
|
||||
|
||||
/// Is `iface` a Wi-Fi NIC, and if so which nl80211 phy backs it? `Some(Some("phy0"))` = wireless
|
||||
/// and we know the phy (so we can query and name it); `Some(None)` = wireless but the phy name
|
||||
/// couldn't be read; `None` = wired (or sysfs is unavailable, which reads the same way — the
|
||||
/// ethtool path then applies, exactly as before).
|
||||
#[cfg(target_os = "linux")]
|
||||
fn wireless_phy(iface: &str) -> Option<Option<String>> {
|
||||
let dir = format!("/sys/class/net/{iface}/phy80211");
|
||||
if !std::path::Path::new(&dir).exists() {
|
||||
return None;
|
||||
}
|
||||
let name = std::fs::read_to_string(format!("{dir}/name"))
|
||||
.ok()
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty());
|
||||
Some(name)
|
||||
}
|
||||
|
||||
/// Whether a Wi-Fi NIC is armed for a magic-packet wake. `iw` is authoritative — it reads the
|
||||
/// live nl80211 WoWLAN state, which is where the trigger actually lives.
|
||||
///
|
||||
/// Two fallbacks for when `iw` can't answer (binary missing, driver without the WoWLAN command,
|
||||
/// no phy name, or a kernel that wants privilege we don't have — the host runs as a plain user
|
||||
/// service, so that last one is not hypothetical):
|
||||
/// * a *positive* ethtool reading counts, a negative one never does — a handful of drivers
|
||||
/// (brcmfmac and friends, i.e. most Raspberry Pi / SoC Wi-Fi) really do expose the
|
||||
/// magic-packet bit through ethtool, while the far more common `Wake-on: d` from a wireless
|
||||
/// driver means nothing at all;
|
||||
/// * failing that, sysfs `device/power/wakeup` — world-readable, and a `disabled` there is
|
||||
/// conclusive in the negative direction: the kernel will not arm this device to wake the
|
||||
/// machine, so whatever WoWLAN triggers the firmware holds can never fire.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn wowlan_has_magic(phy: Option<&str>, iface: &str) -> Option<bool> {
|
||||
if let Some(v) = phy.and_then(iw_wowlan_has_magic) {
|
||||
return Some(v);
|
||||
}
|
||||
if let Some(true) = ethtool_wol_has_magic(iface) {
|
||||
return Some(true);
|
||||
}
|
||||
// Only the negative is meaningful: `enabled` says the device may wake the machine, not that a
|
||||
// magic packet is one of the things that will do it.
|
||||
match device_wakeup_enabled(iface) {
|
||||
Some(false) => Some(false),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// sysfs `/sys/class/net/<iface>/device/power/wakeup` — `enabled`/`disabled`, i.e. whether the
|
||||
/// kernel will arm this device to wake the system at all. `None` when the attribute isn't there
|
||||
/// (platform/SDIO devices often have none) or can't be read.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn device_wakeup_enabled(iface: &str) -> Option<bool> {
|
||||
let text =
|
||||
std::fs::read_to_string(format!("/sys/class/net/{iface}/device/power/wakeup")).ok()?;
|
||||
match text.trim() {
|
||||
"enabled" => Some(true),
|
||||
"disabled" => Some(false),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Ask nl80211 (via `iw phy <phy> wowlan show`) whether the magic-packet trigger is enabled.
|
||||
/// `None` if `iw` is missing or the driver doesn't implement WoWLAN.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn iw_wowlan_has_magic(phy: &str) -> Option<bool> {
|
||||
let out = std::process::Command::new("iw")
|
||||
.args(["phy", phy, "wowlan", "show"])
|
||||
.output()
|
||||
.ok()?;
|
||||
if !out.status.success() {
|
||||
return None;
|
||||
}
|
||||
parse_iw_wowlan(&String::from_utf8_lossy(&out.stdout))
|
||||
}
|
||||
|
||||
/// Parse `ethtool <iface>` for the *current* Wake-on setting and report whether it includes `g`
|
||||
/// (wake on MagicPacket). Returns `None` if ethtool is missing/failed or the field is absent.
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -101,7 +219,13 @@ fn ethtool_wol_has_magic(iface: &str) -> Option<bool> {
|
||||
if !out.status.success() {
|
||||
return None;
|
||||
}
|
||||
let text = String::from_utf8_lossy(&out.stdout);
|
||||
parse_ethtool_wol(&String::from_utf8_lossy(&out.stdout))
|
||||
}
|
||||
|
||||
/// `ethtool <iface>` output → does the *current* Wake-on setting include `g` (MagicPacket)?
|
||||
/// `None` when the field is absent. Split out from the command so it can be unit-tested on any
|
||||
/// platform.
|
||||
fn parse_ethtool_wol(text: &str) -> Option<bool> {
|
||||
for line in text.lines() {
|
||||
let t = line.trim();
|
||||
// The current setting is "Wake-on: <flags>"; skip the "Supports Wake-on: ..." capability
|
||||
@@ -112,3 +236,88 @@ fn ethtool_wol_has_magic(iface: &str) -> Option<bool> {
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// `iw phy <phy> wowlan show` output → is the magic-packet trigger enabled? The two shapes are
|
||||
///
|
||||
/// ```text
|
||||
/// WoWLAN is disabled
|
||||
/// ```
|
||||
/// ```text
|
||||
/// WoWLAN is enabled:
|
||||
/// * wake up on magic packet
|
||||
/// * wake up on pattern match, up to 20 patterns of 16 - 128 bytes
|
||||
/// ```
|
||||
///
|
||||
/// `* wake up on anything` (the nl80211 `any` trigger) counts too — that NIC wakes on every frame
|
||||
/// it receives, magic packets included. Enabled with only other triggers reads as NOT armed,
|
||||
/// which is the honest answer: a magic packet won't wake it. `None` when the output says nothing
|
||||
/// about WoWLAN at all. Split out from the command so it can be unit-tested on any platform.
|
||||
fn parse_iw_wowlan(text: &str) -> Option<bool> {
|
||||
let mut seen = false;
|
||||
let mut magic = false;
|
||||
for line in text.lines() {
|
||||
let t = line.trim();
|
||||
if let Some(state) = t.strip_prefix("WoWLAN is ") {
|
||||
seen = true;
|
||||
if state
|
||||
.trim()
|
||||
.trim_end_matches(':')
|
||||
.eq_ignore_ascii_case("disabled")
|
||||
{
|
||||
return Some(false);
|
||||
}
|
||||
} else if seen && t.starts_with('*') {
|
||||
let l = t.to_ascii_lowercase();
|
||||
if l.contains("magic packet") || l.contains("anything") {
|
||||
magic = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
seen.then_some(magic)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{parse_ethtool_wol, parse_iw_wowlan};
|
||||
|
||||
#[test]
|
||||
fn ethtool_current_setting_not_capability_line() {
|
||||
let armed =
|
||||
"Settings for enp5s0:\n\tSupports Wake-on: pumbg\n\tWake-on: g\n\tLink detected: yes\n";
|
||||
assert_eq!(parse_ethtool_wol(armed), Some(true));
|
||||
// "Supports Wake-on: ...g..." must NOT be read as the current setting.
|
||||
let off = "Settings for enp5s0:\n\tSupports Wake-on: pumbg\n\tWake-on: d\n";
|
||||
assert_eq!(parse_ethtool_wol(off), Some(false));
|
||||
assert_eq!(
|
||||
parse_ethtool_wol("Settings for lo:\n\tLink detected: yes\n"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iw_wowlan_states() {
|
||||
assert_eq!(parse_iw_wowlan("WoWLAN is disabled\n"), Some(false));
|
||||
assert_eq!(
|
||||
parse_iw_wowlan("WoWLAN is enabled:\n * wake up on magic packet\n"),
|
||||
Some(true)
|
||||
);
|
||||
// Enabled, but not for magic packets — a magic packet will not wake this NIC.
|
||||
assert_eq!(
|
||||
parse_iw_wowlan(
|
||||
"WoWLAN is enabled:\n * wake up on pattern match, up to 20 patterns of 16 - 128 bytes\n"
|
||||
),
|
||||
Some(false)
|
||||
);
|
||||
// The `any` trigger wakes on every received frame, magic packets included.
|
||||
assert_eq!(
|
||||
parse_iw_wowlan("WoWLAN is enabled:\n * wake up on anything (device continues operating normally)\n"),
|
||||
Some(true)
|
||||
);
|
||||
// Nothing to go on — the driver has no WoWLAN command.
|
||||
assert_eq!(parse_iw_wowlan(""), None);
|
||||
assert_eq!(
|
||||
parse_iw_wowlan("Wiphy phy0\n\tmax # scan SSIDs: 20\n"),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"requirements",
|
||||
"install",
|
||||
"ubuntu",
|
||||
"debian",
|
||||
"fedora",
|
||||
"arch",
|
||||
"bazzite",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -104,10 +104,11 @@ and capture/display glitches.
|
||||
Clients wake a saved host by themselves — auto-wake is on by default — but only once they have seen
|
||||
it awake, which is how they learn its MAC address, and only if the machine is armed to answer a magic
|
||||
packet. The arming is what's usually missing, and a **Linux** host tells you outright: search the web
|
||||
console's **Logs** page for `Wake-on-LAN`, and the line either confirms the card is armed or names
|
||||
the interface and the exact command to arm it. Windows and macOS hosts don't run that check, so go
|
||||
straight to the BIOS/UEFI and network-card steps in
|
||||
[Arming the machine](/docs/wake-on-lan#arming-the-machine).
|
||||
console's **Logs** page for `Wake-on-` — `Wake-on-LAN` for a wired card, `Wake-on-WLAN` for a Wi-Fi
|
||||
one — and the line either confirms the card is armed or names the interface and the exact command to
|
||||
arm it. A Wi-Fi card is armed by a different command than a wired one, and the log line gives the
|
||||
right one. Windows and macOS hosts don't run that check, so go straight to the BIOS/UEFI and
|
||||
network-card steps in [Arming the machine](/docs/wake-on-lan#arming-the-machine).
|
||||
|
||||
## Video is slow to start, or fails across subnets
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -30,14 +30,35 @@ That ordering is the whole prerequisite:
|
||||
> says so rather than pretending. On every client but the Linux one you can also type the MAC in by
|
||||
> hand; see the table below.
|
||||
|
||||
The packet goes to every local interface's subnet broadcast address *and* to `255.255.255.255`, on
|
||||
The packet goes **out of every one of the client's network interfaces** — from a socket bound to
|
||||
that interface's own address, aimed at both its subnet broadcast address and `255.255.255.255` — on
|
||||
UDP ports 9 and 7, repeated three times, plus a unicast to the host's last known address. That
|
||||
spread is deliberate: a sleeping machine has no ARP entry, so a plain unicast cannot find it.
|
||||
spread is deliberate: a sleeping machine has no ARP entry, so a plain unicast cannot find it, and a
|
||||
broadcast sent without binding an interface leaves by the default route only, which on a machine
|
||||
running a VPN or a mesh network is not the LAN the host sleeps on.
|
||||
|
||||
Neither the advert nor a magic packet is authenticated. That is fine here — a wrong address only
|
||||
makes the wake fail, and the host's certificate fingerprint still gates the actual connection. See
|
||||
[Security](/docs/security).
|
||||
|
||||
### Over Wi-Fi
|
||||
|
||||
A host on Wi-Fi wakes from the same packet. The mechanism is **WoWLAN** (Wake on Wireless LAN):
|
||||
the adapter stays associated to your access point while the machine sleeps, the access point holds
|
||||
broadcast frames for its sleeping stations and releases them on the next beacon, and the adapter
|
||||
wakes the machine when one of them is a magic packet. Punktfunk publishes a Wi-Fi card's address
|
||||
exactly like a wired one, so there is nothing different to do on the client — but the card has to be
|
||||
armed for it, which is a different switch from the wired one. See
|
||||
[Linux (Wi-Fi)](#linux-wi-fi) and [Windows](#windows) below.
|
||||
|
||||
Two things can still stop it, and neither is visible from Punktfunk:
|
||||
|
||||
- Some access points and mesh systems drop or rate-limit broadcast traffic to sleeping stations
|
||||
(often as "multicast enhancement", "broadcast filtering" or IGMP snooping). If wired hosts wake
|
||||
and a Wi-Fi one never does, that is the first thing to turn off.
|
||||
- Some laptops and adapters cut power to the Wi-Fi card in deeper sleep states, which drops the
|
||||
association and with it any chance of a wake.
|
||||
|
||||
## Waking from a client
|
||||
|
||||
**Auto-wake on connect** is a client setting, and it is **on by default**. You find it in Settings,
|
||||
@@ -135,7 +156,7 @@ whether a machine may be woken off the network is yours to make.
|
||||
### Check the host log first
|
||||
|
||||
This is the fastest diagnosis. On **Linux**, the host inspects the card carrying the address it
|
||||
advertises, each time it starts advertising, and writes one of two lines:
|
||||
advertises, each time it starts advertising, and writes one line about it. A wired card:
|
||||
|
||||
```text
|
||||
Wake-on-LAN armed (magic packet) on host NIC
|
||||
@@ -145,18 +166,29 @@ Wake-on-LAN armed (magic packet) on host NIC
|
||||
Wake-on-LAN is NOT armed on this host's NIC — clients cannot wake it from sleep.
|
||||
```
|
||||
|
||||
A Wi-Fi card, which is armed through an entirely different mechanism and is asked about separately
|
||||
(`iw phy … wowlan show`, not `ethtool`):
|
||||
|
||||
```text
|
||||
Wake-on-WLAN armed (magic packet) on host Wi-Fi NIC
|
||||
```
|
||||
|
||||
```text
|
||||
Wake-on-WLAN is NOT armed on this host's Wi-Fi NIC — clients cannot wake it from sleep.
|
||||
```
|
||||
|
||||
The warning line goes on to name the interface and the exact command to fix it. The host only
|
||||
reports; it never changes the card's settings. It stays silent when it cannot tell — `ethtool`
|
||||
missing, or not enough privilege — rather than guessing, and it says nothing at all when mDNS
|
||||
adverts are switched off (`PUNKTFUNK_MDNS=0` or `--no-mdns`), because then no address is published
|
||||
either.
|
||||
reports; it never changes the card's settings. It stays silent when it cannot tell — `iw` or
|
||||
`ethtool` missing, a driver that doesn't answer, or not enough privilege — rather than guessing, and
|
||||
it says nothing at all when mDNS adverts are switched off (`PUNKTFUNK_MDNS=0` or `--no-mdns`),
|
||||
because then no address is published either.
|
||||
|
||||
Read the line on the web console's **Logs** page, or in the journal with
|
||||
`journalctl --user -u punktfunk-host`. See [Troubleshooting](/docs/troubleshooting#still-stuck).
|
||||
|
||||
**Windows and macOS hosts do not run this check**, so there is no log line to look for there.
|
||||
|
||||
### Linux
|
||||
### Linux (wired)
|
||||
|
||||
Ask the card what it is doing. `Supports Wake-on:` is the capability; `Wake-on:` is the current
|
||||
setting. `g` means magic packet, `d` means disabled.
|
||||
@@ -174,6 +206,42 @@ sudo ethtool -s enp5s0 wol g
|
||||
On many systems that does not survive a reboot. Re-run `ethtool enp5s0` after the next boot to check,
|
||||
and make it permanent through your distribution's network configuration if it reset.
|
||||
|
||||
### Linux (Wi-Fi)
|
||||
|
||||
`ethtool` is the wrong tool here — most wireless drivers report `Wake-on: d` whether or not they are
|
||||
armed, because the trigger lives in the wireless stack instead. Ask `iw`, using the *phy* behind the
|
||||
interface (`/sys/class/net/wlan0/phy80211/name`, usually `phy0`):
|
||||
|
||||
```bash
|
||||
iw phy phy0 wowlan show
|
||||
```
|
||||
|
||||
`WoWLAN is disabled` means no wake. Armed looks like this, and the `* wake up on magic packet` line
|
||||
is the one that matters:
|
||||
|
||||
```text
|
||||
WoWLAN is enabled:
|
||||
* wake up on magic packet
|
||||
```
|
||||
|
||||
Arm it:
|
||||
|
||||
```bash
|
||||
sudo iw phy phy0 wowlan enable magic-packet
|
||||
```
|
||||
|
||||
That setting is per-phy and NetworkManager re-applies its own on every connection, so on a
|
||||
NetworkManager system make it stick on the connection instead — this survives reboots and
|
||||
reconnects:
|
||||
|
||||
```bash
|
||||
sudo nmcli connection modify <connection> 802-11-wireless.wake-on-wlan magic
|
||||
```
|
||||
|
||||
`iw phy phy0 wowlan show` reporting `command failed: Operation not supported` means the driver has no
|
||||
WoWLAN support at all; that adapter cannot be woken over Wi-Fi. Check `iw list | grep -A5 "WoWLAN"`
|
||||
for what the hardware claims to support.
|
||||
|
||||
### Windows
|
||||
|
||||
Open **Device Manager**, find the network adapter under **Network adapters**, and open its
|
||||
@@ -181,10 +249,17 @@ properties. On the **Power Management** tab, allow the device to wake the comput
|
||||
**Advanced** tab, enable the adapter's magic-packet wake property if it has one. Exact wording
|
||||
depends on the driver.
|
||||
|
||||
Wi-Fi adapters use the same two tabs. The **Advanced** property is often called **Wake on Magic
|
||||
Packet** there too, sometimes **Wake on Wireless LAN**; many Wi-Fi drivers expose neither, and those
|
||||
cannot be woken over Wi-Fi. `powercfg /devicequery wake_armed` lists every device currently allowed
|
||||
to wake the machine — if the adapter is not in it, nothing on the network can wake this host.
|
||||
|
||||
## Limits
|
||||
|
||||
- **Wired Ethernet is what works.** Waking over Wi-Fi is unreliable and depends entirely on the
|
||||
adapter and the platform.
|
||||
- **Wired Ethernet is the sure thing; Wi-Fi works when the adapter supports WoWLAN.** Punktfunk
|
||||
sends the same packet either way and publishes a Wi-Fi card's address like any other, but whether
|
||||
a sleeping adapter is still listening is the adapter's and the access point's decision —
|
||||
see [Over Wi-Fi](#over-wi-fi).
|
||||
- **Connect once while the host is awake**, on the same local network, before you rely on waking it.
|
||||
A host you only ever added by address, on a network where mDNS never reached it, has no learned
|
||||
address — the CLI will tell you so, and the apps will not offer the wake action. Typing the MAC in
|
||||
|
||||
@@ -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 \
|
||||
|
||||
Executable
+132
@@ -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"
|
||||
Reference in New Issue
Block a user