Add cachy-auto-update: unattended background updates for CachyOS

A root systemd service applies pacman, AUR, Flatpak and AppImage updates on
its own, gated on battery state, gaming activity and whether anybody else is
using the package system. The CLI is deliberately two switches plus status.

No user password is stored anywhere: pacman runs as root directly, and the AUR
step - which makepkg forbids running as root - drops to a locked system account
that sudoers permits to call pacman without a password.
This commit is contained in:
Felitendo committed 2026-08-08 02:27:04 +02:00
1 parent 327da1dae3
commit ceb024d7be
28 files changed
+3856 -1

No files matched your search

+214
View File
@@ -0,0 +1,214 @@
#!/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
# 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
# ---------------------------------------------------------------------------
# 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
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 ))