#!/usr/bin/env bash set -euo pipefail shopt -s nullglob repoDir="/var/www/packages.stormux.org" aurRpcUrl="https://aur.archlinux.org/rpc/v5/info" exclude=("gzdoom") require_cmd() { local cmd="$1" if ! command -v "$cmd" >/dev/null 2>&1; then printf 'Required command not found: %s\n' "$cmd" >&2 exit 1 fi } is_excluded() { local packageName="$1" local excludedPackage for excludedPackage in "${exclude[@]}"; do if [[ "$excludedPackage" == "$packageName" ]]; then return 0 fi done return 1 } record_local_package_version() { local packageName="$1" local packageVersion="$2" local packageMapName="$3" local -n packageMapRef="$packageMapName" local existingVersion="${packageMapRef[$packageName]:-}" if [[ -z "$existingVersion" ]] || (( $(vercmp "$packageVersion" "$existingVersion") > 0 )); then packageMapRef["$packageName"]="$packageVersion" fi } read_package_metadata() { local packageFile="$1" pacman -Qip "$packageFile" | parse_pacman_info } parse_pacman_info() { awk ' /^Name[[:space:]]*:/ { packageName=$0 sub(/^Name[[:space:]]*:[[:space:]]*/, "", packageName) } /^Version[[:space:]]*:/ { packageVersion=$0 sub(/^Version[[:space:]]*:[[:space:]]*/, "", packageVersion) } END { if (packageName == "" || packageVersion == "") { exit 1 } printf "%s\t%s\n", packageName, packageVersion } ' } collect_local_packages() { local packageMapName="$1" # shellcheck disable=SC2178 local -n packageMapRef="$packageMapName" local archDir packageFile metadata packageName packageVersion local -a archDirs=("$repoDir/x86_64" "$repoDir/aarch64") for archDir in "${archDirs[@]}"; do if [[ ! -d "$archDir" ]]; then continue fi for packageFile in "$archDir"/*.pkg.tar.zst "$archDir"/*.pkg.tar.xz; do metadata="$(read_package_metadata "$packageFile")" || { printf 'Unable to read package metadata: %s\n' "$packageFile" >&2 exit 1 } packageName="${metadata%%$'\t'*}" packageVersion="${metadata#*$'\t'}" record_local_package_version "$packageName" "$packageVersion" "$packageMapName" done done } extract_aur_version_from_json() { local packageName="$1" jq -r --arg packageName "$packageName" ' .results[] | select(.Name == $packageName) | .Version ' | head -n1 } fetch_aur_version() { local packageName="$1" curl -fsS --get \ --data-urlencode "arg[]=${packageName}" \ "$aurRpcUrl" | extract_aur_version_from_json "$packageName" } print_outdated_packages() { local packageMapName="$1" # shellcheck disable=SC2178 local -n packageMapRef="$packageMapName" local packageName localVersion aurVersion local -a packageNames=() mapfile -t packageNames < <(printf '%s\n' "${!packageMapRef[@]}" | sort) for packageName in "${packageNames[@]}"; do if is_excluded "$packageName"; then continue fi localVersion="${packageMapRef[$packageName]}" aurVersion="$(fetch_aur_version "$packageName" || true)" if [[ -z "$aurVersion" ]]; then continue fi if (( $(vercmp "$localVersion" "$aurVersion") < 0 )); then printf '%s %s\n' "$packageName" "$aurVersion" fi done } assert_equals() { local expected="$1" local actual="$2" local message="$3" if [[ "$expected" != "$actual" ]]; then printf 'FAIL: %s\nExpected: %s\nActual: %s\n' "$message" "$expected" "$actual" >&2 exit 1 fi } assert_success() { local message="$1" shift if ! "$@"; then printf 'FAIL: %s\n' "$message" >&2 exit 1 fi } assert_failure() { local message="$1" shift if "$@"; then printf 'FAIL: %s\n' "$message" >&2 exit 1 fi } main() { if [[ "${1:-}" == "--self-test" ]]; then run_self_tests return fi require_cmd "curl" require_cmd "jq" require_cmd "pacman" require_cmd "vercmp" if [[ ! -d "$repoDir" ]]; then printf 'Repo dir does not exist: %s\n' "$repoDir" >&2 exit 1 fi # shellcheck disable=SC2034 declare -A localPackages=() collect_local_packages localPackages print_outdated_packages localPackages } run_self_tests() { declare -A packageMap=() local exactMatchJson noMatchJson local extractedVersion="" local pacmanInfo="" assert_success "excluded package should match" is_excluded "gzdoom" assert_failure "non-excluded package should not match" is_excluded "fenrir" record_local_package_version "fenrir" "1:2026.01.20-1" packageMap record_local_package_version "fenrir" "1:2026.01.28-1" packageMap record_local_package_version "fenrir" "1:2026.01.10-1" packageMap assert_equals "1:2026.01.28-1" "${packageMap[fenrir]}" "newest local version should win" exactMatchJson='{"results":[{"Name":"fenrir-git","Version":"1:r3322.4672592d-1"},{"Name":"fenrir","Version":"1:2026.01.28-1"}]}' extractedVersion="$(printf '%s\n' "$exactMatchJson" | extract_aur_version_from_json "fenrir")" assert_equals "1:2026.01.28-1" "$extractedVersion" "exact package name should be selected from AUR JSON" noMatchJson='{"results":[{"Name":"fenrir-git","Version":"1:r3322.4672592d-1"}]}' extractedVersion="$(printf '%s\n' "$noMatchJson" | extract_aur_version_from_json "fenrir")" assert_equals "" "$extractedVersion" "missing exact AUR match should stay empty" pacmanInfo='Name : fenrir Version : 1:2026.01.28-1 Description : A user space console screen reader written in python3' extractedVersion="$(printf '%s\n' "$pacmanInfo" | parse_pacman_info)" assert_equals $'fenrir\t1:2026.01.28-1' "$extractedVersion" "pacman metadata parsing should return name and version" if (( $(vercmp "1:2026.01.20-1" "1:2026.01.28-1") >= 0 )); then printf 'FAIL: older local version should compare lower than AUR version\n' >&2 exit 1 fi printf 'Self-test passed\n' } main "$@"