#!/usr/bin/env bash # # cachy-auto-update-run - one unattended update pass # # Started by cachy-auto-update.service as root. The timer fires hourly and this # decides whether anything should happen; that is what makes deferrals free. # Refusing to run is the normal, expected outcome most of the time, so it is # never treated as an error. # # Copyright (C) 2026 Felitendo # SPDX-License-Identifier: GPL-3.0-or-later set -uo pipefail CAU_LIBDIR="${CAU_LIBDIR:-@LIBDIR@}" for _mod in common config users conditions locks notify \ pkg_pacman pkg_aur pkg_flatpak pkg_appimage; do # shellcheck source=/dev/null if ! source "$CAU_LIBDIR/$_mod.sh"; then printf 'cachy-auto-update-run: cannot load %s/%s.sh\n' "$CAU_LIBDIR" "$_mod" >&2 exit 14 fi done unset _mod FORCE=0 DRY_RUN=0 for arg in "$@"; do case "$arg" in --force) FORCE=1 ;; --dry-run) DRY_RUN=1 ;; --debug) CAU_DEBUG=1 ;; esac done if [[ $EUID -ne 0 ]]; then printf 'cachy-auto-update-run: must run as root\n' >&2 exit 2 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 # of a pacman transaction and leave the database half-written. Done before # anything else so the whole pass is covered, including the lock. if [[ -z ${CAU_INHIBITED:-} ]] && command -v systemd-inhibit > /dev/null 2>&1; then export CAU_INHIBITED=1 exec systemd-inhibit \ --what=sleep:shutdown \ --mode=block \ --who="CachyOS Auto-Update" \ --why="Applying system updates" \ -- "$0" "$@" fi cau_config_load cau_log_open # Record an interruption rather than leaving the previous run's verdict behind. # Without this, killing an interactive run leaves last_result at whatever it was # before - so the menu can keep reporting a problem from hours ago while the # machine is in fact fully up to date, which is worse than saying nothing. # pacman makes the commit phase itself uninterruptible, so the packages either # all landed or none did; only our own bookkeeping is at risk here. _cau_interrupted() { cau_error "Update run interrupted ($1)" cau_state_write last_result interrupted exit 130 } trap '_cau_interrupted SIGINT' INT trap '_cau_interrupted SIGTERM' TERM trap '_cau_interrupted SIGHUP' HUP # --------------------------------------------------------------------------- # Should this run happen at all? # --------------------------------------------------------------------------- defer() { cau_info "Deferred: $1" exit 0 } if ! cau_acquire_lock; then cau_debug "Another run is already in progress" exit 0 fi if (( ! FORCE )); then if [[ $CFG_ENABLED != yes ]]; then cau_debug "Automatic updates are disabled" exit 0 fi if ! cau_update_due; then cau_debug "Not due yet (interval $CFG_INTERVAL)" exit 0 fi fi cau_state_write last_run "$(date +%s)" if (( ! FORCE )); then CAU_SKIP_REASON='' cau_power_ok || defer "$CAU_SKIP_REASON" if [[ $CFG_SKIP_GAMING == yes ]]; then CAU_SKIP_REASON='' if cau_busy; then defer "$CAU_SKIP_REASON" fi fi fi # This one is checked even with --force: proceeding anyway would just hand the # user a lock error instead of doing anything useful. CAU_SKIP_REASON='' if cau_package_manager_busy; then if cau_track_stale_lock; then cau_warn "pacman's database lock appears to be stale" cau_notify normal \ "Package database locked" \ "pacman's lock file looks left over from an interrupted update. Updates are paused until it is cleared." fi defer "$CAU_SKIP_REASON" fi cau_state_clear stale_lock_count # --------------------------------------------------------------------------- # Dry run # --------------------------------------------------------------------------- if (( DRY_RUN )); then printf '%s\n' "Would update on this machine:" if cau_pacman_pending; then printf ' repository packages: %s\n' "$(grep -c . <<< "$CAU_PACMAN_PENDING")" sed 's/^/ /' <<< "$CAU_PACMAN_PENDING" else printf ' repository packages: 0\n' fi if [[ $CFG_AUR == yes ]] && cau_aur_ready; then printf ' AUR packages: %s (%s)\n' "$(cau_aur_pending)" "$CAU_AUR_HELPER" else printf ' AUR packages: skipped\n' fi if [[ $CFG_FLATPAK == yes ]] && cau_have flatpak; then printf ' system Flatpaks: %s\n' "$(cau_flatpak_pending_system)" else printf ' system Flatpaks: skipped\n' fi printf ' AppImages: %s\n' \ "$([[ $CFG_APPIMAGE == yes ]] && printf 'checked via Gear Lever' || printf 'skipped')" exit 0 fi # --------------------------------------------------------------------------- # Do the work # --------------------------------------------------------------------------- cau_info "Starting update run" failed=0 if ! cau_pacman_update; then failed=1 fi if [[ $CFG_AUR == yes ]]; then cau_aur_update || failed=1 fi if [[ $CFG_FLATPAK == yes ]]; then cau_flatpak_update || failed=1 fi if [[ $CFG_APPIMAGE == yes ]]; then cau_appimage_update || true # best effort; never marks the run as failed fi cau_pacman_cleanup pacnew="$(cau_pacman_pacnew_count)" if [[ $pacnew =~ ^[0-9]+$ ]] && (( pacnew > 0 )); then cau_info "$pacnew .pacnew file(s) present; left untouched" fi # --------------------------------------------------------------------------- # Record and report # --------------------------------------------------------------------------- total=$(( CAU_PACMAN_COUNT + CAU_AUR_COUNT + CAU_FLATPAK_COUNT + CAU_APPIMAGE_COUNT )) cau_state_write last_counts \ "$CAU_PACMAN_COUNT $CAU_AUR_COUNT $CAU_FLATPAK_COUNT $CAU_APPIMAGE_COUNT" if (( failed )); then cau_state_write last_result failed cau_error "Update run finished with errors" [[ $CFG_NOTIFY_ERROR == yes ]] && cau_notify critical \ "Update failed" \ "Something went wrong while updating. Run 'cachy-auto-update log' for details." else cau_state_write last_result ok cau_state_write last_success "$(date +%s)" cau_info "Update run finished successfully ($total item(s) updated)" if (( total > 0 )) && [[ $CFG_NOTIFY_SUCCESS == yes ]]; then cau_notify low "System updated" "%d updates were installed." "$total" 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" [[ $CFG_NOTIFY_REBOOT == yes ]] && cau_notify normal \ "Restart recommended" \ "A new kernel was installed. Please restart when it suits you." else cau_state_write reboot_needed 0 fi # Keep the state readable for the unprivileged `status` and `log` commands. # Only the plain files: the build account's home lives in here too and must # keep its own ownership and mode. chmod 0755 "$CAU_STATEDIR" 2>/dev/null || true find "$CAU_STATEDIR" -maxdepth 1 -type f -exec chmod 0644 {} + 2>/dev/null || true exit $(( failed ? 1 : 0 ))