Files
cachy-auto-update/src/lib/pkg_aur.sh
T
Felitendo 6aa9b147b8 Put a progress bar on the desktop, and repair the notifications around it
An unattended run takes twenty minutes and said nothing at all while it worked.
The notification area now carries a live entry for the duration - which step is
running, which package is being unpacked, item count, percentage. It is a job
in the sense of org.kde.JobViewServer, the same mechanism Dolphin uses while
copying files, rather than a notification, which is what makes it a real bar
instead of a line of text.

That mechanism needs a process of its own. The desktop ties a job to the D-Bus
connection that requested it and withdraws it the moment that connection
closes, so gdbus, busctl and dbus-send cannot drive one at all - every
invocation is a fresh connection that closes again immediately. A small helper
therefore runs inside each graphical session for the length of the update,
holding the connection open and taking instructions on stdin. It needs
python-gobject; without it there is no bar and nothing else changes. Position
within the repository step is read from pacman's own "(120/260) upgrading foo"
lines. Its other (n/m) sequences are ignored on purpose: checking keys, package
integrity and loading files each count up to the same total, and following them
would run the bar to the end three times before the first package was unpacked.

The "system updated" notification never arrived, which is what prompted looking
at any of this. Tagged notifications were posted with --replace-id naming the
start message, and Plasma silently drops a Notify() whose replaces_id points at
an expired notification: no bubble, no error, and the id it hands back is the
dead one it just ignored. The start bubble times out in seconds and a run lasts
minutes, so the result fell into exactly that hole every single time. The
previous message is now withdrawn and a fresh one posted.

How long a message stays is set per message rather than left to the daemon.
Anything reporting that the machine still needs a person - a failed update, a
locked package database, packages that had to be held back - waits until it is
dismissed. Everything else times out on its own, a successful update included;
nothing should have to be clicked away for having gone right. Daemons do keep
critical-urgency messages up and the specification asks them to, but that is a
should, it says nothing about the normal-urgency messages here that still need
somebody to act, and urgency separately governs sound and do-not-disturb.

"Restart recommended" is gone, and NotifyReboot with it. The running kernel
loses its module tree the moment pacman unpacks the new one, so the notice
fired while the run was still building AUR packages and pulling Flatpaks, where
it reads as an invitation to restart in the middle of an update. The state is
still recorded and cachy-auto-update status still reports it. The option went
rather than only its default, because an installed system keeps its own
configuration file and would have carried on notifying regardless.
2026-08-14 10:49:02 +02:00

166 lines
4.9 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
cau_progress_step aur "AUR packages"
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"
# The helper builds each package from source with no counter this side of
# its output, so the bar sits at the start of the step until it is done.
cau_progress_item 0 "$pending"
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_progress_item "$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
}