#!/bin/bash # # auria.sh - the main code of auria, an aur helper. # # Copyright 2017, 2018 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: # - bash # - pacman # - jq # - curl>=7.18.0: for --data-urlencode # - cmp # - grep # - sed # # 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: Fix update_all # `- Do not update if AUR version and local version match. # 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 errors and failures in return codes (mostly done). # TODO: Conflicts. # TODO: Find packages only providing a dep (e.g. commonist->java-environment) # `- apparently impossible via AUR RPC. # TODO: (print_localver) case pkg is only provided. # TODO: print_repover may print several results (e.g. parabola/linux-libre). # `- print_localver probably has the same issue. # TODO: Use inform/note/subinform properly. # TODO: Remove package from repo_deps, if it later gets added to the aur # lists. # TODO: Be consistent in the usage of test / [.] / [[.]]. # TODO: (info) parse properly (parse.c) # TODO: Prevent asking for password in case of a --needed with effect. # `- --needed should no longer be necessary, once update_all is fixed. # TODO: Handle failures of single packages in update_all. # `- Alternatively, offer a variable skip_updates[] or so. # TODO: Options, e.g. to only build or to selectively update. # `- Might be (partially) solved by passing args to makepkg. # TODO: Do not attempt to build an already built packages. Or else makepkg # unhappily fails. # `- Alternatively, use makepkg's -f option. # CONSIDER: Use makepkg_options=(--cleanbuild --syncdeps) as default. # 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. # CONSIDER: Remove repo_deps[]. # CONSIDER: Inform about what packages will be installed. # CONSIDER: Newlines. # CONSIDER: Differentiate dep and build-dep. # `- Option to remove build-deps after installation. # CONSIDER: Counter in install_list() # CONSIDER: Check for matching architecture before attempting to build. # Q?: When to set retstr to ''. # 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 pkgext=.pkg.tar.xz # Should be the same as PKGEXT in `makepkg.conf'. typeset arch="$(uname -m)" 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" shopt -s expand_aliases 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, install_list). function install_full { local pkg itype test -z "$1" && return 4 pkg="$1" if [[ "$2" == update ]] then itype=update inform "Start update procedure for ${pkg}." else itype=explicit inform "Start install procedure for ${pkg}." fi #inform "Fetching package information from AUR..." subinform "Resolving dependencies..." resolve_deps "$pkg" $itype '' || return $? echo make_pkgorder || return $? install_list } # Installs all packages in ${aur_pkgorder[@]}, in order. # interactive (<- present_files). function install_list { local pkgbase old_pkgbase_exists pkgver pkgrel epoch pkg_arch version \ package for pkg in "${aur_pkgorder[@]}" do pkgbase="${aur_pkgbase[$pkg]}" if [[ "${aur_itype[$pkg]}" == update ]] then inform "Update ${pkg}..." else inform "Install ${pkg}..." fi # Get sources and cd into $aur_root/$pkgbase. get_sources "$pkgbase" || return $? 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 # Build package. # makepkg's -i flag cannot handle split packages, hence installing has # to be done separately. makepkg --syncdeps "${makepkg_options[@]}" || return 2 echo # Get version and other data (version may have changed due to pkgver()). makepkg --printsrcinfo > "${tmp}/srcinfo" pkgver="$(grep -E '^\s*pkgver' "${tmp}/srcinfo" | sed -E 's/^.*=\s*//')" pkgrel="$(grep -E '^\s*pkgrel' "${tmp}/srcinfo" | sed -E 's/^.*=\s*//')" epoch="$( grep -E '^\s*epoch' "${tmp}/srcinfo" | sed -E 's/^.*=\s*//')" # Get the package's architecture. # If neither any nor $arch were supported, makepkg would've failed # (unless makepkg is configured to build for a different architecture # than $arch, which should not be the case). if grep -qE '^\s*arch\s*=\s*any$' < "${tmp}/srcinfo" then pkg_arch=any else pkg_arch="$arch" fi # Compose the package's version. if test -n "$epoch" then version="${epoch}:${pkgver}-${pkgrel}" else version="${pkgver}-${pkgrel}" fi # Install package. inform "Install built package..." package="${pkg}-${version}-${pkg_arch}${pkgext}" if [[ "${aur_itype[$pkg]}" == dep ]] then sudo pacman -U --asdeps "$package" || return 2 else sudo pacman -U --needed "$package" || return 2 fi echo done } # OLD. # $1 package: name # interactive (<- get_pkgbase, present_files). function install_single { local pkg pkgbase ver localver test -z "$1" && return 4 pkg="$1" #if [[ "$2" == update ]] #then #inform "Start update procedure for ${pkg}." #else #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" "$pkg" "${tmp}/json" explicit || 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 package: name [. pkgver-comparator . version string] # $3 json file (info): filename # $4 installation type: (dep|explicit|update) # $retstr result: numeric id {on success} # interactive {if several pkgbases match}. function get_pkgbase { retstr='' local pkg="$1" local pkgstr="$2" local json="$3" local itype=$4 local -i rcount="$(jq -er '.resultcount' "$json" || return 4)" local -i i if [[ $rcount -eq 0 ]] then if [[ "$itype" == dep ]] then error "Dependency ${pkgstr} could not be resolved." else error "[RPC] No result for ${pkg} found." fi return 2 elif [[ $rcount -gt 1 ]] then # Should never happen. 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 subinform "Pulling updates from AUR..." git pull || return 4 else cd "$git_dir" || return 4 subinform "Cloning sources from AUR..." git clone "https://aur.archlinux.org/${pkgbase}.git" "${pkgbase}" \ || return 4 cd "${pkgbase}" || return 4 fi echo 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) # $3 direct reverse dependency (caller): (name|) # interactive (<- get_pkgbase). function resolve_deps { local itype pkgstr pkg pkgbase pkgver rst rst_kind rst_ver existed new \ abovepkg pkgstr="$1" itype=$2 abovepkg="$3" # 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 and upon update). # This does also populate the $repo_deps array. if [[ "$itype" == dep ]] then depsearch_noaur "$pkg" "$pkgstr" && return 0 elif [[ "$itype" == update ]] then if depsearch_noaur "$pkg" "$pkgstr" then inform "$pkgstr exists in the regular repositories." ask n "Try to update from AUR anyways" || return 0 fi fi # Search the AUR. if [[ -n "${aur_itype["$pkg"]}" ]] then new=false pkgver="${aur_pkgver["$pkg"]}" else subinform " $pkgstr" 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 return 2 fi get_pkgbase "$pkg" "$pkgstr" "${tmp}/json" $itype || 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 # Add edges to the tree if the dep will be taken from the AUR. if test -n "$abovepkg" then aur_deps["$abovepkg"]+=" $pkg " aur_revdeps["$pkg"]+=" $abovepkg " 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) ) # Recurse. local dep for dep in "${deps[@]}" "${makedeps[@]}" do resolve_deps "$dep" dep "$pkg" || return $? done if test -z "${aur_deps["$pkg"]}" then aur_pkgorder+=("$pkg") fi fi } # Requires prepare_sources() to be run and its results unaltered. function make_pkgorder { local pkg if test -z "${aur_pkgorder[0]}" then error "Circular dependency found." return 2 fi ## Go up the reverse tree from bottom to top. local -i i=0 while test -n "${aur_pkgorder[$i]}" do pkg="${aur_pkgorder[$i]}" # Remove the resolved package off the dependency tree. # Note, that the reverse dependencies do not require removal. for revdep in ${aur_revdeps["$pkg"]} do aur_deps["$revdep"]="${aur_deps["$revdep"]/ $pkg /}" if test -z "${aur_deps["$revdep"]}" then aur_pkgorder+=("$revdep") fi done i+=1 done if test -n "${aur_deps["$pkg"]}" then error "Circular dependency found." return 2 fi } # OLD. function install_deptree { local pkg # Use $@ in order to be able to use shift. set "${aur_pkgorder[@]}" while [ $# -ne 0 ] do # Pop. pkg="$1"; shift # TODO: install $pkg # Remove the resolved package off the dependency tree. # Note, that the reverse dependencies do not require removal. for revdep in ${aur_revdeps["$pkg"]} do aur_deps["$revdep"]="${aur_deps["$revdep"]/ $pkg /}" if test -z "${aur_deps["$revdep"]}" then aur_pkgorder+=("$revdep") fi done done } # $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 # Search locally. qpacman -T "$pkgstr" && return 0 # Search in the repos. 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 -Rdd --print-format "%v" "$1" } # $1 package: name # prints repository version: version string function print_repover { pacman -Sdd --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 (<- resolve_deps, install_list). function update_all { local pkg inform "Start update of all AUR packages." for pkg in $(pacman -Qmq) do #qpacman -T "$pkg" && continue subinform "Resolving dependencies for ${pkg}..." resolve_deps "$pkg" update '' || return $? done echo inform "Flatten dependency tree to a list..." make_pkgorder || return $? echo install_list } # OLD. # interactive (<- update). function update_all_old { local pkg 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[]?" "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. # Note, that ^D is considered as a default answer. 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 # -l converts to lowercase 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 # $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 "$@"