#!/bin/sh # SPDX-License-Identifier: GPL-3.0-or-later # hypr-live-update-guard - abort a live GPU/compositor library upgrade. # # Installed as a pacman PreTransaction hook. When an upgrade transaction # would CHANGE the on-disk version of GPU/compositor runtime libraries # (mesa, hyprland, wayland, GPU drivers, ...) AND a Hyprland session is # running, this aborts the transaction BEFORE any package is swapped. # Replacing those libraries out from under a live compositor makes the next # GPU-lib call hit a now "(deleted)" file and SIGABRT, taking the Wayland # clients down with it (hit on ratio 2026-06-07: mesa + hyprland upgraded # live, Hyprland crashed and took awww/insync/emacs with it). Aborting at # PreTransaction is the safe point: nothing has been replaced yet, so the # running session is untouched and the user can re-run from a TTY. # # Version-aware (2026-07-08): a same-version reinstall writes identical # bytes over identical bytes and is harmless to the live session — the # maintenance console's integrity REINSTALL is exactly that after an # update, and the name-only guard was blocking it. Each target's installed # version (pacman -Q) is compared against the sync-db candidate # (expac -S %v); targets that match are pure reinstalls and pass. Both are # read-only queries and run fine inside a hook — the ALPM lock only gates # other transactions (pinned live with db.lck present). A target whose # versions can't be resolved (AUR package, a -U transaction installing a # local file, expac absent) reads as unknown and blocks conservatively — # a -U of a guard-listed package at repo-matching version would slip # through, which is accepted as pathological. # # Pacman feeds the matched package names on stdin (NeedsTargets). # # Test seams / overrides (env): # HYPR_GUARD_RUNNING 1/0 forces the running check (default: pgrep Hyprland) # HYPR_ALLOW_LIVE_UPDATE 1 proceeds anyway (skip the guard) # HYPR_GUARD_SENTINEL path whose existence also proceeds anyway # (default /run/archsetup-allow-live-gpu-update, # cleared on reboot since /run is tmpfs) # HYPR_GUARD_VERSIONS "pkg installed candidate" lines replacing the # pacman/expac lookups; when set, a package absent # from the map reads as unknown (blocks) set -u sentinel="${HYPR_GUARD_SENTINEL:-/run/archsetup-allow-live-gpu-update}" # Explicit override: the user knows what they're doing. if [ "${HYPR_ALLOW_LIVE_UPDATE:-0}" = "1" ] || [ -e "$sentinel" ]; then exit 0 fi hyprland_running() { if [ -n "${HYPR_GUARD_RUNNING:-}" ]; then [ "$HYPR_GUARD_RUNNING" = "1" ] return fi pgrep -x Hyprland >/dev/null 2>&1 } # No live session means no live swap to worry about. Let the upgrade run -- # this is exactly the from-a-TTY-after-logout path the warning points to. hyprland_running || exit 0 # Collect the triggering packages (stdin from NeedsTargets). targets=$(cat 2>/dev/null | sort -u) # "installed candidate" for a package, or nothing when unknown. versions_for() { if [ -n "${HYPR_GUARD_VERSIONS+x}" ]; then printf '%s\n' "$HYPR_GUARD_VERSIONS" \ | awk -v p="$1" '$1 == p { print $2, $3; exit }' else inst=$(pacman -Q -- "$1" 2>/dev/null | awk '{ print $2 }') # expac prints one line per repo carrying the package; the first is # the repo pacman resolves from cand=$(expac -S '%v' -- "$1" 2>/dev/null | head -n 1) printf '%s %s\n' "$inst" "$cand" fi } # Split the targets into version-changing (dangerous live) and same-version # reinstalls (harmless). Unknown versions count as dangerous. changed="" count=0 for pkg in $targets; do count=$((count + 1)) # shellcheck disable=SC2046 # splitting "inst cand" into $1 $2 is the point set -- $(versions_for "$pkg") inst=${1:-} cand=${2:-} if [ -n "$inst" ] && [ -n "$cand" ] && [ "$inst" = "$cand" ]; then continue # pure reinstall: identical bytes, no swap hazard fi changed="$changed $pkg" done # Every guarded target is a pure reinstall -- nothing changes on disk that # the live session has mapped. Let it through silently. if [ "$count" -gt 0 ] && [ -z "$changed" ]; then exit 0 fi # An empty target list shouldn't normally happen (the hook only fires when # dangerous targets exist); if Hyprland is up, stay safe and abort. pkg_lines="" for pkg in $changed; do pkg_lines="${pkg_lines} - ${pkg} " done [ -n "$pkg_lines" ] || pkg_lines=" (GPU/compositor runtime libraries) " cat >&2 <