#!/bin/bash

# check for packages that need to be built, and build a list in the proper build order
# Details:
#  https://github.com/archlinux32/builder/wiki/Build-system#get-package-updates

# TODOs:
#  be more secure in case of update while build(s) is/are still in progress
#   -> (stale) lock files, moving (or changing content of) loop lock files

. "${0%/*}/../conf/default.conf"

usage() {
  >&2 echo ''
  >&2 echo 'get-package-updates: check for packages that need to be built,'
  >&2 echo ' and build a list in the proper build order'
  >&2 echo ''
  >&2 echo 'possible options:'
  >&2 echo '  -b|--block:  If necessary, wait for lock blocking.'
  >&2 echo '  -h|--help:   Show this help and exit.'
  [ -z "$1" ] && exit 1 || exit $1
}

eval set -- "$(
  getopt -o bh \
    --long block \
    --long help \
    -n "$(basename "$0")" -- "$@" || \
  echo usage
)"

block=false

while true
do
  case "$1" in
    -b|--block)
      block=true
    ;;
    -h|--help)
      usage 0
    ;;
    --)
      shift
      break
    ;;
    *)
      >&2 echo 'Whoops, forgot to implement option "'"$1"'" internally.'
      exit -1
    ;;
  esac
  shift
done

if [ $# -ne 0 ]; then
  >&2 echo 'Too many arguments.'
  usage
fi

# delete_package package
# mark $package for deletion
delete_package() {
  echo "$1" >> \
    "${work_dir}/deletion-list.new"
  sed -i "/^${1//./\\.} /d" "${work_dir}/build-list.new"
}

# Update git repositories (official packages, community packages and the repository of package customizations).

for repo in "${repo_paths[@]}"; do
  # TODO:
  #  this is somewhat redundant and slow -- improve it!
  git -C "${repo}" checkout -f master
  git -C "${repo}" clean -xdf
  git -C "${repo}" fetch
  git -C "${repo}" reset --hard origin/master
done

# Read previous git revision numbers from files.

declare -A old_repo_revisions
declare -A new_repo_revisions

for repo in "${!repo_paths[@]}"; do
  old_repo_revisions["${repo}"]="$(
    cat "${work_dir}/${repo}.revision" 2> /dev/null || \
      echo NONE
  )"
  new_repo_revisions["${repo}"]="$(
    git -C "${repo_paths["${repo}"]}" rev-parse HEAD | \
      tee "${work_dir}/${repo}.revision.new"
  )"
done

# Create a lock file for build list.

while true; do
  exec 9> "${build_list_lock_file}"
  if flock -n 9; then
    break
  fi
  if ! ${block}; then
    >&2 echo 'come back (shortly) later - I cannot lock build list.'
    exit
  fi
  sleep 0.1
done

echo 'Check modified packages from the last update, and put them to the build list.'

# Check modified packages from the last update, and put them to the build list.
# If a package is updated, but already on the rebuild list, then just update the git revision number.
# If a package is deleted, remove from the rebuild list, and add it to the deletion list.
# If a new package is added, then ensure that it's not on the deletion list.

cp "${work_dir}/build-list"{,.new}
cp "${work_dir}/deletion-list"{,.new}

for repo in "${!repo_paths[@]}"; do
  (
    # if old revision unknown, mimic "git diff"-output
    if [ "${old_repo_revisions["${repo}"]}" == "NONE" ]; then
      git -C "${repo_paths["${repo}"]}" archive --format=tar HEAD | \
        tar -t | \
        sed 's|^|A\t|'
    else
      git -C "${repo_paths["${repo}"]}" diff --no-renames --name-status "${old_repo_revisions["${repo}"]}" HEAD
    fi
  ) | \
    # only track changes in PKGBUILDs
    grep '/PKGBUILD$' | \
    if [ "${repo}" == "archlinux32" ]; then
    # modify the directory structure from the modifiaction-repository
    # to the one of an original source repository
      sed 's|^\(.\t\)\([^/]\+\)/\([^/]\+\)/\(.\+\)$|\2 \1\3/repos/\2-x86_64/\4|' | \
        while read -r pkg_repo rest; do
          echo "${new_repo_revisions["$(find_git_repository_to_package_repository "${pkg_repo}")"]} ${rest}"
        done
    else
      sed "s|^|${new_repo_revisions["${repo}"]} |"
    fi | \
    grep '^\S\+ .\s[^/]\+/repos/[^/]\+/PKGBUILD$' | \
    # ignore i686
    grep -v -- '-i686/PKGBUILD$' | \
    sed 's|^\(\S\+\) \(.\)\t\([^/]\+\)/repos/\([^/]\+\)-[^/-]\+/PKGBUILD$|\2 \3 \1 \4|' | \
    # ignore staging and testing
    grep -v '\(staging\|testing\)$' | \
    # ignore lib32- packages (they should all have a pendent built for x86_64)
    grep -v '^. lib32-'
done | \
  sort -u | \
  while read -r mode package git_revision repository; do
    case "${mode}" in

      # new or modified PKGBUILD
      "A"|"M")
        sed -i "/^${package//./\\.} /d" "${work_dir}/build-list.new"
        echo "${package} ${git_revision} ${new_repo_revisions["archlinux32"]} ${repository}" >> \
          "${work_dir}/build-list.new"
        sed -i "/^${package//./\\.}\$/d" "${work_dir}/deletion-list.new"
      ;;

      # deleted PKGBUILD
      "D")
        delete_package "${package}"
      ;;

      *)
        >&2 echo "unknown git diff mode '${mode}'"
        exit 1
      ;;

    esac
  done

echo 'Extract dependencies of packages.'

# First, we extract the dependencies of each package.

mkdir -p "${work_dir}/package-infos"

while read -r package git_revision mod_git_revision repository; do

  file_prefix="${work_dir}/package-infos/${package}.${git_revision}.${mod_git_revision}"

  # extract infos from PKGBUILD if not existent yet
  if [ ! -e "${file_prefix}.builds" ] || \
    [ ! -e "${file_prefix}.needs" ] || \
    [ ! -e "${file_prefix}.packages" ]; then
    # delete cached values of old versions of this PKGBUILD
    ls -1 "${file_prefix%.*.*}."* 2> /dev/null | \
      sed 's|^.*/||' | \
      grep "^${package//./\\.}"'\.\([0-9a-f]\{40\}\.\)\{2\}\(builds\|needs\|packages\|SRCINFO\)$' | \
      while read file; do
        rm "${work_dir}/package-infos/${file}"
      done

    PKGBUILD="$(find_pkgbuild "${package}" "${repository}")"

    if [ ! -r "${PKGBUILD}" ]; then
      echo "can't find PKGBUILD to package '${package}' from repository '${repository}': '${PKGBUILD}'"
      exit 1
    fi

    (
      cd "${PKGBUILD%/*}"
      apply_package_customizations
      mksrcinfo -o "${file_prefix}.SRCINFO"
    )

    # otherwise this just calls for trouble
    sed -i '/=\s*$/d' "${file_prefix}.SRCINFO"

    # extract "builds" = provides \cup pkgname
    grep '^\('$'\t''provides\|pkgname\) = ' "${file_prefix}.SRCINFO" | \
      cut -d= -f2 | \
      sed 's|^\s\+||; s|[<>]$||' | \
      sort -u > \
      "${file_prefix}.builds"

    # extract "packages" = pkgname
    grep '^pkgname = ' "${file_prefix}.SRCINFO" | \
      cut -d= -f2 | \
      sed 's|^\s\+||; s|[<>]$||' | \
      sort -u > \
      "${file_prefix}.packages"

    # extract "needs" = ( makedepends \cup checkdepends ) \setminus "builds"
    (
      (
        # this would include runtime dependencies, too:
        # sed -n '/^pkgname = /q;/^'$'\t''depends = /p' "${file_prefix}.SRCINFO"
        grep '^'$'\t''\(makedepends\|checkdepends\) = ' "${file_prefix}.SRCINFO"
      ) | \
        cut -d= -f2 | \
        sed 's|^\s\+||; s|[<>]$||' | \
        sort -u
      sed 'p' "${file_prefix}.builds"
    ) | \
      sort | \
      uniq -u > \
      "${file_prefix}.needs"

    rm "${file_prefix}.SRCINFO"

  fi

done < "${work_dir}/build-list.new"

# ignore blacklisted packages and dependent packages
# this is the first time when all the information is available and up to date

black_listed=''
black_listed_new="$(
  cat "${repo_paths["archlinux32"]}/blacklist"
)"
while [ -n "${black_listed_new}" ]; do
  black_listed="$(
    printf '%s\n%s' "${black_listed}" "${black_listed_new}"
  )"
  black_listed_new="$(
    echo "${black_listed_new}" | \
      while read -r bl_package; do
        echo "${bl_package}"
        echo "${bl_package}"
        ls "${work_dir}/package-infos/" | \
          grep '\.needs$' | \
          while read -r package_info; do
            grep -q "^${bl_package//./\\.}\$" "${work_dir}/package-infos/${package_info}" && \
              echo "${package_info%.*.*.needs}"
          done
      done | \
      sort | \
      uniq -u
  )"
done
echo "${black_listed}" | \
  while read -r package; do
    [ -n "${package}" ] && \
      delete_package "${package}"
  done

# Now we create the partial order.

while read -r package git_revision mod_git_revision repository; do
  # add "$pkgname -> $build-target" to build-order list
  sed "s|\$| ${package}|" "${work_dir}/package-infos/${package}.${git_revision}.${mod_git_revision}.builds"
  # add "$dependency -> $pkgname" to build-order list
  sed "s|^|${package} |" "${work_dir}/package-infos/${package}.${git_revision}.${mod_git_revision}.needs"
done \
  < "${work_dir}/build-list.new" \
  > "${work_dir}/build-order"

echo 'Now actually sort it.'

(
  # this part will have the correct build order, but all the infos are missing
  tsort "${work_dir}/build-order" 2> "${work_dir}/tsort.error" | \
    nl -ba | \
    awk '{print $1 " not-git also-not-git whatever " $2}'
  # this part has all the infos, but possibly the wrong order
  awk '{print "0 " $2 " " $3 " " $4 " " $1}' "${work_dir}/build-list.new"
) | \
  sort -k5,5 -k1nr | \
  # now, we have the correct order and the infos, but in adjacent lines
  uniq -f4 -D | \
  sed '/^0 /d;N;s|\n| |' | \
  # now in one line, each
  sort -k1n,1 | \
  awk '{print $5 " " $7 " " $8 " " $9}' > \
  "${work_dir}/build-list.new.new"

rm --one-file-system -rf "${work_dir}/build-list.loops.new"
mkdir "${work_dir}/build-list.loops.new"

if [ -s "${work_dir}/tsort.error" ]; then
  >&2 echo 'WARNING: There is a dependency cycle!'
  >&2 cat "${work_dir}/tsort.error"
  >&2 echo
  >&2 echo 'I will continue anyway.'
  # save loops in separate files each, so breaking them is easier
  awk '
    /^tsort: \S+: input contains a loop:$/{
      n++;
      getline
    }
    {
      print $2 >"'"${work_dir}"'/build-list.loops.new/loop_" n
    }
    ' "${work_dir}/tsort.error"
else
  rm "${work_dir}/tsort.error"
fi

# Move the .new-files to the actual files

rm -rf --one-file-system "${work_dir}/build-list.loops"
(
  echo "build-list.loops" "build-list.new" "build-list" "deletion-list"
  echo "${!repo_paths[@]}" | \
    sed 's@\( \|$\)@.revision\1@g'
) | \
  tr ' ' '\n' | \
  while read -r file; do
    mv "${work_dir}/${file}.new" "${work_dir}/${file}"
  done

# Remove the lock file

rm -f "${build_list_lock_file}"