Files
punktfunk/packaging/gamescope/build-punktfunk-gamescope.sh
enricobuehler 346385bad8 fix(deb): ship punktfunk-gamescope on apt at last, and support Debian 13
`punktfunk-gamescope` had never been published to the apt registry — not in any
release. It was built inside the host job's Ubuntu 24.04 image, where it cannot
build: our pin vendors wlroots 0.19.3, which floors `wayland-server` at 1.23.1,
and noble ships 1.22.0 (it also lacks libxcb-errors-dev and has only
libdisplay-info 0.1.1). Every rung of that path was a `::warning::` returning 0
and the one hard gate ran last by design, so v0.26.0 and v0.27.0 both released
with the package missing while docs-site told apt users to install it. The same
tags shipped it fine for Arch, Fedora 44 and Bazzite.

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

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

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

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

⚠ Bootstrap: seed `punktfunk-gamescope-trixie:latest` into the LAN registry once
(docker.yml builds it thereafter) or the new job cannot start.
2026-08-13 11:43:54 +02:00

161 lines
8.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# Build gamescope + punktfunk's `pipewire-hdr` patches and install it as `punktfunk-gamescope`.
#
# The ONE build recipe every packaging path calls (Arch PKGBUILD, the Bazzite sysext image, an
# operator building by hand); the nix derivation expresses the same thing declaratively. It never
# touches the distro's own `gamescope` — the binary lands under a different name, and the host's
# resolution order (PUNKTFUNK_GAMESCOPE_BIN > punktfunk-gamescope > gamescope) picks it up.
#
# Usage:
# bash build-punktfunk-gamescope.sh [--rev <git-rev>] [--prefix /usr] [--destdir DIR]
# [--srcdir DIR] [--jobs N] [--no-setcap]
#
# --rev gamescope commit/tag to build (default: the pin below)
# --prefix install prefix (default /usr)
# --destdir staging root for packaging (default: install straight into --prefix)
# --srcdir reuse an existing gamescope checkout instead of cloning
# --no-setcap skip CAP_SYS_NICE (always skipped when --destdir is set — a package sets it)
#
# Needs: git, meson >= 0.58, ninja, a C++20 compiler, and gamescope's own build deps. Those vary
# by distro; the packaging files list them (packaging/gamescope/PKGBUILD for Arch). gamescope
# vendors wlroots / libliftoff / vkroots / libdisplay-info / SPIRV-Headers / reshade as git
# SUBMODULES (plus two meson wraps for glm + stb), so both the clone and the build want network
# access unless the checkout already carries them — hence `--recurse-submodules` below.
set -euo pipefail
# The pinned upstream. Bump together with the patches (they are `git am`-able and rebase cheaply —
# two files, mirroring code that already exists in-tree; see README.md).
GAMESCOPE_REV="5fb8dce4a09d0a68d097b9faf9513782106bc843"
GAMESCOPE_REPO="https://github.com/ValveSoftware/gamescope.git"
REV="$GAMESCOPE_REV" PREFIX=/usr DESTDIR="" SRCDIR="" JOBS="" SETCAP=1 EXTRA_FALLBACK=""
while [ $# -gt 0 ]; do
case "$1" in
--rev) REV="${2:?}"; shift 2 ;;
--prefix) PREFIX="${2:?}"; shift 2 ;;
--destdir) DESTDIR="${2:?}"; shift 2 ;;
--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
# A staged install is a package build: the package manager owns file capabilities.
[ -n "$DESTDIR" ] && SETCAP=0
for tool in git meson ninja; do
command -v "$tool" >/dev/null || { echo "missing tool: $tool" >&2; exit 1; }
done
HERE="$(cd "$(dirname "$0")" && pwd)"
PATCHES=("$HERE"/patches/*.patch)
[ -e "${PATCHES[0]}" ] || { echo "no patches found in $HERE/patches" >&2; exit 1; }
WORK=""
if [ -z "$SRCDIR" ]; then
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
SRCDIR="$WORK/gamescope"
echo "==> cloning gamescope @ ${REV:0:12}"
git clone --recurse-submodules "$GAMESCOPE_REPO" "$SRCDIR"
git -C "$SRCDIR" checkout --recurse-submodules "$REV"
fi
# `git am` needs an identity and a clean tree; both are ours to provide in a throwaway checkout.
# Idempotent: a checkout that already carries the marker is left alone, so a re-run of this script
# against --srcdir does not fail on an already-applied patch.
if grep -q '+pfhdr' "$SRCDIR/src/meson.build"; then
echo "==> patches already applied in $SRCDIR"
else
echo "==> applying punktfunk patches"
git -C "$SRCDIR" \
-c user.name=punktfunk -c user.email=packages@unom.io \
am "${PATCHES[@]}"
fi
BUILD="$SRCDIR/build-punktfunk"
echo "==> configuring"
# We ship ONE binary out of this tree, so build only what leads to it:
# -Denable_tests=false gamescope's own unit tests want Catch2 **v3**
# (`catch2-with-main`); Fedora ships v2, and building someone else's
# test suite is not our job either way.
# -Denable_openvr_support the VR integration pulls the openvr submodule + its build for a
# code path a headless capture session never enters.
# -Denable_gamescope_wsi_layer the WSI layer is a SEPARATE artifact the distro's gamescope
# package already installs; ours must not collide with it.
# (The layer the nested games load is that one — it is version-
# independent of the compositor binary.)
#
# `force_fallback_for` includes **wlroots** on purpose, and it is load-bearing for a binary we
# SHIP: gamescope vendors a wlroots submodule, but meson prefers a system one when the build host
# happens to have `wlroots-devel` — and then links it SHARED. The result starts fine on the build
# host and dies with `libwlroots-0.19.so: cannot open shared object file` anywhere else, which is
# every machine that installs our package. Fedora 43 has no wlroots-devel so it fell back and
# linked static by luck; Fedora 44's `dnf builddep gamescope` pulls one in, and the binary built
# there would not start on the host. Pinning the fallback makes the outcome the same everywhere.
# (gamescope's own meson.build hard-errors if libliftoff/vkroots are missing from this list, so
# all three go together.)
#
# The C++ runtime goes STATIC for the same reason wlroots does: this binary is built on a ROLLING
# distro and has to start on a FROZEN one. Arch's gcc (16.1.1 when this was written) makes the
# compositor require `GLIBCXX_3.4.35`, and SteamOS 3.8.16 ships libstdc++ 3.4.34 — so the published
# Arch package died on the very platform the gamescope backend matters most on ("version
# GLIBCXX_3.4.35 not found"), while every other soname resolved and glibc was never close to the
# limit (the binary asks for 2.38 at most; SteamOS has 2.41). Safe because
# gamescope links NO shared C++ library (its `NEEDED` list is all C — glslang/SPIRV are build-time
# only), so no C++ ABI ever crosses a shared boundary; it costs ~1 MB.
# Appended to LDFLAGS rather than passed as `-Dcpp_link_args`, because that option would REPLACE
# the value meson derives from the environment and silently drop makepkg's hardening flags
# (`-z relro`, `-z now`, `--as-needed`).
export LDFLAGS="${LDFLAGS:-} -static-libstdc++ -static-libgcc"
meson setup "$BUILD" "$SRCDIR" \
--prefix="$PREFIX" \
--buildtype=release \
-Dforce_fallback_for="libliftoff,vkroots,wlroots${EXTRA_FALLBACK:+,$EXTRA_FALLBACK}" \
-Dpipewire=enabled \
-Denable_tests=false \
-Denable_openvr_support=false \
-Denable_gamescope_wsi_layer=false
echo "==> building"
ninja -C "$BUILD" ${JOBS:+-j "$JOBS"}
# Install ONLY the compositor, under our own name. gamescope's `ninja install` would also drop
# gamescopectl/gamescopereaper/gamescopestream + the WSI layer into the prefix, colliding with the
# distro's gamescope package — and we need none of them: the host only ever execs the compositor.
BIN="$BUILD/src/gamescope"
[ -x "$BIN" ] || { echo "build produced no $BIN" >&2; exit 1; }
# The static C++ runtime above is invisible in a successful build and only shows up as a binary
# that will not start on an older distro — so assert it here, where a mistake is a build failure
# instead of a package that dies at `--version` on SteamOS. No `libstdc++.so.6` in NEEDED is the
# whole invariant (and it needs no version threshold to check).
if command -v objdump >/dev/null; then
objdump -p "$BIN" 2>/dev/null | grep -q 'NEEDED.*libstdc++' && {
echo "built binary links libstdc++ dynamically — the static C++ runtime did not take, and this" >&2
echo "package would not start on a distro older than the build host" >&2
exit 1
}
fi
DEST="${DESTDIR}${PREFIX}/bin/punktfunk-gamescope"
echo "==> installing $DEST"
install -Dm755 "$BIN" "$DEST"
if [ "$SETCAP" = 1 ] && command -v setcap >/dev/null; then
# gamescope raises its own scheduling priority; without CAP_SYS_NICE it still runs, just noisier
# and with worse frame pacing. Best-effort — needs root, and a package sets it declaratively.
setcap 'CAP_SYS_NICE=eip' "$DEST" 2>/dev/null \
|| echo "note: could not setcap CAP_SYS_NICE on $DEST (run as root, or let the package do it)"
fi
echo "==> done: $("$DEST" --version 2>&1 | head -1)"
echo " the banner above must contain +pfhdr — that marker is how the host detects HDR support"