Created
November 4, 2024 07:58
-
-
Save sminez/d6ba0f88dd1e5ec7bcbd901ae9203e52 to your computer and use it in GitHub Desktop.
A zsh AUR package manager
This file contains 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 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