#!/usr/bin/env bash
#
# dagpkg - create a directed graph of package dependencies and build
#          them in topological order

# Copyright (C) 2014 Nicolás Reynolds <fauno@parabola.nu>
# Copyright (C) 2014 Michał Masłowski <mtjm@mtjm.eu>
# Copyright (C) 2017, 2024 Luke T. Shumaker <lukeshu@parabola.nu>
#
# License: GNU GPLv3+
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
set -e

. "$(librelib messages)"
. "$(librelib conf)"

# Globals:
# - temp_dir
# - name (sort of, it's also local to visit_pkgbuild() )
# - prev
# - I
# - marks
# - various PKGBUILD variables:
#   - pkgbase/pkgname
#   - epoch/pkgver/pkgrel
#   - arch
#   - {,make,check}depends

# End inmediately but print an useful message
trap_exit() {
	local signal=$1
	shift
	local msg=("$@")
	term_title "error!"
	echo
	error "(%s) %s (leftovers on %s)" \
		"${0##*/}" "$(print "${msg[@]}")" "${temp_dir}"
	trap -- "$signal"
	kill "-$signal" "$$"
}

source_pkgbuild() {
	# Source this PKGBUILD, if it doesn't exist, exit
	if ! load_PKGBUILD &>/dev/null; then
		error "No PKGBUILD in %s" "$PWD"
		exit $EXIT_FAILURE
	fi

	# Save resources
	# This is intentionally less exhaustive than unset_PKGBUILD()
	# XXX: document which things we actually *want* to not be unset.
	unset pkgdesc license groups backup install md5sums sha1sums \
		sha256sums source options &>/dev/null

	unset build package &>/dev/null

	local _pkg
	for _pkg in "${pkgname[@]}"; do
		unset "package_${_pkg}" &>/dev/null || true
	done

	# This is the name of the package
	name="${pkgbase:-${pkgname[0]}}"
}

# Visit a PKGBUILD for graph building.
visit_pkgbuild() {
	log=$1  # The file to appund our results to
	prev=$2 # The name of the previous package

	local name
	source_pkgbuild

	# If it's already built we don't bother
	if is_built "${pkgname[0]}" "$(get_full_version "${pkgname[0]}")"; then
		return
	fi

	# Detect cycle or already visited package
	case "${marks[$name]:-0}" in
		1)
			msg2 "cycle found with %s depending on %s" "$prev" "$name"
			exit $EXIT_FAILURE
			;;
		2)
			return
			;;
	esac

	msg "%s (%s)" "${name}" "${prev}"

	if ! in_array "${CARCH}" "${arch[@]}"; then
		warning "%s isn't ported to %s yet" "${name}" "${CARCH}"
	fi

	# If the envvar I contains this package, ignore it and exit
	if in_array "$name" $I; then
		msg2 "%s ignored" "${name}"
		return
	fi

	# Mark the package as being visited
	marks[$name]=1

	# Recurse into dependencies
	local d w
	for d in "${depends[@]}" "${makedepends[@]}" "${checkdepends[@]}"; do
		# Cleanup dependency versions
		d=${d%%[<>=]*}

		# Where's the pkgbuild?
		w=$(toru-where "$d")

		# Skip if not available
		test -z "$w" && continue

		# Go to this dir
		pushd "$w" &>/dev/null

		visit_pkgbuild "$log" "$name"

		popd &>/dev/null
	done

	# Mark the package as finished
	marks[$name]=2
	# Append it to the reversed list of packages to build.
	echo "$name" >>"${log}"
}

usage() {
	print 'Usage: %s [OPTIONS]' "${0##*/}"
	print 'Build a package and its dependencies.'
	echo
	print 'Options:'
	flag \
		'-h, --help' 'Show this message'
}

main() {
	local args mode=run
	if ! args="$(getopt -n "${0##*/}" -o 'h' -l 'help' -- "$@")"; then
		mode=errusage
	else
		eval "set -- $args"
		local flag
		while true; do
			flag=$1
			shift
			case "$flag" in
				-h | --help) mode=usage ;;
				--) break ;;
				*) panic 'unhandled flag: %q' "$flag" ;;
			esac
		done
		if [[ $mode == run && $# -gt 0 ]]; then
			gnuerror 'Extra arguments: %s' "$*"
			mode=errusage
		fi
	fi
	case "$mode" in
		errusage)
			print "Try '%s --help' for more information." "${0##*/}" >&2
			exit $EXIT_INVALIDARGUMENT
			;;
		usage)
			usage
			exit $EXIT_SUCCESS
			;;
		run) : ;;
		*) panic 'invalid mode: %q' "$mode" ;;
	esac

	# Source variables from libretools
	declare -i ret=0
	load_conf libretools.conf FULLBUILDCMD || ret=$ # and HOOKPREBUILD & HOOKLOCALRELEASE, which are optional
	load_conf makepkg.conf CARCH || ret=$?
	[[ $ret == 0 ]] || exit $ret

	setup_traps trap_exit

	source_pkgbuild

	# A temporary work dir and log file
	temp_dir="${1:-$(mktemp -dt "${name}-testpkg-XXXX")}"
	local log="${temp_dir}/buildorder"

	# Mark array for DFS-based topological sort.  See
	# https://en.wikipedia.org/wiki/Topological_sort for an explanation of
	# the algorithm.  Key: package name, value: 0 for unvisited package, 1
	# during visit, 2 after visit.
	declare -gA marks

	# If we specified a work dir on the cli it means we want to skip
	# dependency graph creation and jump to build whatever is there
	if [ -z "${1}" ]; then
		# Visit the root PKGBUILD to make the graph.
		visit_pkgbuild "$log" ""
	else
		msg "Resuming build..."
	fi

	# enter work dir
	pushd "${temp_dir}" &>/dev/null
	local w
	while read -r order pkg; do
		# skip if already built
		if test -f "${pkg}/built_ok"; then
			warning "tried to build %s twice" "%{pkg}"
			continue
		fi

		# where's this package?
		w="$(toru-where "$pkg")"
		test -z "$w" && continue

		# copy to work dir if not already
		# this means you can make modifications to the pkgbuild during the
		# graph build or remove the dir after a build failure and let dagpkg
		# copy a new version
		test -d "$pkg" || cp -r "$w" "$pkg"
		pushd "$pkg" &>/dev/null

		term_title "%s(%s)" "$pkg" "$order"

		msg "Building %s" "${pkg}"

		# upgrade the system
		# this would probably have to go on HOOKPREBUILD if you're working
		# outside chroots
		sudo -E pacman -Syu --noconfirm

		# run the pre build command from libretools.conf
		if [[ -n $HOOKPREBUILD ]]; then
			${HOOKPREBUILD}
		fi

		# run the build command
		${FULLBUILDCMD}

		# Run local release hook with $1 = $repo
		if [[ -n $HOOKLOCALRELEASE ]]; then
			${HOOKLOCALRELEASE} "$(basename "$(dirname "$w")")"
		fi

		# it's built!
		touch built_ok

		popd &>/dev/null
	done < <(nl "$log")

	popd &>/dev/null
	# cleanup
	rm -rf "${log}" "${temp_dir}"

	term_title "done"
}

main "$@"
