#!/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 / [.] / [[.]]. # TODO: (ask:MARK 1) Handle "" and . # TODO: (info) parse properly (parse.c) # CONSIDER: (repo_deps) array vs. (newline separated string) # CONSIDER: (repo_deps) (space vs. newline) separated string # CONSIDER: (print_localver,print_repover) removal # CONSIDER: Use `local var=value' at beginning of functions. # Q?: When to set retstr to ''. # Q?: Is aur_itype[] needed? # IDEA: (print_restriction) consider to use sed. # # 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. # note: Only for debugging purposes. 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) # For debugging. 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)" } # $1 package: name [. pkgver-comparator . version string] # interactive (<- install_full). function update { install_full "$1" update } # $1 package: name [. pkgver-comparator . version string] # interactive (<- resolve_deps, present_files). 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_deps "$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 package: name # interactive (<- get_pkgbase, present_files). 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 package: name # $2 json file: filename # $retstr result: numeric id {on success} # interactive. 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: name # $retstr old_pkgbuild_exists: boolean {on success} # action: cd ${git_dir}/${pkgbase} {on 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: boolean # $2 package: name # requires: $PWD == ${git_dir}/${pkgbase} # requires: ${tmp}/PKGBUILD (if $old_pkbuild_exists) # interactive. function present_files { local old_pkgbuild_exists pkgname old_pkgbuild_exists=$1 pkgname="$2" if $old_pkgbuild_exists 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 } # note: Any call for explicit packages must be made before any call for # non-explicit ones. # $1 package: name [. pkgver-comparator . version string] # $2 installation type: (dep|explicit|update) # interactive (<- get_pkgbase). function resolve_deps { local itype pkgstr pkg pkgbase pkgver rst rst_kind rst_ver existed new pkgstr="$1" itype="$2" # Check for version restriction. rst=( $(print_restriction "$pkgstr") ) 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" "$pkgstr" && 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 ${pkgstr} 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 ${pkgstr} 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 package: name # $2 package: name [. pkgver-comparator . version string] function depsearch_noaur { local pkg pkgstr pkg="$1" pkgstr="$2" # In case of no dep restriction, look up in list first. [[ "$pkg" == "$pkgstr" ]] && match "$pkg" "${repo_deps[@]}" && return 0 qpacman -T "$pkgstr" && return 0 qpacman -Sp "$pkgstr" \ && { match "$pkg" "${repo_deps[@]}" || repo_deps+=("$pkg"); } \ && return 0 return 1 } # $1 version 1: [version string] {required if -n $comparator} # $2 version 2: [version string] {required if -n $comparator} # $3 comparator: [(-gt|-lt|-ge|-le|-eq)] # returns result: (0|1) function cmp_ver { test -z "$3" && return 0 local -i res=$(vercmp "$1" "$2") eval "[[ ${res} ${3} 0 ]]" return $? } # $1 package: name [. pkgver-comparator . version string] # prints package and restriction: name + (-gt|-lt|-ge|-le|-eq) # + version string 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 package: name # prints local version: version string function print_localver { pacman -R --print-format "%v" "$1" } # $1 package: name # prints repository version: version string function print_repover { pacman -S --print-format "%v" "$1" } # $1 package: name # returns guess: (0|1) function guess_vcs { [[ "$1" =~ -(git|svn|hg|bzr|cvs|fossil|darcs|rcs|arch|mtn)$ ]] } # interactive (<- update). 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 query type: (info | search:(name|name-desc|maintainer)), # $2 output file: filename # $3 sound: (quiet|loud) # ${@:4} args: (string)+ # $retstr error: string {on failure if $quiet} function rpc { retstr='' # According to the website (/rpc/), arg should be subsituted by arg[] # if the query type is info. local qtype search_by quiet args qtype=${1%:*} search_by=${1#*:} file="$2" test "$3" = quiet && quiet=true || quiet=false args=( "${@:4}" ) local -a cargs if [[ "$qtype" == 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=${qtype}" \ -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 } # $* match conditions: (string)+ # prints search results: formatted string function search { rpc search:name-desc "${tmp}/json" loud "$*" || return $? jq -er '.results[] | "\(.Name) \(.Version) \(.Description)"' \ "${tmp}/json" \ | parse 4 0 $($color && echo color) } # $* packages: (string)+ # prints info results: formatted string 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 answer: (y|n|x) # $2 question: string # returns yes / no: (0|1) # interactive. function ask { # -l auto-converts everything to lower case. local -l default local question default=$1 question="$2" while true do if [[ "$default" == "y" ]] then printf "${nbblue}::${nocolor} ${bwhite}%s [Y/n]?${nocolor} " \ "$question" elif [[ "$default" == "n" ]] then printf "${nbblue}::${nocolor} ${bwhite}%s [y/N]?${nocolor} " \ "$question" else printf "${nbblue}::${nocolor} ${bwhite}%s [y/n]?${nocolor} " \ "$question" default="x" fi local -l reply read reply # MARK 1 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 # $retstr answer: string # interactive. function ask_general { retstr='' local default options question default="$1" options="$2" question="$3" if test -z "$opts" then if test -z "$default" then options='...' else options="${default}/..." fi fi local reply printf "${nbblue}::${nocolor} ${bwhite}%s [%s]?${nocolor} " "$question" \ "$options" read reply test -n "$reply" && retstr="$reply" || retstr="$default" } # $1 needle: string # ${@:2} haystack: (string)* # returns yes / no: (0|1) function match { printf "%s\n" "${@:2}" | grep -q "^${1}$" } #main "$@"