diff --git a/Makefile b/Makefile index aa4774b..395325c 100644 --- a/Makefile +++ b/Makefile @@ -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.4 +VERSION ?= 1.0.5 PREFIX ?= /usr DESTDIR ?= diff --git a/po/cachy-auto-update.pot b/po/cachy-auto-update.pot index 059d20f..073ed7f 100644 --- a/po/cachy-auto-update.pot +++ b/po/cachy-auto-update.pot @@ -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 "" diff --git a/po/de.po b/po/de.po index 631c74e..261b8fa 100644 --- a/po/de.po +++ b/po/de.po @@ -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" diff --git a/src/cachy-auto-update-run b/src/cachy-auto-update-run index f6165a3..45fd9cd 100644 --- a/src/cachy-auto-update-run +++ b/src/cachy-auto-update-run @@ -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" diff --git a/src/lib/menu.sh b/src/lib/menu.sh index bbb47c2..441b70c 100644 --- a/src/lib/menu.sh +++ b/src/lib/menu.sh @@ -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 diff --git a/src/lib/pkg_pacman.sh b/src/lib/pkg_pacman.sh index d656c82..2ba468f 100644 --- a/src/lib/pkg_pacman.sh +++ b/src/lib/pkg_pacman.sh @@ -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 +# 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,68 +114,92 @@ cau_pacman_update() { mapfile -t flags < <(cau_pacman_flags) log="$(mktemp)" || return 1 - if pacman -Syu "${flags[@]}" > "$log" 2>&1; then - cat "$log" >> "$CAU_RUNLOG" 2>/dev/null - rm -f "$log" - return 0 - fi + # 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 - cat "$log" >> "$CAU_RUNLOG" 2>/dev/null - kind="$(_cau_pacman_classify "$log")" - cau_warn "pacman -Syu failed ($kind)" - - case "$kind" in - keyring) - # A stale keyring is the one failure that is always safe to fix - # automatically, and it blocks everything else until it is. - cau_info "Refreshing keyrings and retrying" - local -a keyrings=() - pacman -Qq archlinux-keyring &> /dev/null && keyrings+=(archlinux-keyring) - pacman -Qq cachyos-keyring &> /dev/null && keyrings+=(cachyos-keyring) - 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 + while true; do + if pacman -Syu "${flags[@]}" "${extra[@]}" > "$log" 2>&1; then cat "$log" >> "$CAU_RUNLOG" 2>/dev/null - ;; - - conflict) - # A package that has to replace another one. --noconfirm already - # answers "Replace X with Y?" affirmatively; what it declines is - # ":: X and Y are in conflict. Remove Y? [y/N]". --ask is pacman's - # 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 - 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 - fi - cat "$log" >> "$CAU_RUNLOG" 2>/dev/null - ;; - - filesystem) - # Untracked files in the way. Forcing --overwrite here could - # 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" + grep -E '^(removing|replacing) ' "$log" 2>/dev/null \ + | while read -r line; do cau_info " $line"; done rm -f "$log" - return 1 - ;; - esac + return 0 + fi + + cat "$log" >> "$CAU_RUNLOG" 2>/dev/null + 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 + # automatically, and it blocks everything else until it is. + cau_info "Refreshing keyrings and retrying" + local -a keyrings=() + pacman -Qq archlinux-keyring &> /dev/null && keyrings+=(archlinux-keyring) + pacman -Qq cachyos-keyring &> /dev/null && keyrings+=(cachyos-keyring) + if (( ${#keyrings[@]} )); then + cau_run_logged pacman -Sy --noconfirm --color never "${keyrings[@]}" || true + fi + ;; + + conflict) + # A package that has to replace another one. --noconfirm already + # answers "Replace X with Y?" affirmatively; what it declines is + # ":: X and Y are in conflict. Remove Y? [y/N]". --ask is pacman's + # question bitmask: 4 = CONFLICT_PKG, 16 = REMOVE_PKGS. + if [[ $CFG_RESOLVE_CONFLICTS != yes ]]; then + cau_error "Package conflict requires a decision (AutoResolveConflicts is off)" + break + fi + cau_info "Resolving package conflicts automatically and retrying" + 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 + for b in "${blockers[@]}"; do + extra+=(--ignore "$b") + done + CAU_PACMAN_HELD="${blockers[*]}" + cau_warn "Holding back ${blockers[*]} and retrying without them" + ;; + + filesystem) + # Untracked files in the way. Forcing --overwrite here could + # 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" + break + ;; + + *) + break + ;; + esac + done rm -f "$log" CAU_PACMAN_COUNT=0 + CAU_PACMAN_HELD='' return 1 }