#!/bin/sh

# build packages one by one, then upload the binary package to the repository server
# Details:
#  https://github.com/archlinux32/builder/wiki/Build-system#build-packages

# TODOs:

#  repair build for signed git repositories (e.g. community/bitcoin)
#   - this should be ok now (updated devtools32 accordingly)

#  use different build commands for different repositories - do we need
#   this actually?

#  handle if build fails due to "local issues" (e.g. unclean
#   build environment, wrong mirror, ...) - how do we detect this (and
#   not fix the actual issue then)?

#  force different cache for builds (since we don't want to build
#   against an empty i686 cache on a x86_64 host)

#  avoid any-packages from x86_64 mirrors in /var/cache/pacman/pkg of
#   build slave

# shellcheck source=conf/default.conf
. "${0%/*}/../conf/default.conf"

# shellcheck disable=SC2016
usage() {
  >&2 echo ''
  >&2 echo 'build-packages: build package(s) on the build-list'
  >&2 echo ''
  >&2 echo 'possible options:'
  >&2 echo '  -h|--help:   Show this help and exit.'
  >&2 echo '  -l|--local pkgname.git-revision.git-mod-revision.repository:'
  >&2 echo '               Build the given package without asking / reporting to the'
  >&2 echo '               build master (except -u is given). Cannot be combined with'
  >&2 echo '               -n, -t or -x.'
  >&2 echo '  -n count:    Build $count packages (if available), then exit.'
  >&2 echo '               $count=0 is interpreted as infinity.'
  >&2 echo '               The default is $count=1 or 0 iff -t or -x is given.'
  >&2 echo '               Cannot be combined with -l.'
  >&2 echo '  -s|--straw $straw:'
  >&2 echo '               Use this straw instead of the preconfigured ones. -- May be'
  >&2 echo '               given multiple times to allow using multiple straws.'
  >&2 echo '  -t seconds:  Do not request new assignment(s) $seconds seconds after start.'
  >&2 echo '               Cannot be combined with -l.'
  >&2 echo '  -u|--upload: Upload explicitely built package to build master.'
  >&2 echo '               Can only be used with -l.'
  >&2 echo '  -x:          If package build fails, do not request new assignment(s).'
  >&2 echo '               Cannot be combined with -l.'
  [ -z "$1" ] && exit 1 || exit "$1"
}

eval set -- "$(
  getopt -o hl:n:s:t:ux \
    --long help \
    --long local: \
    --long straw: \
    --long upload \
    -n "$(basename "$0")" -- "$@" || \
  echo usage
)"

unset count
unset forced_package
unset forced_straws
exit_after_failure=false
upload_to_build_master=false
timeout=0

while true
do
  case "$1" in
    -h|--help)
      usage 0
    ;;
    -l|--local)
      shift
      if [ -n "${forced_package}" ]; then
        >&2 echo 'Option -l, --local can be given only once.'
        usage
      fi
      forced_package="$1"
    ;;
    -n)
      shift
      count="$1"
      [ "${count}" -eq 0 ] && \
        count=-1
    ;;
    -s|--straw)
      shift
      forced_straws="${forced_straws} $1"
    ;;
    -t)
      shift
      timeout="$1"
    ;;
    -u|--upload)
      upload_to_build_master=true
    ;;
    -x)
      exit_after_failure=true
    ;;
    --)
      shift
      break
    ;;
    *)
      >&2 echo 'Whoops, forgot to implement option "'"$1"'" internally.'
      exit 42
    ;;
  esac
  shift
done

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

if [ -n "${forced_package}" ]; then
  if [ -n "${count}" ] || \
    [ "${timeout}" -ne 0 ] || \
    ${exit_after_failure}; then
    >&2 echo 'Conflicting flags.'
    usage
  fi
else
  if ${upload_to_build_master}; then
    >&2 echo 'Conflicting flags.'
    usage
  fi
  upload_to_build_master=true
fi

if [ -z "${count}" ]; then
  if [ "${timeout}" -ne 0 ] || ${exit_after_failure}; then
    count=-1
  else
    count=1
  fi
fi

if [ -n "${forced_straws}" ]; then
  straws_that_might_repair_failing_builds="${forced_straws# }"
fi

if [ "${timeout}" -ne 0 ]; then
  timeout=$((timeout+$(date +%s)))
fi

while [ "${count}" -ne 0 ]; do

  if [ "${timeout}" -ne 0 ] && [ "${timeout}" -lt "$(date +%s)" ];
  then
    break
  fi

  err=0
  if [ -z "${forced_package}" ]; then
    package=$(
      ssh \
        -i "${master_build_server_identity}" \
        -p "${master_build_server_port}" \
        "${master_build_server_user}@${master_build_server}" \
        'get-assignment'
    ) || err=$?
  else
    package=$(
      echo "${forced_package}" | \
        sed '
          s|\.\([^.]\+\)\.\([^.]\+\)\.\([^.]\+\)$| \1 \2 \3|
        '
    )
  fi

  case ${err} in

    #  0: ok, I gave you an assignment
    0)
      [ ${count} -gt 0 ] && \
        count=$((count-1))
      repository="${package##* }"
      package="${package% *}"
      mod_git_revision="${package##* }"
      package="${package% *}"
      git_revision="${package##* }"
      package="${package% *}"

      if [ "${git_revision##*-}" = 'HEAD' ]; then
        git_revision=$(
          repo_name="${git_revision%-*}"
          eval repo_path='"${repo_paths__'"${repo_name}"'}"'
          if [ -z "${repo_path}" ]; then
            >&2 printf 'Unknown git repository "%s".\n' "${repo_name}"
            exit 2
          fi
          git -C "${repo_path}" rev-parse HEAD
        )
      fi

      if [ "${mod_git_revision}" = 'work-tree' ]; then
        mod_git_revision=$(
          # we can't just create an empty index-file with mktemp, because git doesn't like it
          tmp_subdir=$(mktemp -d);
          trap 'rm -rf --one-file-system "${tmp_subdir}"' EXIT
          export GIT_INDEX_FILE="${tmp_subdir}/index.new"
          git -C "${repo_paths__archlinux32}" add -A
          git -C "${repo_paths__archlinux32}" write-tree
        )
      fi

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

      for repo_name in ${repo_names}; do
        eval repo_path='"${repo_paths__'"${repo_name}"'}"'
        git -C "${repo_path}" fetch
      done

      git_repo=$(find_repository_with_commit "${git_revision}")

      find_pkgbuilds "${package}" "${repository}" "${git_repo}" "${git_revision}" "${mod_git_revision}"

      bail_out() {
        err=$?
        if [ -n "$1" ]; then
          err="$1"
        fi
        cd "${base_dir}"
        recursively_umount_and_rm "${tmp_dir}"
        exit "${err}"
      }
      tmp_dir=$(mktemp -d "${work_dir}/tmp.XXXXXX")
      trap bail_out EXIT

      extract_source_directory "${git_repo}" "${git_revision}" "${mod_git_revision}" "${tmp_dir}"

      cd "${tmp_dir}"
      success=false
      for straw in ${straws_that_might_repair_failing_builds}; do

        if echo "${straw}" | \
          grep -qF ':mirrored_source:'; then
          # maybe a missing source is/was the problem?
          # try to download them from sources.archlinux.org/sources/$repo/$source
          source_name=$(
            makepkg --printsrcinfo | \
              sed -n '
                /^\s*\(epoch\|pkg\(base\|ver\|rel\)\) = /{s|^\s\+||;p}
                /^pkgname = /q
              ' | \
              sed '
                s|^pkgbase = \(.*\)$|0 \1-|
                s|^epoch = \(.*\)$|1 \1:|
                s|^pkgver = \(.*\)$|2 \1-|
                s|^pkgrel = \(.*\)$|3 \1.src.tar.gz|
              ' | \
              sort -k1n,1 | \
              sed '
                s|^[0-9] ||
                :a
                  N
                  s|\n[0-9] ||
                ta
              '
          )
          if ! wget -nc -nd "https://sources.archlinux.org/sources/${git_repo}/${source_name}"; then
            # we can't improve anything
            continue
          fi
          tar -xz --overwrite -f "${source_name}" --exclude PKGBUILD --strip-components=1 || true
        fi

        if echo "${straw}" | \
          grep -qF ':mirrored_source_by_hash:'; then
          # maybe a missing source is/was the problem?
          # download it from sources.archlinux32.org by its hash
          if ! download_sources_by_hash "${package}" "${repository}" "${git_revision}" "${mod_git_revision}"; then
            # we can't improve anything, if no source was downloadable
            continue
          fi
        fi

        if echo "${straw}" | \
          grep -qF ':with_build_support:'; then
          build_command='staging-with-build-support-i686-build'
        else
          build_command='staging-i686-build'
        fi
        if echo "${straw}" | \
          grep -qF ':clean_chroot:'; then
          parameters='-c'
        else
          parameters=''
        fi

        find . -maxdepth 1 -type f \( -name '*.pkg.tar.xz' -o -name '*.pkg.tar.xz.sig' \) -exec \
          rm {} \;

        >&2 printf '%s: building package "%s" (revisions %s %s, repository %s, straw %s) ...' \
          "$(date +'%Y-%m-%d %T')" \
          "${package}" \
          "${git_revision}" \
          "${mod_git_revision}" \
          "${repository}" \
          "${straw}"
        # by piping the log, we don't see anything in the terminal,
        # but all ways to duplicate the logs seem pretty elaborate
        if "${build_command}" ${parameters} > \
          "$(
            date -u --iso-8601=seconds | \
              cut -d+ -f1
          ).build-log" 2>&1; then
          # build successful
          >&2 printf ' ok.\n'
          find . -maxdepth 1 -type f -name '*.pkg.tar.xz' -exec \
            gpg --local-user="${package_key}" --detach-sign {} \;
          # shellcheck disable=SC2046
          tar -cf 'package.tar' -- $(
            find . -maxdepth 1 \( -name '*.pkg.tar.xz' -o -name '*.pkg.tar.xz.sig' \) -not -name '*-debug-*' -printf '%f\n'
            )
          while ${upload_to_build_master}; do
            err=0
            # shellcheck disable=SC2029
            ssh \
              -i "${master_build_server_identity}" \
              -p "${master_build_server_port}" \
              "${master_build_server_user}@${master_build_server}" \
              'return-assignment' "${package}" "${git_revision}" "${mod_git_revision}" "${repository}" \
              < 'package.tar' || \
              err=$?
            case ${err} in
              0)
                # upload successful
                break
              ;;
              1)
                >&2 echo '"return-assignment" was running already.'
                wait_some_time 15
              ;;
              2)
                >&2 echo 'I was too slow, the package is outdated. I will continue ...'
                break
              ;;
              3)
                >&2 echo "'return-assignment' reports a signature error."
                bail_out 1
              ;;
              4)
                >&2 echo "'return-assignment' reports too many or missing packages."
                bail_out 1
              ;;
              *)
                >&2 echo "unknown return code ${err} from 'return-assignment'"
                bail_out 1
            esac
          done
          success=true
          break
        fi
        >&2 printf ' failed.\n'
      done

      if ! ${success}; then
        for log in *'.build-log'; do
          if [ -f "${log}" ]; then
            if ${upload_to_build_master}; then
              gzip "${log}"
            else
              grep -HF '' "${log}"
            fi
          fi
        done
        if ${upload_to_build_master} && \
          tar -cf 'build-logs.gz.tar' \
          -- *'.build-log.gz'; then
          while ! ssh \
            -i "${master_build_server_identity}" \
            -p "${master_build_server_port}" \
            "${master_build_server_user}@${master_build_server}" \
            'return-assignment' "${package}" "${git_revision}" "${mod_git_revision}" "${repository}" 'ERROR' < \
            'build-logs.gz.tar'; do
            wait_some_time 15
          done
        fi
        if ${exit_after_failure}; then
          >&2 echo 'Build failed, exiting now'
          bail_out 0
        fi
      fi

      # clean up tmp_dir
      cd "${base_dir}"
      recursively_umount_and_rm "${tmp_dir}"
      trap - EXIT

      continue

    ;;

    1)

      >&2 echo 'get-assignment told me:'
      >&2 echo ' come back (shortly) later - I was running already'
      wait_some_time 15
      continue

    ;;

    2)

      >&2 echo 'get-assignment told me:'
      >&2 echo ' 2: come back later - there are still packages to be built,'
      >&2 echo ' but currently none has all its dependencies ready'
      wait_some_time 60
      continue

    ;;

    3)

      >&2 echo 'get-assignment told me:'
      >&2 echo ' 3: come back after the next run of get-package-updates - currently'
      >&2 echo ' there are no pending packages'
      exit 0

    ;;

    *)

      >&2 echo "ERROR: Unknown exit code ${err} from 'get-assignment'."
      exit 1

    ;;

  esac

done

>&2 echo 'Done.'