feat(packaging/bazzite): systemd-sysext replaces rpm-ostree layering as the primary install path
Layering is a last resort per the Bazzite docs (slows every OS update, can block upgrades until removed); a sysext never enters an rpm-ostree transaction, survives OS updates, and installs/updates with no reboot — the mechanism Fedora Atomic ships via fedora-sysexts. - build-sysext.sh wraps the built host+web RPMs into punktfunk-<V-R>-x86-64.raw: /etc payload relocated to /usr/share/punktfunk/etc (a sysext carries only /usr), the punktfunk-sysext helper embedded, ID=fedora + VERSION_ID pinned (merges on Bazzite via ID_LIKE; REFUSED after a major rebase instead of running soname-broken binaries — both behaviors validated live on Bazzite 43). SELinux labels are baked in as squashfs pseudo-xattrs from matchpathcon: unlabeled files run fine for user units but system daemons are DENIED (udev couldn't read the gamepad rule under enforcing) — validated on-glass. Refuses duplicate input package names (a stale noarch punktfunk-web next to the x86_64 one built a chimera image with the dead node launcher once). - punktfunk-sysext.sh: install/update/status/remove against per-Fedora-major feeds (…/generic/punktfunk-sysext/f43[-canary]), SHA-256-verified, applies the udev/sysctl scriptlet work + /etc copies, prints the layering-migration hint. Live-validated on the .41 Bazzite box incl. service restart + web console. - publish-sysext-feed.sh + rpm.yml: build + publish the image per matrix leg (fedver 43/44), canary feeds pruned to 6, stable release assets attached. - update-punktfunk.sh warns when the sysext shadows a layered install. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env bash
|
||||
# punktfunk-sysext — install/update the punktfunk host on Bazzite / Fedora Atomic as a
|
||||
# systemd-sysext, the no-layering path (rpm-ostree layering is a last resort per the Bazzite
|
||||
# docs: it slows every update and can block upgrades; a sysext never enters an rpm-ostree
|
||||
# transaction, needs no reboot, and is trivially removable).
|
||||
#
|
||||
# The image overlays /usr from /var/lib/extensions/punktfunk.raw with the host, tray and web
|
||||
# console + their udev/sysctl/systemd-user payload; the RPMs' two /etc files (gamescope
|
||||
# session drop-in, tray autostart) ride inside at /usr/share/punktfunk/etc/ and are copied
|
||||
# into the real /etc here (a sysext can only carry /usr).
|
||||
#
|
||||
# Bootstrap (the script also ships inside the image as /usr/bin/punktfunk-sysext):
|
||||
# curl -fsSLO https://git.unom.io/unom/punktfunk/raw/branch/main/packaging/bazzite/punktfunk-sysext.sh
|
||||
# sudo bash punktfunk-sysext.sh install # or: install --channel canary
|
||||
# Thereafter:
|
||||
# sudo punktfunk-sysext update | status | remove
|
||||
#
|
||||
# Feed: the Gitea generic package registry, one feed per Fedora major x channel
|
||||
# (…/punktfunk-sysext/f43/, f43-canary, f44, …), each a SHA256SUMS + versioned .raw files —
|
||||
# published by .gitea/workflows/rpm.yml from the same RPMs the (legacy) layering path uses.
|
||||
# The image pins ID=fedora + VERSION_ID, so after a major OS rebase the old image is refused
|
||||
# (not merged broken) and `punktfunk-sysext update` re-resolves against the new release.
|
||||
set -euo pipefail
|
||||
|
||||
REGISTRY="${PUNKTFUNK_SYSEXT_REGISTRY:-https://git.unom.io/api/packages/unom/generic/punktfunk-sysext}"
|
||||
CONF=/etc/punktfunk-sysext.conf
|
||||
EXT_DIR=/var/lib/extensions
|
||||
IMG="$EXT_DIR/punktfunk.raw"
|
||||
SIDECAR="$EXT_DIR/.punktfunk.version"
|
||||
MARKER=/usr/lib/extension-release.d/extension-release.punktfunk
|
||||
ETC_SRC=/usr/share/punktfunk/etc
|
||||
|
||||
usage() {
|
||||
sed -n 's/^#\( \|$\)//p' "$0" | sed -n '1,20p'
|
||||
echo "usage: punktfunk-sysext install [--channel stable|canary] [--from-file X.raw]"
|
||||
echo " punktfunk-sysext update [--from-file X.raw] | status | remove"
|
||||
exit "${1:-0}"
|
||||
}
|
||||
need_root() { [ "$(id -u)" = 0 ] || { echo "run as root (sudo)" >&2; exit 1; }; }
|
||||
|
||||
os_version_id() { . /etc/os-release; echo "${VERSION_ID%%.*}"; }
|
||||
channel() { # shellcheck disable=SC1090
|
||||
[ -f "$CONF" ] && . "$CONF"; echo "${CHANNEL:-stable}"; }
|
||||
feed_url() {
|
||||
local suffix=""
|
||||
[ "$(channel)" = canary ] && suffix="-canary"
|
||||
echo "$REGISTRY/f$(os_version_id)$suffix"
|
||||
}
|
||||
|
||||
# latest -> "VERSION FILENAME SHA256" from the feed's SHA256SUMS (highest by version sort).
|
||||
latest() {
|
||||
local feed; feed="$(feed_url)"
|
||||
curl -fsSL "$feed/SHA256SUMS" \
|
||||
| awk '$2 ~ /^punktfunk-.*-x86-64\.raw$/ { v=$2; sub(/^punktfunk-/,"",v); sub(/-x86-64\.raw$/,"",v); print v, $2, $1 }' \
|
||||
| sort -V | tail -n1
|
||||
}
|
||||
|
||||
installed_version() {
|
||||
if [ -f "$MARKER" ]; then
|
||||
sed -n 's/^SYSEXT_VERSION_ID=//p' "$MARKER"
|
||||
elif [ -f "$SIDECAR" ]; then
|
||||
cat "$SIDECAR"
|
||||
fi
|
||||
}
|
||||
merged() { [ -f "$MARKER" ]; }
|
||||
|
||||
post_merge() {
|
||||
if ! merged; then
|
||||
echo "!! image installed but NOT merged — 'systemd-sysext status' / 'journalctl -u systemd-sysext'" >&2
|
||||
echo "!! (an OS release the image doesn't match? 'punktfunk-sysext update' fetches the right one)" >&2
|
||||
return 1
|
||||
fi
|
||||
# What the RPM scriptlets would have done: pick up the uinput/uhid rule + the UDP buffer
|
||||
# sysctl now, no reboot (both also auto-apply at boot once merged — the files live in /usr/lib).
|
||||
udevadm control --reload 2>/dev/null || :
|
||||
udevadm trigger --subsystem-match=misc 2>/dev/null || :
|
||||
for f in /usr/lib/sysctl.d/99-punktfunk-net.conf /usr/lib/sysctl.d/99-punktfunk-client-net.conf; do
|
||||
[ -f "$f" ] && sysctl -q -p "$f" 2>/dev/null || :
|
||||
done
|
||||
# The /etc payload a sysext can't carry. The gamescope-session drop-in is %config(noreplace):
|
||||
# only seed it, never clobber a local edit. The tray autostart entry is not user config.
|
||||
if [ -f "$ETC_SRC/gamescope-session-plus/sessions.d/steam" ] \
|
||||
&& [ ! -e /etc/gamescope-session-plus/sessions.d/steam ]; then
|
||||
install -Dm0644 "$ETC_SRC/gamescope-session-plus/sessions.d/steam" \
|
||||
/etc/gamescope-session-plus/sessions.d/steam
|
||||
fi
|
||||
if [ -f "$ETC_SRC/xdg/autostart/io.unom.Punktfunk.Tray.desktop" ]; then
|
||||
install -Dm0644 "$ETC_SRC/xdg/autostart/io.unom.Punktfunk.Tray.desktop" \
|
||||
/etc/xdg/autostart/io.unom.Punktfunk.Tray.desktop
|
||||
fi
|
||||
}
|
||||
|
||||
# do_install VERSION FILENAME SHA256 | do_install --from-file X.raw
|
||||
do_install() {
|
||||
need_root
|
||||
mkdir -p "$EXT_DIR"
|
||||
local tmp="$EXT_DIR/.punktfunk.raw.new" ver
|
||||
if [ "$1" = --from-file ]; then
|
||||
ver="(local: $(basename "$2"))"
|
||||
cp -f "$2" "$tmp"
|
||||
else
|
||||
ver="$1"
|
||||
echo "downloading punktfunk $ver ($(channel), fedora $(os_version_id))…"
|
||||
curl -fL --progress-bar -o "$tmp" "$(feed_url)/$2"
|
||||
echo "$3 $tmp" | sha256sum -c --quiet
|
||||
fi
|
||||
mv -f "$tmp" "$IMG" # marker inside is extension-release.punktfunk — name must match
|
||||
echo "$ver" > "$SIDECAR"
|
||||
systemctl enable --now systemd-sysext.service >/dev/null 2>&1 || :
|
||||
systemd-sysext refresh
|
||||
post_merge
|
||||
echo "punktfunk $ver merged into /usr."
|
||||
}
|
||||
|
||||
layering_hint() {
|
||||
if command -v rpm-ostree >/dev/null 2>&1 \
|
||||
&& rpm-ostree status 2>/dev/null | grep -q 'LayeredPackages:.*punktfunk'; then
|
||||
cat >&2 <<'EOF'
|
||||
!! punktfunk is ALSO layered via rpm-ostree. The sysext now shadows it, but remove the
|
||||
!! layer so it stops slowing/blocking OS updates (the reason this sysext exists):
|
||||
!! sudo rpm-ostree uninstall punktfunk punktfunk-web && systemctl reboot
|
||||
EOF
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_install() {
|
||||
need_root
|
||||
local from_file=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--channel) printf 'CHANNEL=%s\n' "${2:?}" > "$CONF"; shift 2 ;;
|
||||
--from-file) from_file="${2:?}"; shift 2 ;;
|
||||
*) usage 1 ;;
|
||||
esac
|
||||
done
|
||||
if [ -n "$from_file" ]; then
|
||||
do_install --from-file "$from_file"
|
||||
else
|
||||
local l; l="$(latest)"
|
||||
[ -n "$l" ] || { echo "no image in the feed $(feed_url)" >&2; exit 1; }
|
||||
# shellcheck disable=SC2086
|
||||
do_install $l
|
||||
fi
|
||||
layering_hint
|
||||
cat <<'EOF'
|
||||
|
||||
First-run (once):
|
||||
ujust add-user-to-input-group # virtual gamepads; then log out + back in
|
||||
mkdir -p ~/.config/punktfunk
|
||||
cp /usr/share/punktfunk/host.env.bazzite ~/.config/punktfunk/host.env
|
||||
systemctl --user daemon-reload && systemctl --user enable --now punktfunk-host
|
||||
Updates: sudo punktfunk-sysext update
|
||||
EOF
|
||||
}
|
||||
|
||||
cmd_update() {
|
||||
need_root
|
||||
if [ "${1:-}" = --from-file ]; then do_install --from-file "${2:?}"; return; fi
|
||||
local cur l ver
|
||||
cur="$(installed_version)"
|
||||
l="$(latest)"
|
||||
[ -n "$l" ] || { echo "no image in the feed $(feed_url)" >&2; exit 1; }
|
||||
ver="${l%% *}"
|
||||
if [ "$ver" = "$cur" ] && merged; then
|
||||
echo "already on $cur (channel $(channel)) — nothing to do."
|
||||
return
|
||||
fi
|
||||
echo "updating: ${cur:-<none>} -> $ver"
|
||||
# shellcheck disable=SC2086
|
||||
do_install $l
|
||||
echo "restart the host to pick up the new binary: systemctl --user restart punktfunk-host"
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
echo "channel: $(channel)"
|
||||
echo "feed: $(feed_url)"
|
||||
echo "image: $([ -f "$IMG" ] && du -h "$IMG" | cut -f1 || echo '(not installed)')"
|
||||
echo "merged: $(merged && echo yes || echo no)"
|
||||
echo "installed: $(installed_version || true)"
|
||||
echo "latest: $(latest 2>/dev/null | cut -d' ' -f1 || true)"
|
||||
}
|
||||
|
||||
cmd_remove() {
|
||||
need_root
|
||||
# /etc cleanup needs the /usr payload for the unmodified-compare — do it BEFORE unmerging.
|
||||
if merged; then
|
||||
if cmp -s "$ETC_SRC/gamescope-session-plus/sessions.d/steam" \
|
||||
/etc/gamescope-session-plus/sessions.d/steam 2>/dev/null; then
|
||||
rm -f /etc/gamescope-session-plus/sessions.d/steam
|
||||
fi
|
||||
fi
|
||||
rm -f /etc/xdg/autostart/io.unom.Punktfunk.Tray.desktop
|
||||
rm -f "$IMG" "$SIDECAR" "$CONF"
|
||||
systemd-sysext refresh 2>/dev/null || :
|
||||
echo "punktfunk sysext removed (user config in ~/.config/punktfunk is untouched)."
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
install) shift; cmd_install "$@" ;;
|
||||
update) shift; cmd_update "$@" ;;
|
||||
status) shift; cmd_status ;;
|
||||
remove) shift; cmd_remove ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
Reference in New Issue
Block a user