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:
1 parent
327da1dae3
commit
ceb024d7be
28 files changed
+3856
-1
No files matched your search
@@ -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 ))
|
||||
Reference in new issue
Block a user