#!/usr/bin/env bash
set -euE
# libremakepkg

# Copyright (C) 2010-2012 Nicolás Reynolds <fauno@parabola.nu>
# Copyright (C) 2010-2012 Joshua Ismael Haase Hernández (xihh) <hahj87@gmail.com>
# Copyright (C) 2012 Michał Masłowski <mtjm@mtjm.eu>
# Copyright (C) 2012-2015, 2017-2018, 2024 Luke T. Shumaker <lukeshu@parabola.nu>
# Copyright (C) 2019, 2024 Denis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>
#
# License: GNU GPLv2+
#
# This file is part of Parabola.
#
# Parabola 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 2 of the License, or
# (at your option) any later version.
#
# Parabola 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Parabola. If not, see <http://www.gnu.org/licenses/>.

. "$(librelib conf)"
. "$(librelib messages)"
. "$(librelib chroot/makechrootpkg)"

set -o pipefail
shopt -s nullglob
umask 0022

# Global variables:
readonly _indent="$(librelib indent)"
readonly INCHROOT=$([[ -f /.arch-chroot ]] && echo true || echo false)
NONET=true      # can be changed with the -N flag
ROSTARTDIR=true # can be changed with the -W flag
# {PKG,SRC,SRCPKG,LOG}DEST set at runtime by makepkg.conf
# MAKEFLAGS, PACKAGER set at runtime by makepkg.conf
# LIBREUSER, LIBREHOME are set by conf.sh
librechroot_flags=()

# Hooks ########################################################################

hook_pre_build=(:)
hook_post_build=(:)
hook_check_pkgbuild=(:)
hook_check_pkg=(:)
. "$(librelib chroot/hooks-chcleanup.sh)"
. "$(librelib chroot/hooks-check.sh)"
. "$(librelib chroot/hooks-distcc.sh)"

# Boring/mundane functions #####################################################

indent() {
	"$_indent" ' |  '
}

# Usage: exit_copy $copydir $src_owner
# End immediately, but copy log files out
exit_copy() {
	local copydir=$1
	local src_owner=$2
	if ! $INCHROOT; then
		msg "Copying log and package files out of the chroot..."
		move_products "$copydir" "$src_owner"
	fi
}

# Usage; run_hook $hookname $args...
run_hook() {
	local hookname=$1
	shift
	local hookvar="hook_${hookname}[@]"

	local fails=()

	for hook in "${!hookvar}"; do
		# The "& wait $!" trick is to prevent "||" from
		# disabling "set -e"
		{ "$hook" "$@" |& indent; } &
		wait $! || fails+=("$hook")
	done

	if [[ ${#fails[@]} -gt 0 ]]; then
		error "Failure(s) in %s: %s" "$hookname" "${fails[*]}"
		return $EXIT_FAILURE
	else
		return $EXIT_SUCCESS
	fi
}

# Usage: add_to_local_repo $copydir $pkgfiles...
add_to_local_repo() {
	local copydir=$1
	shift
	mkdir -p "$copydir/repo"
	local pkgfile
	for pkgfile in "$@"; do
		cp "$pkgfile" "$copydir/repo"
		pushd "$copydir/repo" >/dev/null
		repo-add repo.db.tar.gz "${pkgfile##*/}"
		popd >/dev/null
	done
}

hook_post_build+=('chroot_cleanup')
chroot_cleanup() {
	local copydir=$1
	rm -f -- "$copydir"/chrootbuild
}

build() (
	local copydir=$1
	local srcpkg=$2
	local repack=$3
	local makepkg_args=("${@:4}")

	local startdir
	startdir=$(mktemp -d)
	chown "$LIBREUSER:" "$startdir"
	trap "rm -rf -- ${startdir@Q}" EXIT
	sudo -u "$LIBREUSER" bsdtar -xf "$srcpkg" -C "$startdir" --strip-components 1

	local run_ynet=()
	local run_nnet=()
	if $INCHROOT; then
		if $ROSTARTDIR; then
			local _run=(sh -c "mount --bind -o ro -- ${startdir@Q} ${startdir@Q} && cd ${startdir@Q} && \$@" --)
		else
			local _run=(sh -c "cd ${startdir@Q} && \$@" --)
		fi
		run_ynet=(unshare --mount -- "${_run[@]}")
		run_nnet=(unshare --mount --net -- "${_run[@]}")
	else
		if $ROSTARTDIR; then
			librechroot_flags+=(-r "$startdir:/startdir")
		else
			librechroot_flags+=(-w "$startdir:/startdir")
		fi
		run_ynet=(librechroot "${librechroot_flags[@]}" run)
		run_nnet=(librechroot "${librechroot_flags[@]}" -N run)
	fi
	$NONET || run_nnet=("${run_ynet[@]}")

	prepare_chroot "$copydir" "$LIBREHOME" "$repack" false
	run_hook pre_build "$copydir"
	trap "run_hook post_build ${copydir@Q}; rm -rf -- ${startdir@Q}" EXIT
	"${run_nnet[@]}" /chrootbuild "${makepkg_args[@]}" </dev/null |& indent
)

# The main program #############################################################

usage() {
	print "Usage: %s [options]" "${0##*/}"
	print 'This program will build your package.'
	echo
	prose 'If run from outside of a chroot, this command will make the
	       following configuration changes in the chroot:'
	bullet 'whatever changes `librechroot` makes.'
	bullet 'set `{PKG,SRC,SRCPKG,LOG}DEST` in `/etc/makepkg.conf`'
	bullet 'set `MAKEFLAGS` and `PACKAGER` in `/etc/makepkg.conf` to reflect
	        the value outside of the chroot.'
	bullet 'create a `builduser` with the same numeric UID as the
	        invoking $SUDO_USER.'
	bullet 'let this `builduser` call `sudo pacman` without a password.'
	bullet 'set `keyserver-options` in `~builduser/.gnupg/gpg.conf`'
	bullet "copy the user's GnuPG pubring to be the \`builduser\`'s pubring"
	bullet 'add a pacman repositoriy of locally built packages'
	echo
	prose 'This command is configured both with `chroot.conf` (either in
	       `/etc/libretools.d/` or `$XDG_CONFIG_HOME/libretools/`), and with
	       makepkg.conf(5).  However, similarly to makepkg(8), it lets
	       environment variables for {SRC,SRCPKG,PKG,LOG}DEST, MAKEFLAGS and
	       PACKAGER override the settings in makepkg.conf(5).'
	echo
	prose 'The `-n` and `-l` options behave identically to librechroot, see
	       the documentation there.'
	echo
	print 'Options:'

	flag 'librechroot options:' \
		"-n <$(_ CHROOT)>" 'Name of the chroot to use' \
		"-l <$(_ COPY)>" 'Name of, or absolute path to, the chroot copy to use' \
		"-w <$(_ 'PATH[:INSIDE_PATH[:OPTIONS]]')>" 'Bind mount a file or directory, read/write' \
		"-r <$(_ 'PATH[:INSIDE_PATH[:OPTIONS]]')>" 'Bind mount a file or directory, read-only'

	flag 'libremakepkg options:' \
		'-N' "Don't disable networking during prepare(),
		                        build(), and package().  PLEASE don't use
		                        this unless you have a special reason, its
		                        use is a violation of Parabola policy." \
		'-W' "Don't make the startdir read-only.  PLEASE
		                        don't use this unless you have a special
		                        reason, its use is a violation of Parabola
		                        policy." \
		'-R' 'Repackage contents of the package without
		                        rebuilding' \
		"-S <$(_ SRCPKGFILE)>" 'Use an existing --allsource source-package' \
		'-h, --help' 'Show this message'
}

main() {
	# Initial variable values ##############################################
	local copy=$([[ $LIBREUSER == root ]] && echo copy || echo "$LIBREUSER")
	local makepkg_args=(--syncdeps --noconfirm --log --holdver --skipinteg)
	local repack=false
	local chroot=''
	local srcpkg=''

	# Parse command line options ###########################################
	local args mode=run
	if ! args="$(getopt -n "${0##*/}" -o 'n:l:w:r:NWRS:h' -l 'help' -- "$@")"; then
		mode=errusage
	else
		eval "set -- $args"
		local flag
		while true; do
			flag=$1
			shift
			case "$flag" in
				-n)
					if $INCHROOT; then
						gnuerror 'The -%s flag does not make sense inside of a chroot' "${flag#-}"
						mode=errusage
					else
						chroot=$1
					fi
					shift
					;;
				-l)
					if $INCHROOT; then
						gnuerror 'The -%s flag does not make sense inside of a chroot' "${flag#-}"
						mode=errusage
					else
						copy=$1
					fi
					shift
					;;
				-w | -r)
					if $INCHROOT; then
						gnuerror 'The -%s flag does not make sense inside of a chroot' "${flag#-}"
						mode=errusage
					else
						librechroot_flags+=("$flag" "$1")
					fi
					shift
					;;
				-N) NONET=false ;;
				-W) ROSTARTDIR=false ;;
				-R)
					repack=true
					makepkg_args+=(-R)
					;;
				-S)
					srcpkg=$1
					shift
					;;
				-h | --help) mode=usage ;;
				--) break ;;
				*) panic 'unhandled flag: %q' "$flag" ;;
			esac
		done
		if [[ $mode == run && $# != 0 ]]; then
			gnuerror 'Extra arguments: %s' "$*"
			mode=errusage
		fi
	fi
	case "$mode" in
		errusage)
			print "Try '%s --help' for more information." "${0##*/}" >&2
			return $EXIT_INVALIDARGUMENT
			;;
		usage)
			usage
			return $EXIT_SUCCESS
			;;
		run) : ;;
		*) panic 'invalid mode: %q' "$mode" ;;
	esac

	# Resolve the chroot path ##############################################
	local copydir
	if $INCHROOT; then
		copydir='/'
	else
		load_conf chroot.conf CHROOTDIR CHROOT || exit
		[[ -z ${chroot} ]] || CHROOT=$chroot
		if [[ ${copy:0:1} == / ]]; then
			copydir=$copy
		else
			copydir="${CHROOTDIR}/${CHROOT}/${copy}"
		fi
		unset CHROOTDIR CHROOTEXTRAPKG
	fi
	unset chroot

	# Load makepkg configuration ###########################################
	# Note that all of these are globals
	PKGDEST="$(get_var makepkg PKGDEST "$PWD")"
	SRCDEST="$(get_var makepkg SRCDEST "$PWD")"
	SRCPKGDEST="$(get_var makepkg SRCPKGDEST "$PWD")"
	LOGDEST="$(get_var makepkg LOGDEST "$PWD")"
	MAKEFLAGS="$(get_var makepkg MAKEFLAGS '')"
	PACKAGER="$(get_var makepkg PACKAGER '')"

	# Quick sanity check ###################################################

	if ((EUID)); then
		error "This program must be run as root"
		exit $EXIT_NOPERMISSION
	fi

	if [[ -n $srcpkg ]]; then
		if [[ ! -f $srcpkg ]]; then
			error 'Source package does not exist: %s' "$srcpkg"
			exit $EXIT_INVALIDARGUMENT
		fi
	else
		if [[ ! -f PKGBUILD ]]; then
			# This is the message used by makepkg
			error "PKGBUILD does not exist."
			exit $EXIT_FAILURE
		fi
	fi

	# Make sure that the various *DEST directories exist
	sudo -u "$LIBREUSER" mkdir -p -- "$PKGDEST" "$SRCDEST" "$SRCPKGDEST" "$LOGDEST"

	# OK, we are starting now ##############################################

	if $INCHROOT; then
		lock 9 "/build/.lock" \
			"Waiting for existing lock on build directory to be released"
	else
		librechroot_flags+=(
			-n "$CHROOT"
			-l "$copy"
		)

		# Obtain a lock on the chroot
		lock 9 "$copydir.lock" \
			"Waiting for existing lock on chroot copy to be released: [%s]" "$copy"
		# Create the chroot if it does not exist
		msg 'Initializing the chroot...'
		librechroot "${librechroot_flags[@]}" make |& indent
	fi

	# Set target CARCH
	# note that we waited until after locking/creating the chroot to do this
	CARCH="$(MAKEPKG_CONF=$copydir/etc/makepkg.conf get_var makepkg CARCH)"
	export CARCH

	# Pre-build
	msg 'Starting pre-build activities...'
	run_hook check_pkgbuild

	if [[ -n $srcpkg ]]; then
		msg 'Using existing source package %s' "$srcpkg"
		# TODO: symlink $srcpkg to ${SRCPKGDEST}/${pkgbase}-${evr}-${CARCH}${SRCEXT}
	else
		msg 'Downloading sources...'
		local srcpkgdest
		srcpkgdest="$(mktemp -d)"
		chown "$LIBREUSER:" "$srcpkgdest"
		trap "rm -rf -- ${srcpkgdest@Q}" EXIT
		SRCPKGDEST="$srcpkgdest" download_sources "$copydir" "$LIBREUSER" |& indent
		srcpkg=("$srcpkgdest"/*)
		if ((${#srcpkg[@]} != 1)); then
			error 'Something went funny with makepkg --allsource'
			return $EXIT_FAILURE
		fi
		# We want to inject "-$pkgarch" in to srcpkg's filename, right before $SRCEXT
		local srcext pkgarch srcpkg_filename
		srcext="$(MAKEPKG_CONF=$copydir/etc/makepkg.conf get_var makepkg SRCEXT)"
		if [[ "$(bsdtar xfO "$srcpkg" --include='*/.SRCINFO' | grep $'\tarch =')" == $'\tarch = any' ]]; then
			pkgarch=any
		else
			pkgarch=$CARCH
		fi
		srcpkg_filename=${srcpkg##*/}
		srcpkg_filename=${srcpkg_filename%"${srcext}"}-${pkgarch}${srcext}
		mv -T -- "$srcpkg" "$SRCPKGDEST/${srcpkg_filename}"
		srcpkg="$SRCPKGDEST/${srcpkg_filename}"
		rmdir -- "${srcpkgdest}"
		trap EXIT
	fi

	# Build
	msg 'Starting to build the package...'
	trap "exit_copy '$copydir' '$LIBREUSER'" EXIT
	build "$copydir" "$srcpkg" "$repack" "${makepkg_args[@]}"

	# Post-build
	msg 'Starting post-build activities...'
	run_hook check_pkg
	add_to_local_repo "$copydir" "$copydir"/pkgdest/*.pkg.tar* |& indent
}

main "$@"
