#!/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 "$@"