1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
|
#!/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 <<EOF
==========================================================================
BLOCKED: live GPU/compositor library upgrade while Hyprland is running
==========================================================================
Packages in this upgrade change version and can crash the running
compositor if swapped now:
${pkg_lines}
Replacing these out from under a live Hyprland session makes the next
GPU-lib call hit a deleted library and SIGABRT, taking your Wayland apps
down with it (and risking an unclean shutdown).
Do it safely instead -- from a TTY with Hyprland stopped:
1. Log out of Hyprland, or switch to a console (Ctrl+Alt+F2) and log in.
2. Re-run the upgrade there: sudo pacman -Syu
To override and proceed anyway (not recommended while Hyprland runs):
sudo touch $sentinel && sudo pacman -Syu
==========================================================================
EOF
exit 1
|