#!/bin/bash
# aur-sync - download and build AUR packages automatically
[[ -v AUR_DEBUG ]] && set -o xtrace
set -o errexit
argv0=sync
XDG_CACHE_HOME=${XDG_CACHE_HOME:-$HOME/.cache}
XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config}
XDG_DATA_HOME=${XDG_DATA_HOME:-$HOME/.local/share}
XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/run/user/$UID}
AURDEST=${AURDEST:-$XDG_CACHE_HOME/aurutils/$argv0}
PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'

# default arguments
build_args=(--clean --syncdeps)
repo_args=()
log_fmt='diff'

# default options (enabled)
build=1 chkver_depth=2 download=1 view=1 provides=1 graph=1

# default options (disabled)
rotate=0 update=0 repo_targets=0

lib32() {
    awk -v arch="$(uname -m)" '{
        if(arch == "i686") {
            gsub(/^lib32-/,"")
            gsub(/^gcc-multilib$/,"")
        }
        print
    }'
}

# files: $1 pkgname\tpkgbase $2 pkgname (order by $2)
select_pkgbase() {
    awk 'NR == FNR {
        map[$1] = $2
        next
    }
    $1 in map {
        base = map[$1]

        # only print pkgbase on first occurrence
        if (base in seen) {
            next
        } else {
            print base
            seen[base]
        }
    }' "$@"
}

# fields: $1 pkgname, $2 depends[<>=]
tr_ver() {
    awk -F'[<>=]' '{print $1}'
}

complement() {
    # empty set should not return 1
    grep -Fxvf "$@" || return $(( $? - 1 ))
}

trap_exit() {
    if [[ ! -v AUR_DEBUG ]]; then
        rm -rf -- "$tmp" "$tmp_view"
    else
        printf >&2 'AUR_DEBUG: %s: temporary files at %s\n' "$argv0" "$tmp"
        printf >&2 'AUR_DEBUG: %s: temporary files at %s\n' "$argv0" "$tmp_view"
    fi
}

usage() {
    plain >&2 'usage: %s [-d repo] [--root path] [-cdfoSu] pkgname...' "$argv0"
    exit 1
}

source /usr/share/makepkg/util/util.sh
source /usr/share/makepkg/util/message.sh
source /usr/share/makepkg/util/parseopts.sh

if [[ ! -v NO_COLOR ]] && [[ ! -v AUR_DEBUG ]]; then
    [[ -t 2 ]] && colorize
fi

# mollyguard for makepkg
if (( UID == 0 )) && [[ ! -v AUR_ASROOT ]]; then
    warning 'aur-%s is not meant to be run as root.' "$argv0"
    warning 'To proceed anyway, set the %s variable.' 'AUR_ASROOT'
    exit 1
fi

opt_short='d:D:AcfLnorRSTuv'
opt_long=('bind:' 'bind-rw:' 'database:' 'repo:' 'directory:' 'ignore:' 'root:'
          'makepkg-conf:' 'pacman-conf:' 'chroot' 'continue' 'force'
          'ignore-arch' 'log' 'no-confirm' 'no-ver' 'no-graph' 'no-ver-argv'
          'no-view' 'no-provides' 'no-build' 'rm-deps' 'sign' 'temp' 'upgrades'
          'pkgver' 'rebuild' 'rebuild-tree' 'rebuild-all' 'ignore-file:'
          'remove' 'provides-from:' 'new' 'prevent-downgrade' 'verify'
          'results:' 'makepkg-args:' 'format:' 'config:')
opt_hidden=('dump-options' 'allan' 'ignorearch' 'ignorefile:' 'noconfirm'
            'nover' 'nograph' 'nover-argv' 'noview' 'noprovides' 'nobuild'
            'rebuildall' 'rebuildtree' 'rmdeps' 'gpg-sign' 'margs:')

if ! parseopts "$opt_short" "${opt_long[@]}" "${opt_hidden[@]}" -- "$@"; then
    usage
fi
set -- "${OPTRET[@]}"

unset pkg pkg_i repo repo_p ignore_file
while true; do
    case "$1" in
        # sync options
        --allan)
            rotate=1 ;;
        --continue)
            download=0 ;;
        --format)
            shift; log_fmt=$1 ;;
        --ignore)
            shift; IFS=, read -a pkg -r <<< "$1"
            pkg_i+=("${pkg[@]}") ;;
        --ignorefile|--ignore-file)
            shift; ignore_file=$1 ;;
        -o|--nobuild|--no-build)
            build=0 ;;
        --nograph|--no-graph)
            graph=0 ;;
        --nover|--no-ver)
            chkver_depth=0 ;;
        --nover-argv|--no-ver-argv)
            chkver_depth=1 ;;
        --noview|--no-view)
            view=0 ;;
        --noprovides|--no-provides)
            provides=0 ;;
        --provides-from)
            shift; IFS=, read -a repo -r <<< "$1"
            repo_p+=("${repo[@]}") ;;
        --rebuild)
            build_args+=(-f); chkver_depth=1 ;;
        --rebuildtree|--rebuild-tree)
            build_args+=(-f); chkver_depth=0 ;;
        --rebuildall|--rebuild-all)
            build_args+=(-f); chkver_depth=0; repo_targets=1 ;;
        -u|--upgrades)
            update=1 ;;
        # database options
        -d|--database|--repo)
            shift; repo_args+=(-d "$1") ;;
        --root)
            shift; repo_args+=(-r "$1") ;;
        # build options
        -c|--chroot)
            build_args+=(--chroot) ;;
        -f|--force)
            build_args+=(--force) ;;
        --config)
            shift; build_args+=(--config "$1")
            repo_args+=(--config "$1") ;;
        --makepkg-args|--margs)
            shift; build_args+=(--margs "$1") ;;
        --makepkg-conf)
            shift; build_args+=(--makepkg-conf "$1") ;;
        --pacman-conf)
            shift; build_args+=(--pacman-conf "$1") ;;
        --pkgver)
            build_args+=(--pkgver) ;;
        --results)
            shift; build_args+=(--results "$1") ;;
        -S|--sign|--gpg-sign)
            build_args+=(--sign) ;;
        # build options (devtools)
        -D|--directory)
            shift; build_args+=(--directory "$1") ;;
        --bind)
            shift; build_args+=(--bind "$1") ;;
        --bind-rw)
            shift; build_args+=(--bind-rw "$1") ;;
        -T|--temp)
            build_args+=(-T) ;;
        # build options (makepkg)
        -A|--ignorearch|--ignore-arch)
            build_args+=(--ignorearch) ;;
        -L|--log)
            build_args+=(--log) ;;
        -n|--noconfirm|--no-confirm)
            build_args+=(--noconfirm) ;;
        -r|--rmdeps|--rm-deps)
            build_args+=(--rmdeps) ;;
        # build options (repo-add)
        -R|--remove)
            build_args+=(--remove) ;;
        -v|--verify)
            build_args+=(--verify) ;;
        --prevent-downgrade)
            build_args+=(--prevent-downgrade) ;;
        --new)
            build_args+=(--new) ;;
        # other options
        --dump-options)
            printf -- '--%s\n' "${opt_long[@]}" ${AUR_DEBUG+"${opt_hidden[@]}"}
            printf -- '%s' "${opt_short}" | sed 's/.:\?/-&\n/g'
            exit ;;
        --) shift; break ;;
    esac
    shift
done

# shellcheck disable=SC2174
mkdir -pm 0700 "${TMPDIR:-/tmp}/aurutils-$UID"
tmp=$(mktemp -d --tmpdir "aurutils-$UID/$argv0.XXXXXXXX")
tmp_view=$(mktemp -d --tmpdir "aurutils-$UID/view.XXXXXXX")
trap 'trap_exit' EXIT

# Directory for git checksums (revisions viewed by the user, #379)
view_db=$XDG_DATA_HOME/aurutils/view
mkdir -p "$view_db"

# Default to showing PKGBUILD first in patch. (#399)
orderfile=$XDG_CONFIG_HOME/aurutils/$argv0/orderfile
mkdir -p "${orderfile%/*}"

if [[ ! -s $orderfile ]]; then
    printf 'PKGBUILD\n' > "$orderfile"
fi

# Append contents of ignore file
if [[ -f ${ignore_file=$XDG_CONFIG_HOME/aurutils/sync/ignore} ]]; then
    while IFS='#' read -r i _; do
        [[ $i ]] && pkg_i+=("$i")
    done < "$ignore_file"
fi

if (( rotate )); then
    if { hash rot13 && target=$(aur pkglist | shuf -n 1); } 2>/dev/null; then
        exec bash -c "{ aur \"$argv0\" -c \"$target\" && repo-elephant | rot13; } 2>&1 | rot13"
    else
        echo '?'; exit 16 # EBUSY
    fi
fi

if ! (( $# + update + repo_targets )); then
    error '%s: no targets specified' "$argv0"
    exit 1
fi

mkdir -p "$AURDEST"
cd_safe "$tmp"

# db_info: $1 pkgname $2 pkgver
aur repo "${repo_args[@]}" --list --status-file=db >db_info

# Retrieve path to local repo (#448)
{ IFS=: read -r _ db_name
  IFS=: read -r _ db_root
} <db

if [[ -w $db_root/$db_name.db ]]; then
    msg >&2 'Using [%s] repository' "$db_name"
else
    error '%s: %s: permission denied' "$argv0" "$db_root/$db_name.db"
    exit 13
fi

{ if (( $# )); then
      printf '%s\n' "$@"
  fi

  # append repository packages (all)
  if (( repo_targets )); then
      cut -f1 <db_info

  # append repository packages (updated)
  elif (( update )); then
      aur vercmp --quiet <db_info
  fi
} >argv

if [[ -s argv ]]; then
    # shellcheck disable=SC2094
    # depends: $1 pkgname $2 depends $3 pkgbase $4 pkgver
    xargs -a argv -d'\n' aur depends --table -- >depends
else
    plain >&2 "there is nothing to do"
    exit
fi

# pkginfo: $1 pkgname $2 pkgbase $3 pkgver
cut -f2 --complement depends | sort -u >pkginfo

{ if (( ${#pkg_i[@]} )); then
      warning "$argv0: ignoring %s package" "${pkg_i[@]}"
      printf '%s\n' "${pkg_i[@]}"
  fi

  # Packages with equal or newer versions are taken as complement
  # for the queue. If chkver_argv is enabled, packages on the
  # command-line are excluded from this complement.
  if (( chkver_depth )); then
      # note: AUR cannot be queried by pkgbase (FS#57230)
      cut -f1,3 pkginfo | aur vercmp -p db_info -c >current

      # shellcheck disable=SC2002
      case $chkver_depth in
          1) cat current | complement argv ;;
          2) cat current ;;
      esac
  fi

  # Note: this uses pacman's copy of the repo (as used by makepkg -s)
  if (( ${#repo_p[@]} )); then
      cut -f1 pkginfo | complement argv | aur-repo-filter "${repo_p[@]/#/--database=}"
  elif (( provides )); then
      cut -f1 pkginfo | complement argv | aur repo-filter --sync
  fi
} >filter

# pkgname queue (AUR + repos)
if cut -f1,2 depends | tr_ver | tsort >queue_0; then
    tac queue_0 | lib32 | complement filter >queue_1
else
    # input contains a loop
    error '%s: invalid argument' "$argv0"
    exit 22
fi

# pkgbase queue (AUR)
cut -f1,2 pkginfo | select_pkgbase - queue_1 >queue

if [[ -s queue ]]; then
    cd_safe "$AURDEST"
else
    plain >&2 "there is nothing to do"
    exit
fi

if (( download )); then
    msg >&2 "Retrieving package files"
    xargs -a "$tmp"/queue -d'\n' aur fetch -S --results "$tmp"/fetch_results

    # shellcheck disable=SC2034
    while IFS=: read -r mode rev_old rev path; do
        path=${path#file://} name=${path##*/}

        case $mode in
            clone)
                git -C "$path" config diff.orderFile "$orderfile" ;;
            fetch|rebase|reset)
                ;;
        esac
    done < "$tmp"/fetch_results
fi

# Link build files in the queue (absolute links)
while read -r pkg; do
    [[ $pkg ]] && printf '%s\0' "$(pwd -P)/$pkg"
done < "$tmp"/queue | xargs -0 ln -t "$tmp_view" -s --

# Check AUR metadata for validity (#20)
if (( graph )) && ! find "$tmp_view" -name .SRCINFO -exec cat {} + | aur graph >/dev/null; then
    error '%s: failed to verify dependency graph' "$argv0"
    exit 1
fi

if (( view )); then
    declare -A heads

    while read -r pkg; do
        git() { command git -C "$pkg" "$@"; }

        # Ensure every directory is a git repository (--continue)
        if head=$(git rev-parse HEAD); then
            heads[$pkg]=$head
        else
            error '%s: %s: not a git repository' "$argv0" "$pkg"
            exit 22
        fi

        if [[ -f $view_db/$pkg ]] && read -r view < "$view_db/$pkg"; then
            # Check if hash points to a valid object
            if ! git cat-file -e "$view"; then
                error '%s: %s: invalid revision' "$argv0" "$pkg"
                exit 22
            fi

            if [[ $view != "$head" ]]; then
                git --no-pager "$log_fmt" --patch --stat \
                    "$view..$head" > "$tmp_view/$pkg.$log_fmt"
            fi

        elif [[ -f $view_db/$pkg ]]; then
            error '%s: %s: failed to read revision' "$argv0" "$pkg"
            exit 1
        fi
    done < "$tmp"/queue
    unset -f git

    if [[ -v AUR_PAGER ]]; then
        # shellcheck disable=SC2086
        command -- $AUR_PAGER "$tmp_view"

        # Use an additional prompt for file managers with no support
        # for exit codes greater 0 (#673)
        if [[ -v AUR_CONFIRM_PAGER ]]; then
            read -rp $'Press Return to continue or Ctrl+d to abort\n'
        fi
    elif type -P vifm >/dev/null; then
        # Avoid directory prefix in printed paths (#452)
        viewer() ( cd "$1"; vifm -c 'set vifminfo=' -c 'view!' -c '0' - )

        { # Print patch files
          find "$tmp_view" -maxdepth 1 -type f

          # Print build directories in dependency order
          xargs -a "$tmp"/queue -I{} find -L "$tmp_view"/{} -maxdepth 1
        } | viewer "$tmp_view"
    else
        error '%s: no viewer found, please install vifm or set AUR_PAGER' "$argv0"
        exit 1
    fi

    # Update gitsums (viewer exited successfully)
    while read -r pkg; do
        printf '%s\n' "${heads[$pkg]}" > "$view_db/$pkg"
    done < "$tmp"/queue
fi

if (( build )); then
    aur build -a "$tmp"/queue -d "$db_name" --root "$db_root" "${build_args[@]}"
else
    while read -r pkg; do
        [[ $pkg ]] && printf '%s\n' "$(pwd -P)/$pkg"
    done < "$tmp"/queue
fi

# vim: set et sw=4 sts=4 ft=sh:
