#!/bin/bash
#
# SPDX-License-Identifier: GPL-3.0-or-later

#!/hint/bash
#
# This may be included with or without `set -euE`
#
# SPDX-License-Identifier: GPL-3.0-or-later

[[ -z ${_INCLUDE_COMMON_SH:-} ]] || return 0
_INCLUDE_COMMON_SH="$(set +o|grep nounset)"

set +u +o posix
# shellcheck disable=1091
. /usr/share/makepkg/util.sh
$_INCLUDE_COMMON_SH

# Avoid any encoding problems
export LANG=C

# Set buildtool properties
export BUILDTOOL=devtools
export BUILDTOOLVER=20230105-1-any

# check if messages are to be printed using color
if [[ -t 2 && "$TERM" != dumb ]]; then
	colorize
else
	# shellcheck disable=2034
	declare -gr ALL_OFF='' BOLD='' BLUE='' GREEN='' RED='' YELLOW=''
fi

stat_busy() {
	local mesg=$1; shift
	# shellcheck disable=2059
	printf "${GREEN}==>${ALL_OFF}${BOLD} ${mesg}...${ALL_OFF}" "$@" >&2
}

stat_done() {
	# shellcheck disable=2059
	printf "${BOLD}done${ALL_OFF}\n" >&2
}

_setup_workdir=false
setup_workdir() {
	[[ -z ${WORKDIR:-} ]] && WORKDIR=$(mktemp -d --tmpdir "${0##*/}.XXXXXXXXXX")
	_setup_workdir=true
	trap 'trap_abort' INT QUIT TERM HUP
	trap 'trap_exit' EXIT
}

cleanup() {
	if [[ -n ${WORKDIR:-} ]] && $_setup_workdir; then
		rm -rf "$WORKDIR"
	fi
	exit "${1:-0}"
}

abort() {
	error 'Aborting...'
	cleanup 255
}

trap_abort() {
	trap - EXIT INT QUIT TERM HUP
	abort
}

trap_exit() {
	local r=$?
	trap - EXIT INT QUIT TERM HUP
	cleanup $r
}

die() {
	(( $# )) && error "$@"
	cleanup 255
}

##
#  usage : lock( $fd, $file, $message, [ $message_arguments... ] )
##
lock() {
	# Only reopen the FD if it wasn't handed to us
	if ! [[ "/dev/fd/$1" -ef "$2" ]]; then
		mkdir -p -- "$(dirname -- "$2")"
		eval "exec $1>"'"$2"'
	fi

	if ! flock -n "$1"; then
		stat_busy "${@:3}"
		flock "$1"
		stat_done
	fi
}

##
#  usage : slock( $fd, $file, $message, [ $message_arguments... ] )
##
slock() {
	# Only reopen the FD if it wasn't handed to us
	if ! [[ "/dev/fd/$1" -ef "$2" ]]; then
		mkdir -p -- "$(dirname -- "$2")"
		eval "exec $1>"'"$2"'
	fi

	if ! flock -sn "$1"; then
		stat_busy "${@:3}"
		flock -s "$1"
		stat_done
	fi
}

##
#  usage : lock_close( $fd )
##
lock_close() {
	local fd=$1
	# https://github.com/koalaman/shellcheck/issues/862
	# shellcheck disable=2034
	exec {fd}>&-
}

##
# usage: pkgver_equal( $pkgver1, $pkgver2 )
##
pkgver_equal() {
	if [[ $1 = *-* && $2 = *-* ]]; then
		# if both versions have a pkgrel, then they must be an exact match
		[[ $1 = "$2" ]]
	else
		# otherwise, trim any pkgrel and compare the bare version.
		[[ ${1%%-*} = "${2%%-*}" ]]
	fi
}

##
#  usage: find_cached_package( $pkgname, $pkgver, $arch )
#
#    $pkgver can be supplied with or without a pkgrel appended.
#    If not supplied, any pkgrel will be matched.
##
find_cached_package() {
	local searchdirs=("$PWD" "$PKGDEST") results=()
	local targetname=$1 targetver=$2 targetarch=$3
	local dir pkg packages pkgbasename name ver rel arch r results

	for dir in "${searchdirs[@]}"; do
		[[ -d $dir ]] || continue

		shopt -s extglob nullglob
		mapfile -t packages < <(printf "%s\n" "$dir"/"${targetname}"-"${targetver}"-*"${targetarch}".pkg.tar?(.!(sig|*.*)))
		shopt -u extglob nullglob

		for pkg in "${packages[@]}"; do
			[[ -f $pkg ]] || continue

			# avoid adding duplicates of the same inode
			for r in "${results[@]}"; do
				[[ $r -ef $pkg ]] && continue 2
			done

			# split apart package filename into parts
			pkgbasename=${pkg##*/}
			pkgbasename=${pkgbasename%.pkg.tar*}

			arch=${pkgbasename##*-}
			pkgbasename=${pkgbasename%-"$arch"}

			rel=${pkgbasename##*-}
			pkgbasename=${pkgbasename%-"$rel"}

			ver=${pkgbasename##*-}
			name=${pkgbasename%-"$ver"}

			if [[ $targetname = "$name" && $targetarch = "$arch" ]] &&
					pkgver_equal "$targetver" "$ver-$rel"; then
				results+=("$pkg")
			fi
		done
	done

	case ${#results[*]} in
		0)
			return 1
			;;
		1)
			printf '%s\n' "${results[0]}"
			return 0
			;;
		*)
			error 'Multiple packages found:'
			printf '\t%s\n' "${results[@]}" >&2
			return 1
	esac
}


check_package_validity(){
	local pkgfile=$1
	if grep -q "packager = Unknown Packager" <(bsdtar -xOqf "$pkgfile" .PKGINFO); then
		die "PACKAGER was not set when building package"
	fi
	hashsum=sha256sum
	pkgbuild_hash=$(awk -v"hashsum=$hashsum" -F' = ' '$1 == "pkgbuild_"hashsum {print $2}' <(bsdtar -xOqf "$pkgfile" .BUILDINFO))
	if [[ "$pkgbuild_hash" != "$($hashsum PKGBUILD|cut -d' ' -f1)" ]]; then
		die "PKGBUILD $hashsum mismatch: expected $pkgbuild_hash"
	fi
}


# usage: grep_pkginfo pkgfile pattern
grep_pkginfo() {
	local _ret=()
	mapfile -t _ret < <(bsdtar -xOqf "$1" ".PKGINFO" | grep "^${2} = ")
	printf '%s\n' "${_ret[@]#${2} = }"
}


# Get the package name
getpkgname() {
	local _name

	_name="$(grep_pkginfo "$1" "pkgname")"
	if [[ -z $_name ]]; then
		error "Package '%s' has no pkgname in the PKGINFO. Fail!" "$1"
		exit 1
	fi

	echo "$_name"
}


# Get the package base or name as fallback
getpkgbase() {
	local _base

	_base="$(grep_pkginfo "$1" "pkgbase")"
	if [[ -z $_base ]]; then
		getpkgname "$1"
	else
		echo "$_base"
	fi
}


getpkgdesc() {
	local _desc

	_desc="$(grep_pkginfo "$1" "pkgdesc")"
	if [[ -z $_desc ]]; then
		error "Package '%s' has no pkgdesc in the PKGINFO. Fail!" "$1"
		exit 1
	fi

	echo "$_desc"
}


is_debug_package() {
	local pkgfile=${1} pkgbase pkgname pkgdesc
	pkgbase="$(getpkgbase "${pkgfile}")"
	pkgname="$(getpkgname "${pkgfile}")"
	pkgdesc="$(getpkgdesc "${pkgfile}")"
	[[ ${pkgdesc} == "Detached debugging symbols for "* && ${pkgbase}-debug = "${pkgname}" ]]
}


# Source makepkg.conf; fail if it is not found
if [[ -r '/etc/makepkg.conf' ]]; then
	# shellcheck source=config/makepkg/x86_64.conf
	source '/etc/makepkg.conf'
else
	die '/etc/makepkg.conf not found!'
fi

# Source user-specific makepkg.conf overrides
if [[ -r "${XDG_CONFIG_HOME:-$HOME/.config}/pacman/makepkg.conf" ]]; then
	# shellcheck source=/dev/null
	source "${XDG_CONFIG_HOME:-$HOME/.config}/pacman/makepkg.conf"
elif [[ -r "$HOME/.makepkg.conf" ]]; then
	# shellcheck source=/dev/null
	source "$HOME/.makepkg.conf"
fi

cmd=${0##*/}

if [[ ! -f PKGBUILD ]]; then
	die 'No PKGBUILD file'
fi

source=()
# shellcheck source=contrib/makepkg/PKGBUILD.proto
. ./PKGBUILD
pkgbase=${pkgbase:-$pkgname}

case "$cmd" in
	commitpkg)
		if (( $# == 0 )); then
			die 'Usage: commitpkg <reponame> [-f] [-s server] [-l limit] [-a arch] [commit message]'
		fi
		repo="$1"
		shift
		;;
	*pkg)
		repo="${cmd%pkg}"
		;;
	*)
		die 'Usage: commitpkg <reponame> [-f] [-s server] [-l limit] [-a arch] [commit message]'
		;;
esac


if (( ${#validpgpkeys[@]} != 0 )); then
	if [[ -d keys ]]; then
		for key in "${validpgpkeys[@]}"; do
			if [[ ! -f keys/pgp/$key.asc ]]; then
				export-pkgbuild-keys || die 'Failed to export valid PGP keys for source files'
			fi
		done
	else
		export-pkgbuild-keys || die 'Failed to export valid PGP keys for source files'
	fi

	svn add --parents --force keys/pgp/*
fi

# find files which should be under source control
needsversioning=()
for s in "${source[@]}"; do
	[[ $s != *://* ]] && needsversioning+=("$s")
done
for i in 'changelog' 'install'; do
	while read -r file; do
		# evaluate any bash variables used
		eval "file=\"$(sed "s/^\(['\"]\)\(.*\)\1\$/\2/" <<< "$file")\""
		needsversioning+=("$file")
	done < <(sed -n "s/^[[:space:]]*$i=//p" PKGBUILD)
done
for key in "${validpgpkeys[@]}"; do
	needsversioning+=("keys/pgp/$key.asc")
done

# assert that they really are controlled by SVN
if (( ${#needsversioning[*]} )); then
	# svn status's output is only two columns when the status is unknown
	while read -r status filename; do
		[[ $status = '?' ]] && unversioned+=("$filename")
	done < <(svn status -v "${needsversioning[@]}")
	(( ${#unversioned[*]} )) && die "%s is not under version control" "${unversioned[@]}"
fi

rsyncopts=(-e ssh -p '--chmod=ug=rw,o=r' -c -h -L --progress --partial -y)
archreleaseopts=()
while getopts ':l:a:s:f' flag; do
	case $flag in
		f) archreleaseopts+=('-f') ;;
		s) server=$OPTARG ;;
		l) rsyncopts+=("--bwlimit=$OPTARG") ;;
		a) commit_arch=$OPTARG ;;
		:) die "Option requires an argument -- '%s'" "$OPTARG" ;;
		\?) die "Invalid option -- '%s'" "$OPTARG" ;;
	esac
done
shift $(( OPTIND - 1 ))

# check packages for validity
for _arch in "${arch[@]}"; do
	if [[ -n $commit_arch && ${_arch} != "$commit_arch" ]]; then
		continue
	fi
	for _pkgname in "${pkgname[@]}"; do
		fullver=$(get_full_version "$_pkgname")

		if pkgfile=$(find_cached_package "$_pkgname" "$fullver" "$_arch"); then
			check_package_validity "$pkgfile"
		fi
	done

	fullver=$(get_full_version "$pkgbase")
	if pkgfile=$(find_cached_package "$pkgbase-debug" "$fullver" "$_arch"); then
		check_package_validity "$pkgfile"
	fi
done

if [[ -z $server ]]; then
	server='repos.archlinux.org'
fi

if [[ -n $(svn status -q) ]]; then
	msgtemplate="upgpkg: $pkgbase $(get_full_version)"
	if [[ -n $1 ]]; then
		stat_busy 'Committing changes to trunk'
		svn commit -q -m "${msgtemplate}: ${1}" || die
		stat_done
	else
		msgfile="$(mktemp)"
		echo "$msgtemplate" > "$msgfile"
		if [[ -n $SVN_EDITOR ]]; then
			$SVN_EDITOR "$msgfile"
		elif [[ -n $VISUAL ]]; then
			$VISUAL "$msgfile"
		elif [[ -n $EDITOR ]]; then
			$EDITOR "$msgfile"
		else
			vi "$msgfile"
		fi
		[[ -s $msgfile ]] || die
		stat_busy 'Committing changes to trunk'
		svn commit -q -F "$msgfile" || die
		unlink "$msgfile"
		stat_done
	fi
fi

declare -a uploads
declare -a commit_arches
declare -a skip_arches

for _arch in "${arch[@]}"; do
	if [[ -n $commit_arch && ${_arch} != "$commit_arch" ]]; then
		skip_arches+=("$_arch")
		continue
	fi

	for _pkgname in "${pkgname[@]}"; do
		fullver=$(get_full_version "$_pkgname")
		if ! pkgfile=$(find_cached_package "$_pkgname" "$fullver" "${_arch}"); then
			warning "Skipping %s: failed to locate package file" "$_pkgname-$fullver-$_arch"
			skip_arches+=("$_arch")
			continue 2
		fi
		uploads+=("$pkgfile")
	done

	fullver=$(get_full_version "$pkgbase")
	if ! pkgfile=$(find_cached_package "$pkgbase-debug" "$fullver" "$_arch"); then
		continue
	fi
	if ! is_debug_package "$pkgfile"; then
		continue
	fi
	uploads+=("$pkgfile")
done

for pkgfile in "${uploads[@]}"; do
	sigfile="${pkgfile}.sig"
	if [[ ! -f $sigfile ]]; then
		msg "Signing package %s..." "${pkgfile}"
		if [[ -n $GPGKEY ]]; then
			SIGNWITHKEY=(-u "${GPGKEY}")
		fi
		gpg --detach-sign --use-agent --no-armor "${SIGNWITHKEY[@]}" "${pkgfile}" || die
	fi
	if ! gpg --verify "$sigfile" "$pkgfile" >/dev/null 2>&1; then
		die "Signature %s is incorrect!" "$sigfile"
	fi
	uploads+=("$sigfile")
done

for _arch in "${arch[@]}"; do
	if ! in_array "$_arch" "${skip_arches[@]}"; then
		commit_arches+=("$_arch")
	fi
done

if [[ ${#commit_arches[*]} -gt 0 ]]; then
	archrelease "${archreleaseopts[@]}" "${commit_arches[@]/#/$repo-}" || die
fi

if [[ ${#uploads[*]} -gt 0 ]]; then
	new_uploads=()

	# convert to absolute paths so rsync can work with colons (epoch)
	while read -r -d '' upload; do
		new_uploads+=("$upload")
	done < <(realpath -z "${uploads[@]}")

	uploads=("${new_uploads[@]}")
	unset new_uploads
	msg 'Uploading all package and signature files'
	rsync "${rsyncopts[@]}" "${uploads[@]}" "$server:staging/$repo/" || die
fi

if [[ "${arch[*]}" == 'any' ]]; then
	if [[ -d ../repos/$repo-x86_64 ]]; then
		pushd ../repos/ >/dev/null
		stat_busy "Removing %s" "$repo-x86_64"
		svn rm -q "$repo-x86_64"
		svn commit -q -m "Removed $repo-x86_64 for $pkgname"
		stat_done
		popd >/dev/null
	fi
else
	if [[ -d ../repos/$repo-any ]]; then
		pushd ../repos/ >/dev/null
		stat_busy "Removing %s" "$repo-any"
		svn rm -q "$repo-any"
		svn commit -q -m "Removed $repo-any for $pkgname"
		stat_done
		popd >/dev/null
	fi
fi
