From 825b5343a74848bc9b0d85723b6e28695090ce1d Mon Sep 17 00:00:00 2001 From: Einhard Leichtfuß Date: Sun, 22 Oct 2017 00:16:28 +0200 Subject: Initial commit --- auria.sh | 724 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 724 insertions(+) create mode 100755 auria.sh (limited to 'auria.sh') 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 . +# + + +# 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 "$@" -- cgit v1.2.3