- 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.
263 lines
7.1 KiB
Bash
263 lines
7.1 KiB
Bash
#!/usr/bin/env bash
|
|
#
|
|
# cachy-auto-update - unattended updates for CachyOS
|
|
#
|
|
# The command line front end. Everything it does is either flipping a switch in
|
|
# /etc/cachy-auto-update/cachy-auto-update.conf, driving the systemd timer, or
|
|
# handing off to the runner in @LIBEXECDIR@.
|
|
#
|
|
# Copyright (C) 2026 Felitendo
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
set -uo pipefail
|
|
|
|
CAU_LIBDIR="${CAU_LIBDIR:-@LIBDIR@}"
|
|
CAU_LIBEXECDIR="${CAU_LIBEXECDIR:-@LIBEXECDIR@}"
|
|
|
|
for _mod in common config users conditions locks notify menu; do
|
|
# shellcheck source=/dev/null
|
|
if ! source "$CAU_LIBDIR/$_mod.sh"; then
|
|
printf 'cachy-auto-update: cannot load %s/%s.sh\n' "$CAU_LIBDIR" "$_mod" >&2
|
|
exit 14
|
|
fi
|
|
done
|
|
unset _mod
|
|
|
|
CAU_SELF="$(readlink -f "${BASH_SOURCE[0]}")"
|
|
CAU_ARGV=("$@")
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Privilege
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Re-executes itself with elevation when a human is at the terminal, so that
|
|
# typing `cachy-auto-update` just works instead of printing a lecture.
|
|
cau_need_root() {
|
|
cau_is_root && return 0
|
|
|
|
local candidate elevate=''
|
|
if [[ -t 0 && -t 1 ]]; then
|
|
for candidate in sudo run0 doas; do
|
|
if cau_have "$candidate"; then elevate="$candidate"; break; fi
|
|
done
|
|
fi
|
|
|
|
if [[ -n $elevate ]]; then
|
|
exec "$elevate" "$CAU_SELF" "${CAU_ARGV[@]}"
|
|
fi
|
|
|
|
cau_bad "$(cau_msg "This command has to run as root.")"
|
|
exit 2
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Commands
|
|
# ---------------------------------------------------------------------------
|
|
|
|
cau_do_enable() {
|
|
cau_need_root
|
|
|
|
# A broken sudoers drop-in would take down sudo for the whole machine, so
|
|
# it is verified before anything is switched on.
|
|
if [[ -f /etc/sudoers.d/cachy-auto-update ]]; then
|
|
if cau_have visudo && ! visudo -cf /etc/sudoers.d/cachy-auto-update > /dev/null 2>&1; then
|
|
cau_bad "$(cau_msg "/etc/sudoers.d/cachy-auto-update is invalid; refusing to enable.")"
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
cau_config_set Enabled yes || { cau_bad "$(cau_msg "Could not write the configuration file.")"; return 1; }
|
|
|
|
if cau_have systemctl; then
|
|
systemctl enable --now cachy-auto-update.timer > /dev/null 2>&1 \
|
|
|| cau_bad "$(cau_msg "Could not enable the systemd timer.")"
|
|
fi
|
|
|
|
cau_ok "$(cau_msg "Automatic updates are on.")"
|
|
|
|
cau_config_load
|
|
if [[ $CFG_AUR == yes ]] && ! cau_aur_detect_quiet; then
|
|
cau_note "$(cau_msg "No AUR helper found - install paru or yay for AUR updates.")"
|
|
fi
|
|
if [[ $CFG_AUR == yes ]] && ! cau_have makepkg; then
|
|
cau_note "$(cau_msg "base-devel is missing - AUR packages cannot be built without it.")"
|
|
fi
|
|
|
|
cau_offer_disable_cachy_update
|
|
}
|
|
|
|
# Whether an AUR helper exists at all, without the logging cau_aur_detect does.
|
|
cau_aur_detect_quiet() {
|
|
local c
|
|
if [[ -n ${CFG_AUR_HELPER:-} && $CFG_AUR_HELPER != auto ]]; then
|
|
cau_have "$CFG_AUR_HELPER"
|
|
return $?
|
|
fi
|
|
for c in paru yay pikaur; do cau_have "$c" && return 0; done
|
|
return 1
|
|
}
|
|
|
|
# cachy-update ships an enabled user timer that only ever says "N updates
|
|
# available". Once updates apply themselves that notification is pure noise,
|
|
# so offer to silence it - but only ask, never decide.
|
|
cau_offer_disable_cachy_update() {
|
|
local user uid answer found=0
|
|
|
|
[[ -t 0 && -t 1 ]] || return 0
|
|
cau_have systemctl || return 0
|
|
|
|
while read -r user uid; do
|
|
[[ -n $user ]] || continue
|
|
if cau_as_user "$user" "$uid" systemctl --user is-enabled --quiet arch-update.timer 2>/dev/null; then
|
|
found=1
|
|
break
|
|
fi
|
|
done < <(cau_active_session_users)
|
|
|
|
(( found )) || return 0
|
|
|
|
printf '\n %s\n ' \
|
|
"$(cau_msg "cachy-update also notifies about available updates. Turn its notifications off? [y/N]")"
|
|
read -r answer || return 0
|
|
[[ ${answer,,} == y ]] || return 0
|
|
|
|
while read -r user uid; do
|
|
[[ -n $user ]] || continue
|
|
cau_as_user "$user" "$uid" systemctl --user disable --now arch-update.timer > /dev/null 2>&1 || true
|
|
done < <(cau_active_session_users)
|
|
|
|
cau_ok "$(cau_msg "cachy-update's update check has been disabled.")"
|
|
}
|
|
|
|
cau_do_disable() {
|
|
cau_need_root
|
|
|
|
cau_config_set Enabled no || { cau_bad "$(cau_msg "Could not write the configuration file.")"; return 1; }
|
|
|
|
if cau_have systemctl; then
|
|
systemctl disable --now cachy-auto-update.timer > /dev/null 2>&1 || true
|
|
fi
|
|
|
|
cau_ok "$(cau_msg "Automatic updates are off.")"
|
|
}
|
|
|
|
cau_do_notifications() {
|
|
cau_need_root
|
|
|
|
case "${1:-}" in
|
|
on|yes|true|1)
|
|
cau_config_set Notifications yes || return 1
|
|
cau_ok "$(cau_msg "Notifications are on.")"
|
|
;;
|
|
off|no|false|0)
|
|
cau_config_set Notifications no || return 1
|
|
cau_ok "$(cau_msg "Notifications are off.")"
|
|
;;
|
|
*)
|
|
cau_bad "$(cau_msg "Usage: cachy-auto-update notifications on|off")"
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
cau_do_status() {
|
|
cau_config_load
|
|
cau_head " $CAU_PRETTY"
|
|
cau_ui_status
|
|
cau_ui_status_conditions
|
|
printf '\n'
|
|
}
|
|
|
|
cau_do_run() {
|
|
cau_need_root
|
|
# 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() {
|
|
local file="$CAU_RUNLOG" lines=200 follow=0
|
|
|
|
while (( $# )); do
|
|
case "$1" in
|
|
-n) lines="${2:-200}"; shift 2 ;;
|
|
-n*) lines="${1#-n}"; shift ;;
|
|
-f|--follow) follow=1; shift ;;
|
|
-a|--all) file="$CAU_LOGFILE"; shift ;;
|
|
*) shift ;;
|
|
esac
|
|
done
|
|
|
|
if [[ ! -r $file ]]; then
|
|
cau_note "$(cau_msg "No log yet.")"
|
|
return 0
|
|
fi
|
|
|
|
if (( follow )); then
|
|
tail -n "$lines" -f "$file"
|
|
else
|
|
tail -n "$lines" "$file"
|
|
fi
|
|
}
|
|
|
|
cau_do_help() {
|
|
cat <<- EOF
|
|
$CAU_PRETTY $CAU_VERSION
|
|
|
|
$(cau_msg "Usage: cachy-auto-update [command]")
|
|
|
|
$(cau_msg "Commands:")
|
|
enable $(cau_msg "Turn automatic updates on")
|
|
disable $(cau_msg "Turn automatic updates off")
|
|
notifications on|off $(cau_msg "Turn desktop notifications on or off")
|
|
status $(cau_msg "Show the current state and conditions")
|
|
run [--force] [--dry-run] $(cau_msg "Run an update now")
|
|
log [-n N] [-f] [-a] $(cau_msg "Show the log of the last run")
|
|
-h, --help $(cau_msg "Show this help")
|
|
-V, --version $(cau_msg "Show the version")
|
|
|
|
$(cau_msg "Without a command an interactive menu is shown.")
|
|
EOF
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Dispatch
|
|
# ---------------------------------------------------------------------------
|
|
|
|
main() {
|
|
local cmd="${1:-}"
|
|
[[ $# -gt 0 ]] && shift
|
|
|
|
case "$cmd" in
|
|
enable) cau_do_enable "$@" ;;
|
|
disable) cau_do_disable "$@" ;;
|
|
notifications) cau_do_notifications "$@" ;;
|
|
status) cau_do_status "$@" ;;
|
|
run) cau_do_run "$@" ;;
|
|
log) cau_do_log "$@" ;;
|
|
|
|
# Invoked unprivileged from the XDG autostart entry to replay
|
|
# notifications that were produced while nobody was logged in.
|
|
--deliver-notifications)
|
|
cau_config_load
|
|
cau_notify_deliver_queue
|
|
;;
|
|
|
|
-h|--help|help) cau_do_help ;;
|
|
-V|--version) printf '%s %s\n' "$CAU_NAME" "$CAU_VERSION" ;;
|
|
|
|
'')
|
|
cau_need_root
|
|
cau_ui_menu
|
|
;;
|
|
*)
|
|
cau_bad "$(cau_msg "Unknown command: %s" "$cmd")"
|
|
printf '\n'
|
|
cau_do_help
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|