Created
August 30, 2026 08:53
-
-
Save jamespo/3bdc25b13c1710352f3e0a748b008731 to your computer and use it in GitHub Desktop.
yaycache - enhanced version of https://herbort.me/posts/automatically-cleaning-pacman-and-yay-cache-in-arch-linux/ #
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # | |
| # Clean the pacman and yay package caches. | |
| # Based on https://herbort.me/posts/automatically-cleaning-pacman-and-yay-cache-in-arch-linux/ | |
| # | |
| # Intended to run as root (needed to prune /var/cache/pacman/pkg), typically | |
| # from a systemd timer or pacman hook. Everything that touches the user's yay | |
| # cache is re-executed as that user, so root never walks, deletes, or follows | |
| # symlinks under a directory an unprivileged account controls. | |
| # | |
| # Override the target account with YAYCACHE_UID, or the cache location with | |
| # YAYCACHE_DIR. | |
| set -euo pipefail | |
| shopt -s nullglob | |
| export LC_ALL=C | |
| readonly PACCACHE=/usr/bin/paccache | |
| readonly ADMIN_UID="${YAYCACHE_UID:-1000}" | |
| warn() { printf '%s: %s\n' "${0##*/}" "$*" >&2; } | |
| die() { warn "$*"; exit 1; } | |
| ## pkgbase, pkgname and pkgver for every locally installed package. | |
| ## yay names its cache directories after pkgbase, which for split packages is | |
| ## not an installed pkgname, so pkgname alone is not enough to match them up. | |
| pkg_base_name_ver() { | |
| if command -v expac >/dev/null 2>&1; then | |
| expac -Q '%e %n %v' | |
| else | |
| awk ' | |
| /^%NAME%$/ { getline; n = $0 } | |
| /^%VERSION%$/ { getline; v = $0 } | |
| /^%BASE%$/ { getline; b = $0 } | |
| ENDFILE { if (n != "") print (b == "" ? n : b), n, v; n = v = b = "" } | |
| ' /var/lib/pacman/local/*/desc | |
| fi | |
| } | |
| clean_user_cache() { | |
| local cachedir="$1" | |
| [[ -d $cachedir ]] || { warn "no yay cache at $cachedir"; return 0; } | |
| ## Foreign (AUR) packages that are still installed, keyed by both pkgbase | |
| ## and pkgname, mapped to the plain pkgver (no epoch, no pkgrel). | |
| local -A foreign=() keep=() | |
| local name base ver | |
| while IFS= read -r name; do | |
| foreign["$name"]=1 | |
| done < <(pacman -Qqm || true) | |
| while read -r base name ver; do | |
| [[ -n ${foreign[$name]-} ]] || continue | |
| [[ -n $base && $base != '(null)' ]] || base="$name" | |
| ver="${ver%-*}" | |
| ver="${ver#*:}" | |
| keep["$base"]="$ver" | |
| keep["$name"]="$ver" | |
| done < <(pkg_base_name_ver || true) | |
| local -a stale=() | |
| local pkgdir pkgname pkgver f | |
| while IFS= read -r -d '' pkgdir; do | |
| pkgname="${pkgdir##*/}" | |
| ## Foreign package is gone: drop its cache entirely. | |
| if [[ -z ${keep[$pkgname]+set} ]]; then | |
| stale+=("$pkgdir") | |
| continue | |
| fi | |
| pkgver="${keep[$pkgname]}" | |
| ## Remove untracked files (e. g. source/build files) excepting package | |
| ## files and main source files for the installed version. Skipped for | |
| ## -git packages, whose checkout has no version to match against. | |
| if [[ $pkgname != *-git && -n $pkgver && -d $pkgdir/.git ]]; then | |
| while IFS= read -r -d '' f; do | |
| [[ $f == */ ]] && continue # untracked directory | |
| [[ $f == *.pkg.tar* ]] && continue # built package | |
| [[ $f == *"$pkgver"* ]] && continue # sources for installed version | |
| rm -f -- "$pkgdir/$f" | |
| ## --directory stops the walk at the top of an untracked directory, | |
| ## so makepkg's mode 0111 pkg/ is reported (and skipped) rather than | |
| ## descended into, which would warn about being unreadable. | |
| done < <(git -C "$pkgdir" ls-files --others --directory -z || true) | |
| fi | |
| rm -rf -- "$pkgdir/src" | |
| done < <(find "$cachedir" -mindepth 1 -maxdepth 1 -type d -print0) | |
| if (( ${#stale[@]} )); then | |
| rm -rf -- "${stale[@]}" | |
| fi | |
| ## Remove everything for uninstalled foreign packages, keep two latest | |
| ## versions for installed ones. | |
| local -a cargs=() | |
| while IFS= read -r -d '' pkgdir; do | |
| cargs+=(-c "$pkgdir") | |
| done < <(find "$cachedir" -mindepth 1 -maxdepth 1 -type d -print0) | |
| if (( ${#cargs[@]} )); then | |
| "$PACCACHE" -qruk0 "${cargs[@]}" || warn "paccache -k0 failed for $cachedir" | |
| "$PACCACHE" -qrk2 "${cargs[@]}" || warn "paccache -k2 failed for $cachedir" | |
| fi | |
| } | |
| main() { | |
| [[ -x $PACCACHE ]] || die "$PACCACHE not found; install pacman-contrib" | |
| local entry admin admin_home cachedir | |
| entry="$(getent passwd "$ADMIN_UID")" || die "no passwd entry for uid $ADMIN_UID" | |
| IFS=: read -r admin _ _ _ _ admin_home _ <<<"$entry" | |
| [[ -n $admin && -n $admin_home ]] || die "malformed passwd entry for uid $ADMIN_UID" | |
| cachedir="${YAYCACHE_DIR:-$admin_home/.cache/yay}" | |
| if (( EUID == ADMIN_UID )); then | |
| clean_user_cache "$cachedir" | |
| elif (( EUID == 0 )); then | |
| ## Drop to $admin for the cache work. The function bodies are piped in | |
| ## rather than re-execing $0, which $admin may not be able to read. | |
| { | |
| declare -p PACCACHE | |
| declare -f warn pkg_base_name_ver clean_user_cache | |
| ## cd / because $admin usually cannot read root's cwd (/root is | |
| ## 0700), which makes find fail to restore its initial directory. | |
| printf 'set -euo pipefail\nshopt -s nullglob\nexport LC_ALL=C\ncd /\nclean_user_cache "$1"\n' | |
| } | runuser -u "$admin" -- bash -s -- "$cachedir" \ | |
| || warn "cleaning $cachedir as $admin failed" | |
| else | |
| die "must run as root or as $admin (uid $ADMIN_UID)" | |
| fi | |
| if (( EUID != 0 )); then | |
| warn "not root; skipping /var/cache/pacman/pkg" | |
| return 0 | |
| fi | |
| ## Keep latest version for uninstalled native packages, two latest for | |
| ## installed ones. | |
| "$PACCACHE" -qruk1 || warn "paccache -k1 failed" | |
| "$PACCACHE" -qrk2 -c /var/cache/pacman/pkg || warn "paccache -k2 failed" | |
| } | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment