Compare commits

...
2 Commits
Author SHA1 Message Date
Felitendo 9fd2458d20 Hold back blocking packages instead of failing the whole upgrade
A repo package that replaces something an installed AUR package still depends
on aborted the entire transaction, and would have done so on every subsequent
run - one stale AUR package was enough to cut a machine off from all updates
indefinitely. Observed in the wild: percona-server-clients replaces
libperconaserverclient without providing it, while heidisql-qt6-bin hard-depends
on it, blocking 213 unrelated package updates.

pacman names the offending package in its dependency errors, so it is now
extracted and passed to --ignore for one retry: the other 213 packages go
through and the blocker is reported. The hold is per-run, never written to
IgnorePkg, so it disappears by itself once upstream catches up.

The single retry is also now a bounded recovery loop, because fixing one
problem regularly uncovers the next - a conflict resolved with --ask=20 can
surface a dependency error behind it. Each remedy is applied at most once.

The held-back set is shown in the menu and notified only when it changes, so a
blocker waiting on an upstream fix does not produce the same message daily.
2026-08-08 15:12:43 +02:00
Felitendo f593cc1933 Menu: flip the switches without asking for a keypress
Toggling automatic updates or notifications returned to a 'press any key'
prompt for no reason - the status block at the top of the menu already shows
the new state. cau_bad and cau_note now flag that they printed something, so
the acknowledgement only appears when there is genuinely something to read
(a failed sudoers check, a missing AUR helper) instead of after every toggle.
2026-08-08 15:05:38 +02:00
7 changed files with 128 additions and 23 deletions

No files matched your search

+1 -1
View File
@@ -8,7 +8,7 @@
# Overridable so a packager can pass the version it is actually building
# (`make VERSION=$pkgver`). The literal below is the fallback for builds
# straight from a checkout, and is what a release tag has to carry.
VERSION ?= 1.0.3
VERSION ?= 1.0.5
PREFIX ?= /usr
DESTDIR ?=
+11
View File
@@ -243,3 +243,14 @@ msgstr ""
msgid "pacman's lock file looks left over from an interrupted update. Updates are paused until it is cleared."
msgstr ""
msgid "Some packages were held back"
msgstr ""
#, c-format
msgid "%s could not be updated and was skipped. Everything else is up to date."
msgstr ""
#, c-format
msgid "Held back: %s"
msgstr ""
+11
View File
@@ -244,3 +244,14 @@ msgstr "Paketdatenbank gesperrt"
msgid "pacman's lock file looks left over from an interrupted update. Updates are paused until it is cleared."
msgstr "Die Sperrdatei von pacman scheint von einem abgebrochenen Update übrig zu sein. Bis sie entfernt ist, pausieren die Updates."
msgid "Some packages were held back"
msgstr "Einige Pakete wurden zurückgehalten"
#, c-format
msgid "%s could not be updated and was skipped. Everything else is up to date."
msgstr "%s konnte nicht aktualisiert werden und wurde übersprungen. Alles andere ist aktuell."
#, c-format
msgid "Held back: %s"
msgstr "Zurückgehalten: %s"
+16
View File
@@ -202,6 +202,22 @@ else
fi
fi
# Packages skipped so the rest of the upgrade could proceed. The notification
# only fires when the set changes: a blocker waiting on an upstream fix would
# otherwise produce the same message every single day.
prev_held="$(cau_state_read held_back '')"
if [[ -n $CAU_PACMAN_HELD ]]; then
cau_state_write held_back "$CAU_PACMAN_HELD"
cau_warn "Held back: $CAU_PACMAN_HELD"
if [[ $CAU_PACMAN_HELD != "$prev_held" && $CFG_NOTIFY_ERROR == yes ]]; then
cau_notify normal "Some packages were held back" \
"%s could not be updated and was skipped. Everything else is up to date." \
"$CAU_PACMAN_HELD"
fi
else
cau_state_clear held_back
fi
if cau_pacman_reboot_needed; then
cau_state_write reboot_needed 1
cau_info "A kernel update needs a restart"
+9 -2
View File
@@ -146,11 +146,18 @@ cau_run_logged() {
# Terminal helpers for the CLI
# ---------------------------------------------------------------------------
# Set by cau_bad and cau_note. The menu redraws immediately after an action,
# which would wipe the screen; this marks that something was printed that the
# user still has to read, so only those cases wait for a keypress. A plain
# success needs no acknowledgement - the status block at the top of the menu
# already shows the new state.
CAU_UI_NEEDS_ACK=''
cau_say() { printf '%s\n' "$*"; }
cau_head() { printf '\n%s%s%s\n\n' "$CAU_C_BOLD$CAU_C_BLUE" "$*" "$CAU_C_RESET"; }
cau_ok() { printf '%s✔%s %s\n' "$CAU_C_GREEN" "$CAU_C_RESET" "$*"; }
cau_bad() { printf '%s✘%s %s\n' "$CAU_C_RED" "$CAU_C_RESET" "$*" >&2; }
cau_note() { printf '%s•%s %s\n' "$CAU_C_DIM" "$CAU_C_RESET" "$*"; }
cau_bad() { CAU_UI_NEEDS_ACK=1; printf '%s✘%s %s\n' "$CAU_C_RED" "$CAU_C_RESET" "$*" >&2; }
cau_note() { CAU_UI_NEEDS_ACK=1; printf '%s•%s %s\n' "$CAU_C_DIM" "$CAU_C_RESET" "$*"; }
# ---------------------------------------------------------------------------
# State files
+14 -2
View File
@@ -87,6 +87,13 @@ cau_ui_status() {
"$(cau_msg "A restart is recommended to finish a kernel update.")" \
"$CAU_C_RESET"
fi
local held
held="$(cau_state_read held_back '')"
if [[ -n $held ]]; then
printf '\n %s%s%s\n' "$CAU_C_YELLOW" \
"$(cau_msg "Held back: %s" "$held")" "$CAU_C_RESET"
fi
}
# cau_ui_status_conditions
@@ -152,17 +159,22 @@ cau_ui_menu() {
printf '%s\n' "$choice"
case "$choice" in
# The two switches flip and return straight to the menu, where the
# status block shows the result. Only a warning or an error holds
# the screen (see CAU_UI_NEEDS_ACK).
1)
CAU_UI_NEEDS_ACK=''
if [[ $CFG_ENABLED == yes ]]; then cau_do_disable; else cau_do_enable; fi
cau_pause
if [[ -n $CAU_UI_NEEDS_ACK ]]; then cau_pause; fi
;;
2)
CAU_UI_NEEDS_ACK=''
if [[ $CFG_NOTIFICATIONS == yes ]]; then
cau_do_notifications off
else
cau_do_notifications on
fi
cau_pause
if [[ -n $CAU_UI_NEEDS_ACK ]]; then cau_pause; fi
;;
3) cau_do_run --force; cau_pause ;;
4) cau_do_log; cau_pause ;;
+66 -18
View File
@@ -10,6 +10,9 @@
CAU_PACMAN_COUNT=0
CAU_PACMAN_PENDING=''
# Packages that had to be skipped so the rest of the upgrade could go through.
CAU_PACMAN_HELD=''
# Base flags for every unattended pacman invocation.
cau_pacman_flags() {
printf '%s\n' --noconfirm --color never --noprogressbar --disable-download-timeout
@@ -55,6 +58,8 @@ _cau_pacman_classify() {
if grep -qiE 'are in conflict|unresolvable package conflicts' "$log"; then
printf 'conflict\n'
elif grep -qiE 'could not satisfy dependencies|breaks dependency|unable to satisfy dependency' "$log"; then
printf 'dependency\n'
elif grep -qiE 'signature from .* is (unknown trust|marginal trust|invalid)|invalid or corrupted package \(PGP signature\)|key ".*" is unknown|keyring is not writable' "$log"; then
printf 'keyring\n'
elif grep -qiE 'exists in filesystem' "$log"; then
@@ -64,6 +69,25 @@ _cau_pacman_classify() {
fi
}
# _cau_pacman_blockers <logfile>
# The packages standing in the way of an otherwise fine upgrade. pacman names
# them in its dependency errors:
#
# :: removing libperconaserverclient breaks dependency 'libperconaserverclient'
# required by heidisql-qt6-bin
# :: unable to satisfy dependency 'foo' required by bar
#
# In the first form the package being removed is the one to keep; in the second
# it is the package that cannot be installed.
_cau_pacman_blockers() {
local log="$1"
{
sed -nE "s/.*removing ([^ ]+) breaks dependency.*/\\1/p" "$log"
sed -nE "s/.*unable to satisfy dependency '[^']*' required by ([^ ]+).*/\\1/p" "$log"
} | grep -E '^[A-Za-z0-9@._+-]+$' | sort -u
}
# cau_pacman_update
# Returns 0 on success (including "nothing to do"), 1 on a failure the user
# needs to hear about. CAU_PACMAN_COUNT holds how many packages moved.
@@ -90,8 +114,17 @@ cau_pacman_update() {
mapfile -t flags < <(cau_pacman_flags)
log="$(mktemp)" || return 1
if pacman -Syu "${flags[@]}" > "$log" 2>&1; then
# Recovery loop rather than a single retry: fixing one problem regularly
# uncovers the next (a conflict resolved into a dependency error, say).
# Each remedy is applied at most once, so this always terminates.
local -a extra=() blockers=() tried=()
local attempt=0 b
while true; do
if pacman -Syu "${flags[@]}" "${extra[@]}" > "$log" 2>&1; then
cat "$log" >> "$CAU_RUNLOG" 2>/dev/null
grep -E '^(removing|replacing) ' "$log" 2>/dev/null \
| while read -r line; do cau_info " $line"; done
rm -f "$log"
return 0
fi
@@ -100,6 +133,11 @@ cau_pacman_update() {
kind="$(_cau_pacman_classify "$log")"
cau_warn "pacman -Syu failed ($kind)"
if (( ++attempt > 3 )) || [[ " ${tried[*]} " == *" $kind "* ]]; then
break
fi
tried+=("$kind")
case "$kind" in
keyring)
# A stale keyring is the one failure that is always safe to fix
@@ -111,12 +149,6 @@ cau_pacman_update() {
if (( ${#keyrings[@]} )); then
cau_run_logged pacman -Sy --noconfirm --color never "${keyrings[@]}" || true
fi
if pacman -Syu "${flags[@]}" > "$log" 2>&1; then
cat "$log" >> "$CAU_RUNLOG" 2>/dev/null
rm -f "$log"
return 0
fi
cat "$log" >> "$CAU_RUNLOG" 2>/dev/null
;;
conflict)
@@ -126,18 +158,29 @@ cau_pacman_update() {
# question bitmask: 4 = CONFLICT_PKG, 16 = REMOVE_PKGS.
if [[ $CFG_RESOLVE_CONFLICTS != yes ]]; then
cau_error "Package conflict requires a decision (AutoResolveConflicts is off)"
rm -f "$log"
return 1
break
fi
cau_info "Resolving package conflicts automatically and retrying"
if pacman -Syu "${flags[@]}" --ask=20 > "$log" 2>&1; then
cat "$log" >> "$CAU_RUNLOG" 2>/dev/null
grep -E '^(removing|replacing) ' "$log" 2>/dev/null \
| while read -r line; do cau_info " $line"; done
rm -f "$log"
return 0
extra+=(--ask=20)
;;
dependency)
# Something installed still depends on a package the repos want
# to drop or replace - almost always an AUR package that has not
# caught up yet. Nothing here can fix that, and it is not worth
# failing over: letting one stuck package block every other
# update indefinitely is far worse on an unattended machine.
# Hold the blockers back and upgrade everything else.
mapfile -t blockers < <(_cau_pacman_blockers "$log")
if (( ${#blockers[@]} == 0 )); then
cau_error "Dependency problem with no package to hold back"
break
fi
cat "$log" >> "$CAU_RUNLOG" 2>/dev/null
for b in "${blockers[@]}"; do
extra+=(--ignore "$b")
done
CAU_PACMAN_HELD="${blockers[*]}"
cau_warn "Holding back ${blockers[*]} and retrying without them"
;;
filesystem)
@@ -145,13 +188,18 @@ cau_pacman_update() {
# silently clobber something the user put there deliberately, so
# this one stays a human decision.
cau_error "Files on disk conflict with the update; manual review needed"
rm -f "$log"
return 1
break
;;
*)
break
;;
esac
done
rm -f "$log"
CAU_PACMAN_COUNT=0
CAU_PACMAN_HELD=''
return 1
}