aboutsummaryrefslogtreecommitdiff
path: root/auria.sh
diff options
context:
space:
mode:
Diffstat (limited to 'auria.sh')
-rwxr-xr-xauria.sh724
1 files changed, 724 insertions, 0 deletions
diff --git a/auria.sh b/auria.sh
new file mode 100755
index 0000000..c2cb441
--- /dev/null
+++ b/auria.sh
@@ -0,0 +1,724 @@
+#!/bin/bash
+#
+# auria.sh - the main code of auria, an aur helper.
+#
+# Copyright 2017, Einhard Leichtfuß
+#
+# This file is part of auria.
+#
+# auria is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# auria is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with auria. If not, see <http://www.gnu.org/licenses/>.
+#
+
+
+# deps:
+# - pacman
+# - jq
+# - curl>=7.18.0: for --data-urlencode
+# - cmp
+#
+# optdeps:
+# - ed: default editor
+#
+
+
+# PROBLEM: The AUR RPC interface considers depends_i686 and similar as
+# depends (or similar).
+# PROBLEM: The AUR RPC does not search for provides (e.g. dictd-foldoc).
+# PROBLEM/NOPROBLEM: Things may change while downloading things (e.g. deps).
+# TODO: search: append (group) [installed{ :verstring}] if applicable
+# - group not easily available;
+# - installed is probably time consuming - alpm could be used.
+# TODO: use guess_devel.
+# TODO: differentiate dep and build-dep.
+# TODO: differentiate errors and failures in return codes (mostly done).
+# TODO: conflicts.
+# TODO: find packages only providing a dep (e.g. commonist->java-environment)
+# TODO: (print_localver) case pkg is only provided.
+# TODO: print_repover may print several results (e.g. java-environment).
+# TODO: use inform/note/subinform properly.
+# TODO: remove package from repo_deps, if it later gets added to the aur
+# lists
+# TODO: modify and use parse for info.
+# TODO: Be consistent in the usage of test / [.] / [[.]].
+# CONSIDER: (repo_deps) array vs. (newline separated string)
+# CONSIDER: (repo_deps) (space vs. newline) separated string
+# Q?: When to set retstr to ''.
+# Q?: Is aur_itype[] needed?
+#
+# NOTE: (jq) `// empty' converts null to empty, which becomes "".
+
+
+# Default options (must be kept in sync with the distributed version of the
+# global configuration file).
+typeset global_conffile=/etc/auria_conf.sh
+typeset local_conffile="$HOME/.config/auria_conf.sh"
+typeset git_dir="$HOME/aur"
+typeset -a makepkg_options=()
+typeset color=false
+typeset editor=ed
+typeset diff_program=diff
+typeset yes_pkgs=()
+typeset -a devel_pkgs=()
+typeset -i curl_connect_timeout=5
+typeset -i curl_max_time=20
+
+test -n "$EDITOR" && editor="$EDITOR"
+
+# Source configuration files.
+test -f "$global_conffile" && . "$global_conffile"
+test -f "$local_conffile" && . "$local_conffile"
+
+alias parse=./parse
+
+typeset -a curl_args=(
+ --connect-timeout $curl_connect_timeout
+ --max-time $curl_max_time
+)
+
+alias qpacman='pacman > /dev/null 2>&1'
+
+typeset nocolor bviolet bwhite bgreen bred byellow
+typeset nbviolet nbwhite nbgreen nbred nbyellow
+if $color
+then
+ nocolor=$'\e[0m'
+ bviolet=$'\e[1;35m'
+ bwhite=$'\e[1;38m'
+ bgreen=$'\e[1;32m'
+ bred=$'\e[1;31m'
+ byellow=$'\e[1;33m'
+ bblue=$'\e[1;34m'
+ nbviolet="${nocolor}${bviolet}"
+ nbwhite="${nocolor}${bwhite}"
+ nbgreen="${nocolor}${bgreen}"
+ nbred="${nocolor}${bred}"
+ nbyellow="${nocolor}${byellow}"
+ nbblue="${nocolor}${bblue}"
+else
+ makepkg_options+=(--no-color)
+fi
+
+# return string of the last function returning a string.
+typeset retstr
+
+typeset -a repo_deps # dependencies in official repositories.
+typeset -A aur_deps # aur deps for each aur package (space sep.).
+typeset -A aur_revdeps # aur pkgs depending on an pkg, for each.
+typeset -A aur_itype # explicit / dep / update / ""
+typeset -A aur_pkgbase
+typeset -A aur_pkgver
+typeset -a aur_pkgorder # properly ordered array of to be installed pkgs.
+
+function reset_vars
+{
+ repo_deps=()
+ aur_deps=()
+ aur_revdeps=()
+ aur_itype=()
+ aur_pkgbase=()
+ aur_pkgver=()
+ aur_pkgorder=()
+}
+
+typeset tmp
+function cleanup
+{
+ test -n "$tmp" && test -d "$tmp" && rm -rf "$tmp"
+}
+trap cleanup EXIT
+
+function main
+{
+ prepare || return $?
+
+ case "$1" in
+ update)
+ update_all;;
+ install)
+ install_full "$2";;
+ search)
+ search "${@:2}";;
+ rpc)
+ rpc "${@:2}";;
+ *)
+ error "Functionality ${1} not implemented."
+ return 2
+ esac
+
+ local ret=$?
+ cleanup
+ return $?
+}
+
+function prepare
+{
+ if test -e "$git_dir"
+ then
+ if ! test -d "$git_dir"
+ then
+ error "${git_dir} is not a directory."
+ return 2
+ fi
+ else
+ if ! mkdir "$git_dir"
+ then
+ error "Failed to create ${git_dir}."
+ return 2
+ fi
+ fi
+
+ tmp="$(mktemp -d || return 4)"
+ #CARCH="$(uname -m)"
+}
+
+function update
+{
+ install_full "$1" update
+}
+
+# $1
+function install_full
+{
+ local pkg pkgbase update a ver localver itype
+
+ test -z "$1" && return 4
+ pkg="$1"
+
+ if [[ "$2" == update ]]
+ then
+ update=true
+ itype=update
+ #inform "Start update procedure for ${pkg}."
+ else
+ update=false
+ itype=explicit
+ #inform "Start install procedure for ${pkg}."
+ fi
+
+ #inform "Fetching package information from AUR..."
+ inform "Resolving dependencies..."
+ resolve_dependencies "$pkg" "$itype"
+ # TODO: Make order.
+
+ # TODO: Do this for each dep (in proper order).
+ {
+ pkgbase="${aur_pkgbase[$pkg]}"
+
+ # Get sources and cd into $aur_root/$pkgbase.
+ get_sources "$pkgbase" || return $?
+ local old_pkgbuild_exists=$retstr
+ }
+
+ if ! test -f PKGBUILD
+ then
+ error "PKGBUILD does not exist."
+ return 2
+ fi
+
+ if ! match "$pkg" "${yes_pkgs[@]}"
+ then
+ present_files $old_pkgbuild_exists
+ ask y "Continue" || return 0
+ fi
+
+ makepkg "${makepkg_options[@]}" -i || return 2
+}
+
+# $1
+function install_single
+{
+ local pkg pkgbase update a ver localver
+
+ test -z "$1" && return 4
+ pkg="$1"
+
+ if [[ "$2" == update ]]
+ then
+ update=true
+ #inform "Start update procedure for ${pkg}."
+ else
+ update=false
+ #inform "Start install procedure for ${pkg}."
+ fi
+
+ inform "Fetching package information from AUR..."
+ #inform "Resolving dependencies..."
+ subinform " ${pkg}"
+ rpc info "${tmp}/json" loud "$*" || return $?
+
+ get_pkgbase "$pkg" "${tmp}/json" || return $?
+ test -z "$retstr" && return 0; local -i i=$retstr
+ pkgbase="$(jq -er ".results[$i].PackageBase" "${tmp}/json" || return 4)"
+
+ get_sources "$pkgbase" || return $?
+ local old_pkgbuild_exists=$retstr
+
+
+ if ! test -f PKGBUILD
+ then
+ error "PKGBUILD does not exist."
+ return 2
+ fi
+
+ if ! match "$pkg" "${yes_pkgs[@]}"
+ then
+ present_files $old_pkgbuild_exists
+ ask y "Continue" || return 0
+ fi
+
+ makepkg "${makepkg_options[@]}" -i || return 2
+}
+
+# $1 pkgname: string
+# $2 json file: string
+# returns on success [$retstr] result_id: int
+function get_pkgbase
+{
+ retstr=''
+ local pkg="$1"
+ local json="$2"
+
+ local -i rcount="$(jq -er '.resultcount' "$json" || return 4)"
+ local -i i
+ if [[ $rcount -eq 0 ]]
+ then
+ error "[RPC] No result for ${pkg} found."
+ return 2
+ elif [[ $rcount -gt 1 ]]
+ then
+ inform "Several package bases match:"
+ i=0
+ while [[ $i -lt $rcount ]]
+ do
+ subinform " [$i] $(jq -er \
+ ".results[$i] | \"\\(.PackageBase) (\\(.Version))\"" "$json" \
+ || return 4)"
+ done
+
+ while true
+ do
+ ask_general "" "0-$((rcount-1))/n" "Which one do you want"
+
+ if [[ "$retstr" =~ ^[0-9]*$ ]]
+ then
+ (( $retstr >= 0 && $retstr < $rcount )) && break
+ elif [[ "${retstr,,}" =~ ^no?$ ]]
+ then
+ retstr=''
+ break
+ fi
+ done
+ else
+ retstr=0
+ fi
+}
+
+# $1 pkgbase: string
+# returns on success [$retstr] old_pkgbuild_exists: bool
+# note: Changes working directory to "${git_dir}/${pkgbase}" upon success.
+function get_sources
+{
+ retstr=''
+ local pkgbase="$1"
+
+ local old_pkgbuild_exists=false
+ if test -d "${git_dir}/${pkgbase}"
+ then
+ cd "${git_dir}/${pkgbase}" || return 4
+ if test -f PKGBUILD
+ then
+ cp PKGBUILD "${tmp}/PKGBUILD" || return 4
+ old_pkgbuild_exists=true
+ fi
+ inform "Pulling updates from AUR..."
+ git pull || return 4
+ else
+ cd "$git_dir" || return 4
+ inform "Cloning sources from AUR..."
+ git clone "https://aur.archlinux.org/${pkgbase}.git" "${pkgbase}" \
+ || return 4
+ cd "${pkgbase}" || return 4
+ fi
+
+ retstr=$old_pkgbuild_exists
+}
+
+# $1 old_pkgbuild_exists: bool
+# $2 pkgname: string
+# Requires $PWD = ${git_dir}/${pkgbase}
+# Requires ${tmp}/PKGBUILD if $old_pkbuild_exists
+function present_files
+{
+ local pkgname="$2"
+
+ if $1
+ then
+ if cmp "${tmp}/PKGBUILD" PKGBUILD
+ then
+ ask n "PKGBUILD did not change. View anyway" && "$editor" PKGBUILD
+ else
+ ask y "PKGBUILD has changed. View diff" \
+ && "$diff_program" "${tmp}/PKGBUILD" PKGBUILD
+ ask n "View the whole file" && "$editor" PKGBUILD
+ fi
+ else
+ ask y "View the PKGBUILD" && "$editor" PKGBUILD
+ fi
+
+ if test -f "${pkgname}.install"
+ then
+ ask_general "${pkgname}.install" '' "View other file"
+ else
+ ask_general '' '' "View other file"
+ fi
+ local file="$retval"
+
+ while test -n "$file"
+ do
+ "$editor" "$file"
+ file="$(ask_general '' '' "View other file")"
+ done
+}
+
+# Any call for explicit packages must be made before any call for
+# non-explicit ones.
+# $1 package string: string[+ comparator, version string]
+# $2 installation type: (dep|explicit|update)
+function resolve_deps
+{
+ local itype pkgdepstr pkg pkgbase pkgver rst rst_kind rst_ver existed new
+
+ pkgdepstr="$1"
+ itype="$2"
+
+ # Check for version restriction.
+ rst=( $(print_restriction "$pkgdepstr") )
+ pkg="${rst[0]}"
+ rst_kind="${rst[1]}"
+ rst_ver="${rst[2]}"
+
+ # Search locally and in the repos first (for deps).
+ # This does also populate the $repo_deps array.
+ [[ "$itype" == dep ]] && depsearch_noaur "$pkg" "$pkgdepstr" && return 0
+
+ # Search the AUR.
+ if [[ -n "${aur_itype["$pkg"]}" ]]
+ then
+ new=false
+ pkgver="${aur_pkgver["$pkg"]}"
+ else
+ new=true
+ rpc info "${tmp}/json" quiet "$pkg" || return $?
+ if [[ -n "$retstr" ]]
+ then
+ if [[ "$itype" == dep ]]
+ then
+ error "Dependency ${pkgdepstr} could not be resolved."
+ else
+ error "[RPC] ${retstr}"
+ fi
+ fi
+
+ get_pkgbase "$pkg" "${tmp}/json" || return $?
+ test -z "$retstr" && return 1; local -i i=$retstr
+ pkgbase="$(jq -er ".results[$i].PackageBase" "${tmp}/json" || return 4)"
+ aur_pkgbase["$pkg"]="$pkgbase"
+ pkgver="$(jq -er ".results[$i].Version" "${tmp}/json" || return 4)"
+ aur_pkgver["$pkg"]="$pkgver"
+
+ aur_itype["$pkg"]=$itype
+ fi
+
+ if ! cmp_ver "$pkgver" "$rst_ver" "$rst_kind"
+ then
+ error "Dependency ${pkgdepstr} could not be resolved."
+ return 2
+ fi
+
+ if $new
+ then
+ local deps makedeps
+ deps=( $(jq -r ".results[$i].Depends[]?" "${tmp}/json" \
+ || return 4) )
+ makedeps=( $(jq -r ".results[$i].MakeDepends[]?" "${tmp}/json" \
+ || return 4) )
+
+ aur_deps["$pkg"]="$(printf " %s " "${deps[@]}" "${makedeps[@]}")"
+
+ local dep
+ for dep in "${deps[@]}" "${makedeps[@]}"
+ do
+ echo $dep
+ aur_revdeps["$dep"]="$pkg"
+ resolve_deps "$dep" dep
+ done
+ fi
+}
+
+# $1 pkgname: string
+# $2 pkgdepstr: string [+ comparator + version string]
+function depsearch_noaur
+{
+ local pkg pkgdepstr
+ pkg="$1"
+ pkgdepstr="$2"
+
+ # In case of no dep restriction, look up in list first.
+ [[ "$pkg" == "$pkgdepstr" ]] \
+ && match "$pkg" "${repo_deps[@]}" && return 0
+
+ qpacman -T "$pkgdepstr" && return 0
+
+ qpacman -Sp "$pkgdepstr" \
+ && { match "$pkg" "${repo_deps[@]}" || repo_deps+=("$pkg"); } \
+ && return 0
+
+ return 1
+}
+
+# $1 ver1: version string
+# $2 ver2: version string
+# $3 comparator: (-gt|-lt|-ge|-le|-eq)
+function cmp_ver
+{
+ test -z "$3" && return 0
+ local -i res=$(vercmp "$1" "$2")
+ eval "[[ ${res} ${3} 0 ]]"
+ return $?
+}
+
+# $1 pkgname, possibly with restriction: string[(<|>|<=|>=|=)ver]
+# IDEA: consider to use sed instead.
+function print_restriction
+{
+ local v
+ v="${1##*<=}"; test "$v" != "$1" && printf "%s\n" "${1%<=*} -le ${v}" \
+ && return 0
+ v="${1##*<}"; test "$v" != "$1" && printf "%s\n" "${1%<*} -lt ${v}" \
+ && return 0
+ v="${1##*>=}"; test "$v" != "$1" && printf "%s\n" "${1%>=*} -ge ${v}" \
+ && return 0
+ v="${1##*>}"; test "$v" != "$1" && printf "%s\n" "${1%>*} -gt ${v}" \
+ && return 0
+ v="${1##*=}"; test "$v" != "$1" && printf "%s\n" "${1%=*} -eq ${v}" \
+ && return 0
+
+ printf "%s\n" "$1"
+}
+
+# $1 pkgname: string
+# TODO: needed?
+# TODO: case pkg is only provided.
+function print_localver
+{
+ pacman -R --print-format "%v" "$1"
+}
+
+# $1 pkgname: string
+function print_repover
+{
+ pacman -S --print-format "%v" "$1"
+}
+
+# $1 pkgname: string
+function guess_vcs
+{
+ [[ "$1" =~ -(git|svn|hg|bzr|cvs|fossil|darcs|rcs|arch|mtn)$ ]]
+}
+
+function update_all
+{
+ for pkg in "$(pacman -Qmq)"
+ do
+ update "$pkg"
+ done
+}
+
+function inform
+{
+ printf "${nbblue}::${nocolor} ${bwhite}%s${nocolor}\n" "$*" >&2
+}
+
+function subinform
+{
+ printf "%s\n" "$*"
+}
+
+function note
+{
+ printf "${nbwhite}%s${nocolor}\n" "$*" >&2
+}
+
+function warn
+{
+ printf "${nbyellow}warning:${nocolor} %s\n" "$*" >&2
+}
+
+function error
+{
+ printf "${nbred}error:${nocolor} %s\n" "$*" >&2
+}
+
+# $1 type: (info|search(:(name|name-desc|maintainer)),
+# $2 output-file: string
+# $3 sound: [quiet|loud]
+# ${@:4} args: (string)*
+# returns if ! $print_error [$retstr] error: [string]
+function rpc
+{
+ retstr=''
+ # According to the website (/rpc/), arg should be subsituted by arg[]
+ # if type is info.
+
+ local type search_by quiet args
+ type=${1%:*}
+ search_by=${1#*:}
+ file="$2"
+ test "$3" = quiet && quiet=true || quiet=false
+ args=( "${@:4}" )
+
+ local -a cargs
+ if [[ "$type" == info ]]
+ then
+ for arg in "${args[@]}"
+ do
+ cargs+=(--data-urlencode "arg[]=${arg}")
+ done
+ else
+ cargs=(--data "search_by=${search_by}" --data-urlencode "arg=${args[0]}")
+ fi
+
+ curl "${curl_args[@]}" --silent --get "${cargs[@]}" \
+ "https://aur.archlinux.org/rpc/?v=5&type=${type}" \
+ -o "$file" \
+ || { error "Failed to query AUR RPC interface."; return 2; }
+
+ if [[ "$(jq -er '.type' "$file" || return 4)" == error ]]
+ then
+ if $quiet
+ then
+ retstr="$(jq -er '.error' "$file")"
+ else
+ error "[RPC] $(jq -er '.error' "$file")"
+ return 2
+ fi
+ fi
+}
+
+function search
+{
+ rpc search:name-desc "${tmp}/json" loud "$*" || return $?
+
+ jq -er '.results[] | "\(.Name) \(.Version) \(.Description)"' \
+ "${tmp}/json" \
+ | parse 4 0 $($color && echo color)
+}
+
+function info
+{
+ rpc info "${tmp}/json" loud "$@" || return $?
+
+ local -a lines
+ lines=(
+ "Name" ".Name // empty"
+ "Package Base" ".PackageBase // empty"
+ "Version" ".Version // empty"
+ "Description" ".Description // empty"
+ "URL" ".URL // empty"
+ "Votes" ".NumVotes // empty"
+ "Popularity" ".Popularity // empty"
+ "Out Of Date" ".OutOfDate // empty"
+ "Maintainer" ".Maintainer // empty"
+ "Licenses" ".License[]?"
+ "Groups" ".Group // empty"
+ "Provides" ".Provides[]?"
+ "Depends On" ".Depends[]?"
+ "Make Deps" ".MakeDepends[]?"
+ "Optional Deps" ".OptDepends[]?"
+ "Conflicts With" ".Conflicts[]?"
+ "Replaces" ".Replaces[]?"
+ )
+
+ jq -er ".results[] | $(printf -- '"--%s:", %s,' "${lines[@]}") empty" \
+ "${tmp}/json"
+}
+
+# $1 default: y/n/x, $2 question: string
+function ask
+{
+ # -l auto-converts everything to lower case.
+ local -l default
+ default=$1
+
+ while true
+ do
+ if [[ "$default" == "y" ]]
+ then
+ printf "${nbblue}::${nocolor} ${bwhite}%s [Y/n]?${nocolor} " "$2"
+ elif [[ "$default" == "n" ]]
+ then
+ printf "${nbblue}::${nocolor} ${bwhite}%s [y/N]?${nocolor} " "$2"
+ else
+ printf "${nbblue}::${nocolor} ${bwhite}%s [y/n]?${nocolor} " "$2"
+ default="x"
+ fi
+
+ local -l reply
+ read reply
+
+ if [[ "$reply" =~ ^y(es)?$ ]]
+ then
+ return 0
+ elif [[ "$reply" =~ ^no?$ ]]
+ then
+ return 1
+ elif [[ "$default" == "x" || "$reply" != "" ]]
+ then
+ continue
+ elif [[ "$default" == "y" ]]
+ then
+ return 0
+ else # "$default" = "n"
+ return 1
+ fi
+ done
+}
+
+# $1 default answer: string, $2: options: string, $3 question: string
+# returns answer [$retstr]: string
+function ask_general
+{
+ if test -z "$2"
+ then
+ if test -z "$1"
+ then
+ set "$1" "..." "$3"
+ else
+ set "$1" "${1}/..." "$3"
+ fi
+ fi
+
+ local reply
+ printf "${nbblue}::${nocolor} ${bwhite}%s [%s]?${nocolor} " "$3" "$2"
+ read reply
+
+ test -n "$reply" && retstr="$reply" || retstr="$1"
+}
+
+function match
+{
+ printf "%s\n" "${@:2}" | grep -q "^${1}$"
+}
+
+#main "$@"