Compare commits

..
5 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
Felitendo 20fbadbc95 Menu: act on a single keypress instead of waiting for Enter
Also stops Enter from quitting the menu (it used to share the quit branch with
q) and swallows the tail of an escape sequence, so one arrow key redraws once
rather than three times and never closes the menu.
2026-08-08 14:58:28 +02:00
Felitendo e42fb4f61c Make the reported version follow the version actually being built
VERSION is now overridable, so the PKGBUILD passes $pkgver into make and the
two can no longer disagree. v1.0.1 shipped a Makefile that still said 1.0.0, so
'cachy-auto-update --version' reported the wrong release.
2026-08-08 14:45:07 +02:00
Felitendo 459cbbf8e7 Fix locale handling, menu re-entry and empty-checkupdates logging
- The runner now forces LC_ALL=C itself instead of relying on the unit's
  Environment=. pacman failures are classified by matching its output, so a
  run started by hand on a German system was misclassifying every failure.
- 'Update now' in the menu exec'd the runner, which terminated the menu.
- Log a distinct message when checkupdates is unavailable, instead of
  claiming zero pending packages before a full upgrade.
2026-08-08 02:35:34 +02:00
8 changed files with 168 additions and 31 deletions

No files matched your search

+4 -1
View File
@@ -5,7 +5,10 @@
# targets degrade to a no-op when msgfmt/scdoc are missing, so the tree stays # targets degrade to a no-op when msgfmt/scdoc are missing, so the tree stays
# usable for development without the build dependencies installed. # usable for development without the build dependencies installed.
VERSION := 1.0.0 # 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.5
PREFIX ?= /usr PREFIX ?= /usr
DESTDIR ?= DESTDIR ?=
+12 -1
View File
@@ -134,7 +134,7 @@ msgstr ""
msgid "Quit" msgid "Quit"
msgstr "" msgstr ""
msgid "Press Enter to continue..." msgid "Press any key to continue..."
msgstr "" msgstr ""
#. Commands #. Commands
@@ -243,3 +243,14 @@ msgstr ""
msgid "pacman's lock file looks left over from an interrupted update. Updates are paused until it is cleared." msgid "pacman's lock file looks left over from an interrupted update. Updates are paused until it is cleared."
msgstr "" 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 ""
+13 -2
View File
@@ -135,8 +135,8 @@ msgstr "Aktuelle Bedingungen anzeigen"
msgid "Quit" msgid "Quit"
msgstr "Beenden" msgstr "Beenden"
msgid "Press Enter to continue..." msgid "Press any key to continue..."
msgstr "Zum Fortfahren Enter drücken …" msgstr "Zum Fortfahren eine Taste drücken …"
#. Commands #. Commands
msgid "Automatic updates are on." msgid "Automatic updates are on."
@@ -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." 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." 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"
+3 -1
View File
@@ -170,7 +170,9 @@ cau_do_status() {
cau_do_run() { cau_do_run() {
cau_need_root cau_need_root
exec "$CAU_LIBEXECDIR/cachy-auto-update-run" "$@" # Not exec'd: this is also called from the menu, which has to survive the
# run and redraw afterwards.
"$CAU_LIBEXECDIR/cachy-auto-update-run" "$@"
} }
cau_do_log() { cau_do_log() {
+23
View File
@@ -40,6 +40,13 @@ if [[ $EUID -ne 0 ]]; then
exit 2 exit 2
fi fi
# Force a neutral locale here rather than relying on the unit's Environment=,
# so a run started by hand behaves exactly like one started by the timer.
# pacman failures are classified by matching its output, and on a German system
# that output is German. Text aimed at a person does not come through here - a
# notification is rendered in the recipient's own locale by cau_msg_in.
export LC_ALL=C LANGUAGE=
# Re-exec under an inhibitor so a suspend or shutdown cannot land in the middle # Re-exec under an inhibitor so a suspend or shutdown cannot land in the middle
# of a pacman transaction and leave the database half-written. Done before # of a pacman transaction and leave the database half-written. Done before
# anything else so the whole pass is covered, including the lock. # anything else so the whole pass is covered, including the lock.
@@ -195,6 +202,22 @@ else
fi fi
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 if cau_pacman_reboot_needed; then
cau_state_write reboot_needed 1 cau_state_write reboot_needed 1
cau_info "A kernel update needs a restart" cau_info "A kernel update needs a restart"
+9 -2
View File
@@ -146,11 +146,18 @@ cau_run_logged() {
# Terminal helpers for the CLI # 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_say() { printf '%s\n' "$*"; }
cau_head() { printf '\n%s%s%s\n\n' "$CAU_C_BOLD$CAU_C_BLUE" "$*" "$CAU_C_RESET"; } 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_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_bad() { CAU_UI_NEEDS_ACK=1; 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_note() { CAU_UI_NEEDS_ACK=1; printf '%s•%s %s\n' "$CAU_C_DIM" "$CAU_C_RESET" "$*"; }
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# State files # State files
+31 -6
View File
@@ -87,6 +87,13 @@ cau_ui_status() {
"$(cau_msg "A restart is recommended to finish a kernel update.")" \ "$(cau_msg "A restart is recommended to finish a kernel update.")" \
"$CAU_C_RESET" "$CAU_C_RESET"
fi 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 # cau_ui_status_conditions
@@ -145,31 +152,49 @@ cau_ui_menu() {
printf ' [q] %s\n' "$(cau_msg "Quit")" printf ' [q] %s\n' "$(cau_msg "Quit")"
printf '\n > ' printf '\n > '
read -r choice || { printf '\n'; return 0; } # One keypress, no Enter. -s keeps the raw character out of the
# display so the echo below is the only thing printed, and a failing
# read means EOF (Ctrl-D, or a script piping input) - that quits.
read -rsn1 choice || { printf '\n'; return 0; }
printf '%s\n' "$choice"
case "$choice" in 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) 1)
CAU_UI_NEEDS_ACK=''
if [[ $CFG_ENABLED == yes ]]; then cau_do_disable; else cau_do_enable; fi 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) 2)
CAU_UI_NEEDS_ACK=''
if [[ $CFG_NOTIFICATIONS == yes ]]; then if [[ $CFG_NOTIFICATIONS == yes ]]; then
cau_do_notifications off cau_do_notifications off
else else
cau_do_notifications on cau_do_notifications on
fi fi
cau_pause if [[ -n $CAU_UI_NEEDS_ACK ]]; then cau_pause; fi
;; ;;
3) cau_do_run --force; cau_pause ;; 3) cau_do_run --force; cau_pause ;;
4) cau_do_log; cau_pause ;; 4) cau_do_log; cau_pause ;;
5) cau_ui_status_conditions; cau_pause ;; 5) cau_ui_status_conditions; cau_pause ;;
q|Q|'') return 0 ;; q|Q) return 0 ;;
$'\e')
# Arrow keys and friends arrive as ESC [ X. Swallow the rest so
# one keypress does not redraw the menu three times. Escape is
# deliberately not a quit key: that would make a stray arrow
# key close the menu.
read -rsn2 -t 0.05 _ 2>/dev/null || true
;;
# Anything else, Enter included, just redraws.
*) ;; *) ;;
esac esac
done done
} }
cau_pause() { cau_pause() {
printf '\n %s' "$(cau_msg "Press Enter to continue...")" printf '\n %s' "$(cau_msg "Press any key to continue...")"
read -r _ || true read -rsn1 _ || true
printf '\n'
} }
+73 -18
View File
@@ -10,6 +10,9 @@
CAU_PACMAN_COUNT=0 CAU_PACMAN_COUNT=0
CAU_PACMAN_PENDING='' 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. # Base flags for every unattended pacman invocation.
cau_pacman_flags() { cau_pacman_flags() {
printf '%s\n' --noconfirm --color never --noprogressbar --disable-download-timeout 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 if grep -qiE 'are in conflict|unresolvable package conflicts' "$log"; then
printf 'conflict\n' 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 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' printf 'keyring\n'
elif grep -qiE 'exists in filesystem' "$log"; then elif grep -qiE 'exists in filesystem' "$log"; then
@@ -64,6 +69,25 @@ _cau_pacman_classify() {
fi 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 # cau_pacman_update
# Returns 0 on success (including "nothing to do"), 1 on a failure the user # 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. # needs to hear about. CAU_PACMAN_COUNT holds how many packages moved.
@@ -76,15 +100,31 @@ cau_pacman_update() {
return 0 return 0
fi fi
if [[ -n $CAU_PACMAN_PENDING ]]; then
CAU_PACMAN_COUNT="$(grep -c . <<< "$CAU_PACMAN_PENDING")" CAU_PACMAN_COUNT="$(grep -c . <<< "$CAU_PACMAN_PENDING")"
[[ $CAU_PACMAN_COUNT =~ ^[0-9]+$ ]] || CAU_PACMAN_COUNT=0 [[ $CAU_PACMAN_COUNT =~ ^[0-9]+$ ]] || CAU_PACMAN_COUNT=0
cau_info "Updating $CAU_PACMAN_COUNT repository package(s)" cau_info "Updating $CAU_PACMAN_COUNT repository package(s)"
else
# checkupdates is unavailable, so the list is unknown and pacman is
# asked to work it out itself.
CAU_PACMAN_COUNT=0
cau_info "Running a full system upgrade (pending list unavailable)"
fi
mapfile -t flags < <(cau_pacman_flags) mapfile -t flags < <(cau_pacman_flags)
log="$(mktemp)" || return 1 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 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" rm -f "$log"
return 0 return 0
fi fi
@@ -93,6 +133,11 @@ cau_pacman_update() {
kind="$(_cau_pacman_classify "$log")" kind="$(_cau_pacman_classify "$log")"
cau_warn "pacman -Syu failed ($kind)" cau_warn "pacman -Syu failed ($kind)"
if (( ++attempt > 3 )) || [[ " ${tried[*]} " == *" $kind "* ]]; then
break
fi
tried+=("$kind")
case "$kind" in case "$kind" in
keyring) keyring)
# A stale keyring is the one failure that is always safe to fix # A stale keyring is the one failure that is always safe to fix
@@ -104,12 +149,6 @@ cau_pacman_update() {
if (( ${#keyrings[@]} )); then if (( ${#keyrings[@]} )); then
cau_run_logged pacman -Sy --noconfirm --color never "${keyrings[@]}" || true cau_run_logged pacman -Sy --noconfirm --color never "${keyrings[@]}" || true
fi 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) conflict)
@@ -119,18 +158,29 @@ cau_pacman_update() {
# question bitmask: 4 = CONFLICT_PKG, 16 = REMOVE_PKGS. # question bitmask: 4 = CONFLICT_PKG, 16 = REMOVE_PKGS.
if [[ $CFG_RESOLVE_CONFLICTS != yes ]]; then if [[ $CFG_RESOLVE_CONFLICTS != yes ]]; then
cau_error "Package conflict requires a decision (AutoResolveConflicts is off)" cau_error "Package conflict requires a decision (AutoResolveConflicts is off)"
rm -f "$log" break
return 1
fi fi
cau_info "Resolving package conflicts automatically and retrying" cau_info "Resolving package conflicts automatically and retrying"
if pacman -Syu "${flags[@]}" --ask=20 > "$log" 2>&1; then extra+=(--ask=20)
cat "$log" >> "$CAU_RUNLOG" 2>/dev/null ;;
grep -E '^(removing|replacing) ' "$log" 2>/dev/null \
| while read -r line; do cau_info " $line"; done dependency)
rm -f "$log" # Something installed still depends on a package the repos want
return 0 # 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 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) filesystem)
@@ -138,13 +188,18 @@ cau_pacman_update() {
# silently clobber something the user put there deliberately, so # silently clobber something the user put there deliberately, so
# this one stays a human decision. # this one stays a human decision.
cau_error "Files on disk conflict with the update; manual review needed" cau_error "Files on disk conflict with the update; manual review needed"
rm -f "$log" break
return 1 ;;
*)
break
;; ;;
esac esac
done
rm -f "$log" rm -f "$log"
CAU_PACMAN_COUNT=0 CAU_PACMAN_COUNT=0
CAU_PACMAN_HELD=''
return 1 return 1
} }