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,225 @@
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# Paths, logging, translations and small shared helpers.
|
||||
#
|
||||
# Sourced by both the CLI and the update runner, so it must not assume it is
|
||||
# running as root and must not produce any output on its own.
|
||||
|
||||
CAU_VERSION="@VERSION@"
|
||||
CAU_NAME="cachy-auto-update"
|
||||
CAU_PRETTY="CachyOS Auto-Update"
|
||||
|
||||
# Directories. All of them are overridable so the tree can be exercised
|
||||
# straight from a git checkout without installing anything.
|
||||
CAU_LIBDIR="${CAU_LIBDIR:-@LIBDIR@}"
|
||||
CAU_LIBEXECDIR="${CAU_LIBEXECDIR:-@LIBEXECDIR@}"
|
||||
CAU_LOCALEDIR="${CAU_LOCALEDIR:-@LOCALEDIR@}"
|
||||
CAU_CONFDIR="${CAU_CONFDIR:-/etc/cachy-auto-update}"
|
||||
CAU_CONFIG="${CAU_CONFIG:-${CAU_CONFDIR}/cachy-auto-update.conf}"
|
||||
CAU_STATEDIR="${CAU_STATEDIR:-/var/lib/cachy-auto-update}"
|
||||
CAU_CACHEDIR="${CAU_CACHEDIR:-/var/cache/cachy-auto-update}"
|
||||
CAU_LOGDIR="${CAU_LOGDIR:-/var/log/cachy-auto-update}"
|
||||
CAU_RUNDIR="${CAU_RUNDIR:-/run/cachy-auto-update}"
|
||||
|
||||
CAU_LOGFILE="${CAU_LOGDIR}/cachy-auto-update.log"
|
||||
CAU_RUNLOG="${CAU_LOGDIR}/last-run.log"
|
||||
CAU_LOCKFILE="${CAU_RUNDIR}/run.lock"
|
||||
CAU_NOTIFY_QUEUE="${CAU_STATEDIR}/notify-queue"
|
||||
|
||||
# The locked system account that builds and installs AUR packages. Created by
|
||||
# sysusers.d; its home lives under the state directory.
|
||||
CAU_BUILD_USER="cachy-auto-update"
|
||||
CAU_BUILD_HOME="${CAU_STATEDIR}/builder"
|
||||
|
||||
# Pacman's own database lock. Presence means somebody is mid-transaction.
|
||||
CAU_PACMAN_LOCK="/var/lib/pacman/db.lck"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Translations
|
||||
# ---------------------------------------------------------------------------
|
||||
# Messages are authored in English and translated through gettext. The runner
|
||||
# is started with LC_ALL=C so that pacman/upower output stays parseable, which
|
||||
# means every user-facing string has to opt back into a real locale explicitly
|
||||
# via cau_msg / cau_msg_in.
|
||||
|
||||
export TEXTDOMAIN="cachy-auto-update"
|
||||
export TEXTDOMAINDIR="${CAU_LOCALEDIR}"
|
||||
|
||||
# The locale user-facing text should be rendered in.
|
||||
#
|
||||
# Standard POSIX precedence, deliberately: LC_ALL wins outright, and LC_ALL=C
|
||||
# really does mean English. The service sets LC_ALL=C so that pacman and upower
|
||||
# stay parseable, which makes the log English - correct, since the log is a
|
||||
# technical artefact. Anything aimed at a person (a desktop notification) does
|
||||
# not go through here at all; it names the recipient's own locale explicitly
|
||||
# via cau_msg_in and cau_user_locale.
|
||||
cau_ui_locale() {
|
||||
local l="${CAU_UI_LOCALE:-}"
|
||||
|
||||
if [[ -z $l ]]; then
|
||||
l="${LC_ALL:-}"
|
||||
[[ -z $l ]] && l="${LC_MESSAGES:-}"
|
||||
[[ -z $l ]] && l="${LANG:-}"
|
||||
fi
|
||||
|
||||
if [[ -z $l && -r /etc/locale.conf ]]; then
|
||||
l="$(sed -n 's/^LANG=//p' /etc/locale.conf | tr -d '"' | head -n1)"
|
||||
fi
|
||||
|
||||
printf '%s\n' "${l:-C}"
|
||||
}
|
||||
|
||||
# cau_msg <msgid> [printf args...]
|
||||
# Translate a string into the UI locale and printf it (no trailing newline).
|
||||
cau_msg() {
|
||||
cau_msg_in "$(cau_ui_locale)" "$@"
|
||||
}
|
||||
|
||||
# cau_msg_in <locale> <msgid> [printf args...]
|
||||
cau_msg_in() {
|
||||
local locale="$1" msgid="$2" translated
|
||||
shift 2
|
||||
|
||||
translated="$(LC_ALL="$locale" LANGUAGE="${locale%%.*}" gettext -- "$msgid" 2>/dev/null)"
|
||||
[[ -n $translated ]] || translated="$msgid"
|
||||
|
||||
# shellcheck disable=SC2059 # the format string is the translated message
|
||||
printf -- "$translated" "$@"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Output and logging
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if [[ -t 1 && -z ${NO_COLOR:-} ]]; then
|
||||
CAU_C_RESET=$'\033[0m'
|
||||
CAU_C_BOLD=$'\033[1m'
|
||||
CAU_C_DIM=$'\033[2m'
|
||||
CAU_C_BLUE=$'\033[38;2;23;147;209m'
|
||||
CAU_C_GREEN=$'\033[32m'
|
||||
CAU_C_YELLOW=$'\033[33m'
|
||||
CAU_C_RED=$'\033[31m'
|
||||
else
|
||||
CAU_C_RESET='' CAU_C_BOLD='' CAU_C_DIM='' CAU_C_BLUE=''
|
||||
CAU_C_GREEN='' CAU_C_YELLOW='' CAU_C_RED=''
|
||||
fi
|
||||
|
||||
# Appends to the persistent log when writable (i.e. when running as root) and
|
||||
# always echoes to stderr so journald picks it up for the service.
|
||||
cau_log() {
|
||||
local level="$1"
|
||||
shift
|
||||
local line
|
||||
line="$(date '+%Y-%m-%d %H:%M:%S') [$level] $*"
|
||||
|
||||
printf '%s\n' "$line" >&2
|
||||
if [[ -n ${CAU_LOG_OPEN:-} ]]; then
|
||||
printf '%s\n' "$line" >> "$CAU_LOGFILE" 2>/dev/null || true
|
||||
printf '%s\n' "$line" >> "$CAU_RUNLOG" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
cau_info() { cau_log INFO "$@"; }
|
||||
cau_warn() { cau_log WARN "$@"; }
|
||||
cau_error() { cau_log ERROR "$@"; }
|
||||
cau_debug() { [[ -n ${CAU_DEBUG:-} ]] && cau_log DEBUG "$@"; return 0; }
|
||||
|
||||
# Opens the log files for this run. Only meaningful as root.
|
||||
cau_log_open() {
|
||||
mkdir -p "$CAU_LOGDIR" 2>/dev/null || return 0
|
||||
chmod 0750 "$CAU_LOGDIR" 2>/dev/null || true
|
||||
: > "$CAU_RUNLOG" 2>/dev/null || return 0
|
||||
CAU_LOG_OPEN=1
|
||||
}
|
||||
|
||||
# Runs a command, streaming its combined output into the run log. Returns the
|
||||
# command's exit status.
|
||||
cau_run_logged() {
|
||||
if [[ -n ${CAU_LOG_OPEN:-} ]]; then
|
||||
"$@" >> "$CAU_RUNLOG" 2>&1
|
||||
else
|
||||
"$@" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Terminal helpers for the CLI
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
cau_say() { printf '%s\n' "$*"; }
|
||||
cau_head() { printf '\n%s%s%s\n\n' "$CAU_C_BOLD$CAU_C_BLUE" "$*" "$CAU_C_RESET"; }
|
||||
cau_ok() { printf '%s✔%s %s\n' "$CAU_C_GREEN" "$CAU_C_RESET" "$*"; }
|
||||
cau_bad() { printf '%s✘%s %s\n' "$CAU_C_RED" "$CAU_C_RESET" "$*" >&2; }
|
||||
cau_note() { printf '%s•%s %s\n' "$CAU_C_DIM" "$CAU_C_RESET" "$*"; }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# State files
|
||||
# ---------------------------------------------------------------------------
|
||||
# Every piece of state is one small file under CAU_STATEDIR. That keeps the
|
||||
# CLI, the runner and the login-time notification delivery from needing any
|
||||
# shared parsing code.
|
||||
|
||||
cau_state_read() {
|
||||
local key="$1" default="${2:-}"
|
||||
if [[ -r "$CAU_STATEDIR/$key" ]]; then
|
||||
cat "$CAU_STATEDIR/$key"
|
||||
else
|
||||
printf '%s\n' "$default"
|
||||
fi
|
||||
}
|
||||
|
||||
cau_state_write() {
|
||||
local key="$1"
|
||||
shift
|
||||
mkdir -p "$CAU_STATEDIR" 2>/dev/null || return 1
|
||||
printf '%s\n' "$*" > "$CAU_STATEDIR/$key"
|
||||
}
|
||||
|
||||
cau_state_clear() {
|
||||
rm -f "$CAU_STATEDIR/$1" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Misc
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
cau_have() { command -v "$1" > /dev/null 2>&1; }
|
||||
|
||||
cau_is_root() { [[ $EUID -eq 0 ]]; }
|
||||
|
||||
# Turns a duration such as "1d", "6h", "30m" or a bare number of seconds into
|
||||
# seconds. Returns the fallback for anything unparseable.
|
||||
cau_duration_to_seconds() {
|
||||
local v="$1" fallback="${2:-86400}" num unit
|
||||
|
||||
[[ $v =~ ^([0-9]+)([smhdw]?)$ ]] || { printf '%s\n' "$fallback"; return; }
|
||||
num="${BASH_REMATCH[1]}"
|
||||
unit="${BASH_REMATCH[2]}"
|
||||
|
||||
case "$unit" in
|
||||
s|'') printf '%s\n' "$num" ;;
|
||||
m) printf '%s\n' "$(( num * 60 ))" ;;
|
||||
h) printf '%s\n' "$(( num * 3600 ))" ;;
|
||||
d) printf '%s\n' "$(( num * 86400 ))" ;;
|
||||
w) printf '%s\n' "$(( num * 604800 ))" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Human-readable "x minutes ago" for a unix timestamp. Empty input yields the
|
||||
# translated "never".
|
||||
cau_time_ago() {
|
||||
local ts="$1" now delta
|
||||
|
||||
[[ $ts =~ ^[0-9]+$ ]] || { cau_msg "never"; printf '\n'; return; }
|
||||
|
||||
now="$(date +%s)"
|
||||
delta=$(( now - ts ))
|
||||
(( delta < 0 )) && delta=0
|
||||
|
||||
if (( delta < 60 )); then cau_msg "just now"
|
||||
elif (( delta < 3600 )); then cau_msg "%d minutes ago" "$(( delta / 60 ))"
|
||||
elif (( delta < 86400 )); then cau_msg "%d hours ago" "$(( delta / 3600 ))"
|
||||
else cau_msg "%d days ago" "$(( delta / 86400 ))"
|
||||
fi
|
||||
printf '\n'
|
||||
}
|
||||
Reference in new issue
Block a user