Skip to content

Instantly share code, notes, and snippets.

@sminez
Created November 4, 2024 07:58
Show Gist options
  • Save sminez/d6ba0f88dd1e5ec7bcbd901ae9203e52 to your computer and use it in GitHub Desktop.
Save sminez/d6ba0f88dd1e5ec7bcbd901ae9203e52 to your computer and use it in GitHub Desktop.
A zsh AUR package manager
#!/usr/bin/env zsh
# Arch User Repository (AUR) querying and package install without the need for
# a full blown binary that breaks with system upgrades.
#
# That said, zayoure is intended as a minimal helper script around searching,
# cloning and building aur packages. It does not support the full command set
# of pacman and it does not 'helpfully' do things on your behalf. It IS verbose
# in explaining why it failed so read the output, read the source and try not
# to brick your system ;)
#
# Optional dependencies:
# - bat -- syntax highlight PKGBUILD files when outputing to the terminal
#
# TODO:
# - Write zsh completion file
#
# AUR API docs are found here:
# https://wiki.archlinux.org/index.php/Aurweb_RPC_interface
#
# result format for searches:
# { ID: int, Name: str, PackageBaseID: int, PackageBase: str, Version: str,
# Description: str, URL: str, NumVotes: int, Popularity: int, OutOfDate: ?,
# Maintainer: str, FirstSubmitted: epoch, LastModified: epoch, URLPath: str }
# == vars and config ==
typeset -AH C
AUR_PACKAGE_ROOT='https://aur.archlinux.org/packages'
AUR_API_ROOT='https://aur.archlinux.org/rpc'
ZAYOURE_ROOT="$HOME/.aur"
CACHE_DIR="$ZAYOURE_ROOT/cache"
REPO_DIR="$ZAYOURE_ROOT/repos"
C=(
red '\033[1;31m' green '\033[1;32m' yellow '\033[1;33m' blue '\033[1;34m'
purple '\033[1;35m' cyan '\033[1;36m' white '\033[1;37m'
)
C_HEADING="$C[purple]"
C_PACMAN="$C[blue]"
C_ERROR="$C[red]"
NC='\033[0m'
# == helper functions ==
function usage {
cat <<EOF
usage: zayoure <option> [package]
options:
-S download, build & install a package by name from AUR
-Ss search for packages by name over the AUR rpc API
-Sw clone repository files from AUR but don't install
-Syu check for new package versions in AUR & update installed packages
-Q list locally installed packages and their versions
-Qi show the PKGBUILD for a given installed package
-R remove a package and delete local files
-o open AUR page for a package in the browser
EOF
}
function heading { echo -e "${C_HEADING}:: $C[white]$1$NC" }
function error_and_exit { echo -e "${C_ERROR}error:$NC $1" && exit 1 }
function require_package_arg { [ -z "$1" ] && error_and_exit "no package specified" }
function continue_or_exit {
local confirm prompt=${1:-"continue?"}
echo; heading "$prompt [Y/n]"
read -q confirm; echo # read -q doesn't drop to the next line
[ "$confirm" = "y" ] || exit 0
}
function require_external {
for prog in $*; do
if ! [ -x "$(command -v $prog)" ]; then
error_and_exit "'$prog' is required for zayoure to run"
fi
done
}
function ensure_zayoure_dirs {
if ! [ -d "$REPO_DIR" ]; then
heading "initialising source download directory at '$REPO_DIR'"
mkdir -p "$REPO_DIR"
fi
if ! [ -d "$CACHE_DIR" ]; then
heading "initialising cache directory at '$CACHE_DIR'"
mkdir -p "$CACHE_DIR"
fi
}
function print_package_summary {
local name=$1 version=$2 description=$3 votes=$4 votestr
# we only have vote info if we are pulling results from the web
[ -z "$votes" ] && votestr="" || votestr=" [$votes]"
echo -e "$C[red]aur/$C[white]$name $C[green]$version$NC$votestr"
echo -e "$(echo $description | fmt -w 76 | sed 's/^/ /g')\n"
}
function show_pkgbuild {
local package=$1 pkgbuild=$REPO_DIR/$1/PKGBUILD
require_package_arg $package
[ -x "$(command -v bat)" ] && bat --pager=never "$pkgbuild" || cat "$pkgbuild"
}
function summary_from_pkgbuild {
local name version description pkgbuild package=$1
require_package_arg $package
pkgbuild="$REPO_DIR/$1/PKGBUILD"
name="$(grep '^pkgname' $pkgbuild | cut -d'=' -f2 | xargs)"
version="$(grep '^pkgver' $pkgbuild | cut -d'=' -f2 | xargs)"
description="$(grep '^pkgdesc' $pkgbuild | cut -d'=' -f2 | xargs)"
print_package_summary $name $version $description
}
# == user facing functionality ==
# TODO: include created/last updated to get a feel for staleness?
function search {
local package=$1 results
require_package_arg $package
results=$(curl -s "$AUR_API_ROOT/?v=5&type=search&by=name&arg=$package" |
jq -r '.results | map([.Name, .Version, .Description, .NumVotes]) | .[] | @sh')
# NOTE: zsh parameter expansion magic:
# ${(f)var} -> split input on newlines
# ${(z)var} -> split input into distinct words
# ${(Q)var} -> remove one layer of quoting
for res in ${(f)results}; do
print_package_summary ${(Q)${(z)res}}
done
}
function remove {
local package=$1 dir="$REPO_DIR/$1"
require_package_arg $package
[ -d "$dir" ] || error_and_exit "'$package' is not currently installed"
heading "removing '$package' from local packages"
rm -rf "$dir"
heading "done"
}
function try_open_in_browser {
local package=$1 resp
require_package_arg $package
resp=$(curl -s -I "$AUR_PACKAGE_ROOT/$1")
if [[ "$resp" =~ '^curl: (6).*' ]];then
error_and_exit "unable to curl the aur: check your network connection"
fi
if [ "$(echo "$resp" | awk '/HTTP/ { print $2 }')" != "404" ]; then
xdg-open "$AUR_PACKAGE_ROOT/$package" &
else
error_and_exit "package '$package' was not found"
fi
}
function list_installed {
local package=$1
if [ -z "$package" ]; then
for pkg in $(ls "$REPO_DIR"); do
summary_from_pkgbuild "$pkg"
done
else
[ -d "$REPO_DIR/$package" ] || error_and_exit "package '$package' was not found"
summary_from_pkgbuild "$package"
fi
}
function clone_package {
local package=$1
require_package_arg $package
[ -d "$REPO_DIR/$package" ] && error_and_exit "'$package' directory already exists"
heading "cloning '$package' from the AUR..."
cd "$REPO_DIR" && git clone "https://aur.archlinux.org/$package.git"
}
function sync {
local package=$1 confirm
require_package_arg $package
if [ -d "$REPO_DIR/$package" ]; then
heading "using current local version of '$package' for install"
summary_from_pkgbuild "$package"
echo "use 'zayoure -Sy $package' to update your local version if needed"
else
clone_package "$package" || error_and_exit "failed to clone '$package'"
fi
show_pkgbuild "$package"
heading "PKGBUILD that will be used to install '$package'"
continue_or_exit
heading "the following flags will be passed to 'makepkg' when building this package"
echo -e "please review the them and make sure both that you understand what each flag"
echo -e "does and that you are happy to proceed\n"
echo -e " -C force a clean build (remove \$srcdir before build)"
echo -e " -c clean up leftover workfiles after the build"
echo -e " -f override pre-existing build of this package"
echo -e " -i install after successful build using ${C_PACMAN}pacman$NC"
echo -e " -s install missing dependencies using ${C_PACMAN}pacman$NC during the build"
echo -e " -r clean up build deps brought in by -s after install"
echo -e " --needed tell ${C_PACMAN}pacman$NC not to reinstall this package if it is up to date"
echo -e " --asdeps tell ${C_PACMAN}pacman$NC to mark the package as non-explicitly installed\n"
heading "confirming $C[yellow]y$C[white] will invoke ${C_PACMAN}makepkg"
continue_or_exit
cd "$REPO_DIR/$package" && makepkg -Ccfisr --needed --asdeps
}
function fetch {
local package=$1
if [ -z "$package" ]; then
cd "$REPO_DIR"
for d in $(ls -d *(/)); do
heading "fetching latest for $d..."
cd "$d" && git fetch
cd ..
done
else
heading "fetching latest for '$package'..."
cd "$REPO_DIR/$package" && git fetch
fi
}
function update {
local package=$1
if [ -z "$package" ]; then
continue_or_exit "update all local local packages with latest local changes?"
cd "$REPO_DIR"
for d in $(ls -d *(/)); do
cd "$d" && \
git checkout $(git rev-parse --abbrev-ref HEAD) && \
makepkg -Ccfisr --noconfirm --needed --asdeps
cd ..
done
else
continue_or_exit "update '$package' with latest local changes?"
cd "$package" && \
git checkout $(git rev-parse --abbrev-ref HEAD) && \
makepkg -Ccfisr --noconfirm --needed --asdeps
fi
}
# == main ==
# First things first: this is a shell script that clones things our of the aur.
# It should NEVER be run as root on your system so die if we detect we are
# running as root and slap the wrist of the user.
if [[ $EUID -eq 0 ]]; then
echo -e "${C_ERROR}error:$NC running as root"
echo "zayoure can not be run as root. If you want to do things with the aur as"
echo "root then use the arch build system manually and make sure you know what"
echo "you are doing."
echo "zayoure is a shell script: it is not responsible if you brick your system."
exit 1
fi
# make sure we have the minimal subset of external programs to function
require_external makepkg pacman curl fmt git jq grep cat xdg-open
# init our cache directories if they don't currently exist
ensure_zayoure_dirs
# grab user input: note that we won't always have a package name but we verify
# this lazily using 'require_package_arg' in functions that can not proceed
# without one. In some cases, our behaviour switches to 'for all packages' if
# the user has not provided a package name to stay in keeping with pacman.
flag="$1"
package="$2"
# we should now be good to go, so parse the flag and do some stuff
case "$flag" in
-Syu) fetch $package && update $package;;
-Ss) search $package;;
-Sw) clone_package $package;;
-Su) update $package;;
-Sy) fetch $package;;
-S) sync $package;;
-Qi) show_pkgbuild $package;;
-Q) list_installed $package;;
-R) remove $package;;
-o) try_open_in_browser $package;;
*) usage;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment