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.
159 lines
4.6 KiB
Bash
159 lines
4.6 KiB
Bash
# shellcheck shell=bash
|
|
#
|
|
# AUR packages.
|
|
#
|
|
# makepkg - and therefore paru and yay - refuse to run as root, so this is the
|
|
# one part of the run that cannot happen in the service's own context. It is
|
|
# executed as the locked "cachy-auto-update" system account instead, which
|
|
# sysusers.d creates with no password and no shell. That account is granted
|
|
# NOPASSWD access to /usr/bin/pacman through /etc/sudoers.d/cachy-auto-update,
|
|
# which is what lets the helper install what it built without a human present.
|
|
#
|
|
# The alternative - stashing the user's password somewhere the daemon can read
|
|
# it - buys nothing: whatever can decrypt it is exactly what an attacker would
|
|
# already have.
|
|
|
|
CAU_AUR_COUNT=0
|
|
CAU_AUR_HELPER=''
|
|
|
|
# Notify only after this many consecutive failed AUR runs. A single failed
|
|
# build is routine (upstream broke a tarball, a checksum moved) and self-heals
|
|
# a day later; nagging about it on someone's parents' machine is noise.
|
|
CAU_AUR_FAILURE_THRESHOLD=2
|
|
|
|
# cau_aur_detect
|
|
# Resolves the helper to use, honouring AURHelper from the config.
|
|
cau_aur_detect() {
|
|
local candidate
|
|
|
|
CAU_AUR_HELPER=''
|
|
|
|
if [[ -n $CFG_AUR_HELPER && $CFG_AUR_HELPER != auto ]]; then
|
|
if cau_have "$CFG_AUR_HELPER"; then
|
|
CAU_AUR_HELPER="$CFG_AUR_HELPER"
|
|
return 0
|
|
fi
|
|
cau_warn "Configured AUR helper '$CFG_AUR_HELPER' not found"
|
|
return 1
|
|
fi
|
|
|
|
for candidate in paru yay pikaur; do
|
|
if cau_have "$candidate"; then
|
|
CAU_AUR_HELPER="$candidate"
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
# cau_as_build_user <command> [args...]
|
|
# A deliberately small environment: the helper gets its own HOME and cache so
|
|
# nothing it downloads ever lands in a human's home directory.
|
|
cau_as_build_user() {
|
|
runuser -u "$CAU_BUILD_USER" -- env \
|
|
"HOME=$CAU_BUILD_HOME" \
|
|
"USER=$CAU_BUILD_USER" \
|
|
"LOGNAME=$CAU_BUILD_USER" \
|
|
"XDG_CACHE_HOME=$CAU_CACHEDIR" \
|
|
"XDG_CONFIG_HOME=$CAU_BUILD_HOME/.config" \
|
|
"XDG_DATA_HOME=$CAU_BUILD_HOME/.local/share" \
|
|
"PATH=/usr/local/sbin:/usr/local/bin:/usr/bin" \
|
|
LC_ALL=C \
|
|
"$@"
|
|
}
|
|
|
|
# cau_aur_ready
|
|
# True when everything the AUR path needs is actually in place.
|
|
cau_aur_ready() {
|
|
cau_aur_detect || { cau_info "No AUR helper installed; skipping AUR updates"; return 1; }
|
|
|
|
if ! getent passwd "$CAU_BUILD_USER" > /dev/null; then
|
|
cau_warn "Build account '$CAU_BUILD_USER' is missing; skipping AUR updates"
|
|
return 1
|
|
fi
|
|
|
|
# -l asks sudo whether the command is permitted; -n guarantees it can
|
|
# never block on a password prompt. Testing `sudo -n true` instead would
|
|
# fail by design, because the rule is scoped to pacman alone.
|
|
if ! cau_as_build_user sudo -n -l /usr/bin/pacman > /dev/null 2>&1; then
|
|
cau_warn "Build account cannot run pacman without a password; check /etc/sudoers.d/cachy-auto-update"
|
|
return 1
|
|
fi
|
|
|
|
if ! cau_have makepkg; then
|
|
cau_warn "makepkg not found (base-devel missing); skipping AUR updates"
|
|
return 1
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# cau_aur_helper_args
|
|
# The flags that turn an interactive helper into a silent one.
|
|
cau_aur_helper_args() {
|
|
case "$CAU_AUR_HELPER" in
|
|
paru)
|
|
printf '%s\n' -Sua --noconfirm --skipreview --removemake --cleanafter --color never
|
|
[[ $CFG_DEVEL == yes ]] && printf '%s\n' --devel
|
|
;;
|
|
yay)
|
|
printf '%s\n' -Sua --noconfirm --removemake --cleanafter --color never
|
|
printf '%s\n' --answerclean All --answerdiff None --answeredit None --answerupgrade None
|
|
[[ $CFG_DEVEL == yes ]] && printf '%s\n' --devel
|
|
;;
|
|
pikaur)
|
|
printf '%s\n' -Sua --noconfirm --noedit
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# cau_aur_pending
|
|
# Number of AUR packages with an update available.
|
|
cau_aur_pending() {
|
|
local out
|
|
out="$(cau_as_build_user "$CAU_AUR_HELPER" -Qua 2>/dev/null | grep -c .)" || out=0
|
|
[[ $out =~ ^[0-9]+$ ]] || out=0
|
|
printf '%s\n' "$out"
|
|
}
|
|
|
|
# cau_aur_update
|
|
# Returns 0 on success or "nothing to do", 1 on a failure worth reporting.
|
|
# Transient build failures are swallowed until they repeat.
|
|
cau_aur_update() {
|
|
local pending failures
|
|
local -a args
|
|
|
|
cau_aur_ready || return 0
|
|
|
|
pending="$(cau_aur_pending)"
|
|
if (( pending == 0 )); then
|
|
cau_info "No AUR updates pending"
|
|
cau_state_clear aur_failures
|
|
return 0
|
|
fi
|
|
|
|
cau_info "Updating $pending AUR package(s) with $CAU_AUR_HELPER"
|
|
mapfile -t args < <(cau_aur_helper_args)
|
|
|
|
if cau_run_logged cau_as_build_user "$CAU_AUR_HELPER" "${args[@]}"; then
|
|
CAU_AUR_COUNT="$pending"
|
|
cau_state_clear aur_failures
|
|
return 0
|
|
fi
|
|
|
|
CAU_AUR_COUNT=0
|
|
failures="$(cau_state_read aur_failures 0)"
|
|
[[ $failures =~ ^[0-9]+$ ]] || failures=0
|
|
failures=$(( failures + 1 ))
|
|
cau_state_write aur_failures "$failures"
|
|
|
|
if (( failures >= CAU_AUR_FAILURE_THRESHOLD )); then
|
|
cau_error "AUR update failed $failures times in a row"
|
|
return 1
|
|
fi
|
|
|
|
cau_warn "AUR update failed (attempt $failures); will retry on the next run"
|
|
return 0
|
|
}
|