#!/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_STATE_HOME=${XDG_STATE_HOME:-$HOME/.local/state}
XDG_DATA_HOME=${XDG_DATA_HOME:-$HOME/.local/share}
AURDEST=${AURDEST:-$XDG_CACHE_HOME/aurutils/$argv0}
AUR_SYNC_USE_NINJA=${AUR_SYNC_USE_NINJA:-0}
AUR_SYNC_GNUPGHOME=${AUR_SYNC_GNUPGHOME:-$XDG_DATA_HOME/aurutils/$argv0/gnupg}
PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'

# Avoid CDPATH screwing with cd (#1047)
unset -v CDPATH

# default arguments
build_args=(--syncdeps) build_repo_args=()
depends_args=() view_args=() filter_args=() fetch_args=() graph_args=() reparse_args=()

# default options
build=1 chkver_depth=2 download=1 view=1 provides=1 graph=1 keep_going=1

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

args_csv() {
    # shellcheck disable=SC2155
    local str=$(printf '%s,' "$@")
    printf '%s' "${str%,}"
}

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

select_pkgspec() {
    awk -F'/' -v needle="$1" '{
        if (NF == 2 && $1 == needle) {
            print $2
        }
        else if (NF == 1) {
            print $1
        }
    }' "${@:2}"
}

swap() {
    awk '{print $2 "\t" $1}' "$@"
}

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

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

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

# Clarify tsort errors (#1039)
diag_depcycle() {
	echo >&2 'Error:'

cat <<EOF | pr -to 4 >&2
aur-$argv0 encountered a cycle while resolving dependencies. This may be caused
by an error in the PKGBUILD or by cyclic checkdepends. For the latter, retry
building the package(s) with --no-check.
EOF
}

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

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

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

# option parsing
opt_short='d:D:k:U:ACcfKLnorRSTuv'
opt_long=('bind:' 'bind-rw:' 'database:' 'directory:' 'ignore:' 'root:'
          'makepkg-conf:' 'pacman-conf:' 'chroot' 'continue' 'force' 'ignore-arch'
          'log' 'no-confirm' 'no-ver' 'no-graph' 'no-sync' 'no-ver-argv' 'no-view'
          'no-provides' 'no-build' 'rmdeps' 'sign' 'temp' 'upgrades' 'pkgver'
          'rebuild' 'rebuild-tree' 'rebuild-all' 'ignore-file:' 'remove'
          'provides-from:' 'new' 'prevent-downgrade' 'verify' 'makepkg-args:'
          'format:' 'no-check' 'keep-going:' 'user:' 'rebase' 'reset' 'ff' 'exclude:'
          'columns' 'prefix' 'save:' 'clean' 'cleanbuild' 'auto-key-retrieve')
opt_hidden=('dump-options' 'allan' 'ignorearch' 'ignorefile:' 'noconfirm'
            'nover' 'nograph' 'nosync' 'nover-argv' 'noview' 'noprovides' 'nobuild'
            'rebuildall' 'rebuildtree' 'rm-deps' 'gpg-sign' 'margs:' 'nocheck'
            'no-checkdepends' 'nocheckdepends' 'optdepends' 'repo:' 'autokeyretrieve')

if opts=$(getopt -o "$opt_short" -l "$(args_csv "${opt_long[@]}" "${opt_hidden[@]}")" -n "$argv0" -- "$@"); then
    eval set -- "$opts"
else
    usage
fi

unset pkg pkg_i repo repo_p ignore_file stdout_file
while true; do
    case "$1" in
        # sync options
        --allan)
            rotate=1 ;;
        --continue)
            download=0 ;;
        --ignore)
            shift; IFS=, read -a pkg -r <<< "$1"
            pkg_i+=("${pkg[@]}") ;;
        --ignorefile|--ignore-file)
            shift; ignore_file=$1 ;;
        -k|--keep-going)
            shift; keep_going=$1 ;;
        -o|--nobuild|--no-build)
            build=0 ;;
        --columns)
            build=0; columns=1 ;;
        --save)
            shift; stdout_file=$1 ;;
        --optdepends)
            depends_args+=(--optdepends)
            graph_args+=(-v OPTDEPENDS=1) ;;
        --nocheck|--no-check|--nocheckdepends|--no-checkdepends)
            depends_args+=(--no-checkdepends)
            build_args+=(--no-check)
            graph_args+=(-v CHECKDEPENDS=0) ;;
        --nograph|--no-graph)
            graph=0 ;;
        --nosync|--no-sync)
            build_args+=(--no-sync) ;;
        --nover|--no-ver)
            chkver_depth=0 ;;
        --nover-argv|--no-ver-argv)
            chkver_depth=1 ;;
        --noview|--no-view)
            view=0 ;;
        --noprovides|--no-provides)
            provides=0 ;;
        -K|--auto-key-retrieve|--autokeyretrieve)
            auto_key_retrieve=1 ;;
        --provides-from)
            shift; IFS=, read -a repo -r <<< "$1"
            repo_p+=("${repo[@]}")
            provides=1 ;;
        --rebuild)
            # Command-line targets are excluded from `repo-filter`
            build_args+=(-f); chkver_depth=1 ;;
        --rebuildtree|--rebuild-tree)
            # Dependencies may be removed by `repo-filter` (#1066)
            build_args+=(-f); chkver_depth=0; provides=0 ;;
        --rebuildall|--rebuild-all)
            build_args+=(-f); chkver_depth=0; repo_targets=1 ;;
        -u|--upgrades)
            update=1 ;;
        # database options
        -d|--database|--repo)
            shift; build_repo_args+=(-d "$1") ;;
        --root)
            shift; build_repo_args+=(--root "$1") ;;
        # fetch options
        --ff)
            fetch_args+=(--ff) ;;
        --rebase)
            fetch_args+=(--rebase) ;;
        --reset)
            fetch_args+=(--reset) ;;
        # view options
        --format)
            shift; view_args+=(--format "$1") ;;
        --exclude)
            shift; view_args+=(--exclude "$1") ;;
        --prefix)
            view_args+=(--prefix) ;;  # experimental
        # build options
        -c|--chroot)
            build_args+=(--chroot) ;;
        -f|--force)
            build_args+=(--force) ;;
        -C|--clean)
            build_args+=(--clean) ;;
        --cleanbuild)
            build_args+=(--cleanbuild) ;;
        --makepkg-args|--margs)
            shift; build_args+=(--margs "$1") ;;
        --makepkg-conf)
            shift; build_args+=(--makepkg-conf "$1") ;;
        --pacman-conf)
            shift; build_args+=(--pacman-conf "$1")
            filter_args+=(--config "$1") ;;
        --pkgver)
            build_args+=(--pkgver) ;;
        -S|--sign|--gpg-sign)
            build_args+=(--sign) ;;
        -U|--user)
            shift; build_args+=(--user "$1") ;;
        # 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")
trap 'trap_exit' EXIT

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

if (( $# + update + repo_targets == 0 )); then
    printf >&2 '%s: no targets specified\n' "$argv0"
    exit 1
fi

# Write --no-build / --columns results to a file (#1077)
if [[ -v stdout_file ]]; then
    stdout_file=$(realpath -- "$stdout_file")
fi

# Retrieve path to local repo (#448, #700, #1135)
{ IFS=: read -r _ db_name
  IFS=: read -r _ db_root
  IFS=: read -r _ db_path
  IFS=: read -r _ _
  IFS=: read -r _ _
} < <(aur build "${build_args[@]}" "${build_repo_args[@]}" --status)

# Print an error if `build --status` fails (#1151)
if ! wait "$!"; then
    printf '%s: error: failed to read build configuration\n' "$argv0"
    exit 1
fi

msg >&2 'Using [%s] repository' "$db_name"

# Ignores file can be the result of process substitution (#880)
: "${ignore_file=$XDG_CONFIG_HOME/aurutils/sync/ignore}"

if [[ -r $ignore_file ]] && [[ ! -d $ignore_file ]]; then
    # Append file ignores to command-line ignores
    while read -r i; do pkg_i+=("$i"); done < <(select_pkgspec "$db_name" "$ignore_file")
fi

# Diagnostic on which packages are ignored
if (( ${#pkg_i[@]} )); then
    # Do not include command-line arguments as ignores (#952)
    mapfile -t pkg_i < <(complement <(printf '%s\n' "$@") <(printf '%s\n' "${pkg_i[@]}"))
    printf '%s: packages ignored: %s\n' "$argv0" "${pkg_i[*]}"

    # Ignore local repository targets (#1146)
    reparse_args+=(--ignore "$(args_csv "${pkg_i[@]}")")
fi >&2

# Retrieve list of local repository packages ($1 pkgname $2 pkgver)
aur repo-parse "${reparse_args[@]}" -p "$db_path" --list >"$tmp"/db_info

# Build list of AUR targets
{ if (( $# )); then
      # append command-line arguments
      printf '%s\n' "$@"
  fi

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

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

# Build AUR dependency graph
if [[ -s $tmp/argv ]]; then
    # shellcheck disable=SC2094
    aur depends --jsonl "${depends_args[@]}" - <"$tmp"/argv >"$tmp"/depends.jsonl
else
    printf >&2 '%s: there is nothing to do\n' "$argv0"
    exit
fi

# pkginfo: $1 pkgname $2 pkgbase $3 pkgver
aur format -f '%n\t%b\t%v\n' "$tmp"/depends.jsonl | sort -u >"$tmp"/pkginfo

{ if (( ${#pkg_i[@]} )); then
      printf '%s\n' "${pkg_i[@]}"  # Ignored packages
  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 "$tmp"/pkginfo | aur vercmp -p "$tmp"/db_info -c >"$tmp"/current

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

  if (( provides )); then
      if (( ${#repo_p[@]} )); then
          filter_args+=("${repo_p[@]/#/--repo=}")
      else
          filter_args+=(--sync)
      fi

      # Note: this uses pacman's copy of the repo (as used by makepkg -s)
      cut -f1 "$tmp"/pkginfo | aur repo-filter "${filter_args[@]}" | complement "$tmp"/argv
  fi
} >"$tmp"/filter

# Filter out targets determined in the steps above recursively (#1136, #1140)
aur sync--filter -p "$tmp"/depends.jsonl -f "$tmp"/filter >"$tmp"/graph

# XXX: a flat file is needed for aur-{graph,fetch,view}. `ninja` requires the
# build files to be present before dependency resolution (with `ninja -n`) can
# occur, and `ninja -t targets` sorts in alphabetical order. This implies
# that dependency cycles cannot be resolved before retrieving files with
# aur-fetch with `ninja` alone. `tsort` could either be used in this case (with
# a less nice diagnostic on cycles), or fetches done in an arbitrary order
# (e.g. sort -u) with checks for cycles done at build-time.
if ! tsort <"$tmp"/graph >"$tmp"/queue; then
    diag_depcycle
    exit 22
fi

if [[ -s $tmp/queue ]]; then
    cd -- "$AURDEST"
else
    printf >&2 '%s: there is nothing to do\n' "$argv0"
    exit
fi

if (( download )); then
    msg >&2 "Retrieving package files"
    aur fetch -S "${fetch_args[@]}" --discard - < "$tmp"/queue >&2
else
    xargs -a "$tmp"/queue stat >/dev/null || exit 2 # ensure all directories are available
fi

if (( auto_key_retrieve )); then
    # shellcheck disable=SC2174
    mkdir -pm 0700 -- "$AUR_SYNC_GNUPGHOME"
    declare -A keys_uniq

    # Retrieve unique set of gpg keys to be imported
    while IFS= read -r path; do
        if [[ -f $path/.SRCINFO ]]; then
            mapfile -t keys < <(pacini "$path"/.SRCINFO 'validpgpkeys')

            for key in "${keys[@]}"; do
                keys_uniq[$key]=1
            done
        fi
    done < "$tmp"/queue

    if (( ${#keys_uniq[@]} )); then
        printf >&2 '%s: importing key %s\n' "${!keys_uniq[@]}"
        GNUPGHOME="$AUR_SYNC_GNUPGHOME" gpg --recv-keys "${!keys_uniq[@]}" >&2
    fi

    # Pass on verified keys to makepkg
    build_args+=(--makepkg-gnupghome="$AUR_SYNC_GNUPGHOME")
fi

# Verify dependency tree (#20)
# Use pacini to preprocess .SRCINFO to avoid CRLF (#1203)
if (( graph )); then
    if ! { while read -r pkg; do
               [[ $pkg ]] && printf '%s\0' "$pkg/.SRCINFO"
           done
         } | xargs -0 cat -- | pacini | aur graph "${graph_args[@]}" REVERSE=1
    then
        printf >&2 '%s: failed to verify dependency graph\n' "$argv0"
        exit 1
    fi <"$tmp"/queue >"$tmp"/graph

    # Recompute dependencies to include `provides` (#837)
    if ! tsort < "$tmp"/graph >"$tmp"/queue; then
        diag_depcycle
        exit 22
    fi
fi

{ # Sort dependencies (`aur-sync--ninja`, `--columns`)
  swap "$tmp"/graph | sort -k1b,1 -k1 -u >"$tmp"/graph.ninja

  # Resolve absolute paths (`--no-build`)
  while read -r pkg; do
      [[ $pkg ]] && printf '%s\n' "$AURDEST/$pkg"
  done <"$tmp"/queue >"$tmp"/queue.realpath
}

# Inspect package files
if (( view )); then
    aur view -a "$tmp"/queue "${view_args[@]}"
fi

# `--columns` / `--no-build` output
if (( columns )) && [[ -v stdout_file ]]; then
    cat "$tmp"/graph.ninja >"$stdout_file"

elif (( columns )); then
    cat "$tmp"/graph.ninja

elif ! (( build )) && [[ -v stdout_file ]]; then
    cat "$tmp"/queue.realpath >"$stdout_file"

elif ! (( build )); then
    cat "$tmp"/queue.realpath

# Build dependency tree with ninja (#908)
elif (( AUR_SYNC_USE_NINJA )); then
    # Apply `--save` (#1091)
    if [[ -v stdout_file ]]; then
        cat "$tmp"/graph.ninja >"$stdout_file"
    fi

    # Directory for stamp files (concurrent aur-sync processes)
    mkdir -p -- "$XDG_STATE_HOME"/aurutils/$argv0
    ninja_dir=$XDG_STATE_HOME/aurutils/$argv0/ninja-$USER-$$
    mkdir -- "$ninja_dir"

    # Generate build.ninja
    # input:  $AURDEST/pkgbase/PKGBUILD
    # output: $ninja_dir/pkgbase.stamp
    aur sync--ninja "$AURDEST" <"$tmp"/graph.ninja >"$ninja_dir"/build.ninja \
        -- aur build "${build_args[@]}" -d "$db_name" --root "$db_root"

    if NINJA_STATUS='[%s/%t] ' ninja -C "$ninja_dir" -k "$keep_going"; then
        # Remove ninja directory on successful build
        rm -rf "$ninja_dir"
    else
        # Print all targets in dependency order
        NINJA_STATUS='[%s/%t] ' ninja -nC /var/empty -f "$ninja_dir"/build.ninja | \
            # [\w@\.\-\+]: valid characters for pkgname
            # alternative: [^\s]+ from rule `env -C ... > pkgbase.stamp`
            pcregrep -o1 -o3 '(\[\d+/\d+\] )(.+?)([\w@\.\-\+]+)(\.stamp)' | while read -r status pkg
        do
            if [[ -f $ninja_dir/$pkg.stamp ]]; then
                printf "${BOLD}${BLUE}%s${ALL_OFF} %s\t${BOLD}${GREEN}[OK]${ALL_OFF}\n" "$status" "$pkg"
            else
                printf "${BOLD}${BLUE}%s${ALL_OFF} %s\t${BOLD}${RED}[FAIL]${ALL_OFF}\n" "$status" "$pkg"
            fi
        done | column -t

        # Preserve ninja directory
        printf '%s: build files at %s\n' "$argv0" "$ninja_dir"
    fi
else
    # Apply `--save` (#1091)
    if [[ -v stdout_file ]]; then
        cat "$tmp"/queue.realpath >"$stdout_file"
    fi
    aur build "${build_args[@]}" -a "$tmp"/queue.realpath -d "$db_name" --root "$db_root"
fi

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