#!/usr/bin/env bash
# librefetch
#
# Copyright (C) 2013-2018, 2024 Luke T. Shumaker <lukeshu@parabola.nu>
# Copyright (C) 2020, 2024 Bill Auger <mr.j.spam.me@gmail.com>
#
# License: GNU GPLv3+
#
# This file is part of LibreFetch.
#
# LibreFetch 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.
#
# LibreFetch 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 LibreFetch. If not, see <http://www.gnu.org/licenses/>.

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

readonly _indent="$(librelib indent)"

tmpfiles=()
tmpdirs=()
trap 'rm -f -- "${tmpfiles[@]}"; rm -rf -- "${tmpdirs[@]}"' EXIT

cmd=${0##*/}
usage() {
	print "Usage: %s [OPTIONS] SOURCE_URL [OUTPUT_FILE]" "$cmd"
	print "   or: %s -[g|S|M|h]" "$cmd"
	print "Downloads or creates a liberated source tarball."
	echo
	prose "The default mode is to create OUTPUT_FILE, first by trying
	       download mode, then create mode."
	echo
	prose "If OUTPUT_FILE isn't specified, it defaults to the non-directory
	       part of SOURCE_URL, in the current directory."
	echo
	prose "Unless '-C' is specified, if SOURCE_URL does not begin with a
	       configured mirror, create mode is inhibited."
	echo
	prose "In download mode, it simply tries to download SOURCE_URL.  At the
	       beginning of a URL, 'libre://' expands to the first configured
	       mirror."
	echo
	prose "In create mode, it either looks at a build script and uses that
	       to create the source tarball, or it uses GPG to create a
	       signature (if OUTPUT_FILE ends with \`.sig\` or \`.sig.part\`).
	       If it is using GPG to create a signature, but the file that it is
	       trying to sign doesn't exist yet, it recurses on itself to first
	       create that file.  SOURCE_URL is ignored, except that it is used
	       to set the default value of OUTPUT_FILE, and that it may be used
	       when recursing."
	echo
	prose "The default build script is 'PKGBUILD', or 'SRCBUILD' if it
	       exists."
	echo
	prose 'Other options, if they are valid `makepkg` options, are passed
	       straight to makepkg.'
	echo
	print "Example usage:"
	print '  $ %s https://repo.parabola.nu/other/mypackage/mypackage-1.0.tar.gz' "$cmd"
	echo
	print "Options:"
	flag \
		'Download/create settings:' \
		"-C" "Force create mode (don't download)" \
		"-D" "Force download mode (don't create)" \
		"-p <$(_ FILE)>" "Use an alternate build script (instead of
		                    'PKGBUILD').  If an SRCBUILD exists in the same
		                    directory, it is used instead" \
		'...' 'Any options documented in `makepkg -h` are
		                    accepted, and in create mode are passed to
		                    the modified copy of `makepkg`' \
		'Alternate modes:' \
		"-g, --geninteg" "Generate integrity checks for source files" \
		"-S, --srcbuild" "Print the effective build script (SRCBUILD)" \
		"-M, --makepkg" "Generate and print the location of the
		                    effective makepkg script" \
		"-h, --help" "Show this message"
}

main() {
	local mode BUILDFILE makepkg_opts extra_opts
	parse_options "$@"
	doit
}

# `doit` is the "inner" main function; with option parsing and such
# already performed.  Instead of taking arguments, it makes use of a
# few global variables:
#
#  - $mode={errusage|usage|download-create|create|download|checksums|srcbuild|makepkg}
#  - $BUILDFILE=$(realpath -lm -- PKGBUILD)
#  - $makepkg_opts=(flags to pass to makepkg)
#  - $extra_opts=(positional arguments)
#
# $extra_opts is empty for mode={checksums|srcbuild|makepkg}, and is
# "SRC_URL [DST_FILENAME]" for mode={create|download|download-create}.
doit() {

	# Mode: errusage #######################################################

	if [[ $mode == errusage ]]; then
		print "Try '%s --help' for more information." "$cmd" >&2
		exit $EXIT_INVALIDARGUMENT
	fi

	# Mode: usage ##########################################################

	if [[ $mode == usage ]]; then
		usage
		exit $EXIT_SUCCESS
	fi

	# Mode: makepkg ########################################################

	makepkg="$(modified_makepkg)"
	if [[ $mode == makepkg ]]; then
		printf '%s\n' "$makepkg"
		exit $EXIT_SUCCESS
	fi
	tmpdirs+=("${makepkg%/*}")

	# (generate srcbuild) ##################################################

	local BUILDFILEDIR="${BUILDFILE%/*}"
	if [[ -f "${BUILDFILEDIR}/SRCBUILD" ]]; then
		BUILDFILE="${BUILDFILEDIR}/SRCBUILD"
	fi
	if [[ ! -f $BUILDFILE ]]; then
		error "%s does not exist." "$BUILDFILE"
		exit $EXIT_FAILURE
	fi
	case "$BUILDFILE" in
		*/SRCBUILD) srcbuild="$(modified_srcbuild "$BUILDFILE")" ;;
		*) srcbuild="$(modified_pkgbuild "$BUILDFILE")" ;;
	esac
	tmpfiles+=("$srcbuild")

	# Mode: checksums ######################################################

	if [[ $mode == checksums ]]; then
		"$makepkg" "${makepkg_opts[@]}" -g -p "$srcbuild" |
			case ${BUILDFILE##*/} in
				PKGBUILD) sed -e 's/^[a-z]/mk&/' -e 's/^\s/  &/' ;;
				SRCBUILD) cat ;;
			esac
		exit $EXIT_SUCCESS
	fi

	# Mode: srcbuild #######################################################

	if [[ $mode == srcbuild ]]; then
		cat "$srcbuild"
		exit $EXIT_SUCCESS
	fi

	# (parse extra_opts) ###################################################

	local src="${extra_opts[0]}"
	local dst="${extra_opts[1]:-${src##*/}}"

	# Don't canonicalize $src unless [[ $mode == *download* ]] and
	# we've validated that $MIRRORS is configured.

	# Canonicalize $dst.
	dst="$(realpath -Lm -- "$dst")"

	# Mode: download #######################################################

	if [[ $mode == *download* ]]; then
		load_conf librefetch.conf MIRRORS DOWNLOADER || exit

		# Canonicalize $src.
		if [[ $src == libre://* ]]; then
			src="${MIRRORS[0]}/${src#libre://}"
		fi

		# Check to see if $src is a candidate for 'create' mode.
		local inmirror=false
		local mirror
		for mirror in "${MIRRORS[@]}"; do
			if [[ $src == "$mirror"* ]]; then
				inmirror=true
				break
			fi
		done
		if ! $inmirror; then
			# Inhibit 'create'.
			mode=download
		fi

		local dlcmd="${DOWNLOADER}"
		[[ $dlcmd == *%u* ]] || dlcmd="$dlcmd %u"
		dlcmd="${dlcmd//\%o/\"\$dst\"}"
		dlcmd="${dlcmd//\%u/\"\$src\"}"

		if (
			set -o pipefail
			{ eval "$dlcmd"; } |& if $inmirror; then indent "$(_ 'download')"; else cat; fi >&2
		); then
			exit $EXIT_SUCCESS
		fi
	fi

	# Mode: create #########################################################

	if [[ $mode == *create* ]]; then
		local base_dst=${dst%.part}
		local suffix=${dst#"$base_dst"}

		if [[ $base_dst == *.sig ]]; then
			if ! [[ -e ${base_dst%.sig} ]]; then
				extra_opts=("${src%.sig}" "${base_dst%.sig}")
				doit || exit
			fi
			(
				set -o pipefail
				create_signature "${base_dst%.sig}" |& indent "$(_ 'create')" >&2
			) || exit
			if [[ -n $suffix ]]; then
				mv -f "$base_dst" "$dst"
			fi
		else
			export PKGEXT=${base_dst##*/} # so the file uses the correct compression
			export PKGDEST=${dst%/*}
			export pkg_file=$dst

			(
				cd "$BUILDFILEDIR"
				set -o pipefail
				"$makepkg" --log "${makepkg_opts[@]}" -p "$srcbuild" |& indent "$(_ 'create')" >&2
			) || exit
		fi
	fi
}

# Usage: parse_options "$@"
#
# Sets the variables:
#
#  - $mode={download-create|create|download|checksums|srcbuild|makepkg|help}
#  - $BUILDFILE=$(realpath -lm -- PKGBUILD)
#  - $makepkg_opts=(flags to pass to makepkg)
#  - $extra_opts=(positional arguments)
#
# $extra_opts is empty for mode={checksums|srcbuild|makepkg}, and is
# "SRC_URL [DST_FILENAME]" for mode={create|download|download-create}.
parse_options() {
	BUILDFILE="$(realpath -Lm PKGBUILD)"
	makepkg_opts=()
	extra_opts=()
	mode=download-create

	local {shrt,long}{1,2}

	# makepkg options
	local makepkg_orig
	makepkg_orig="$(type -p makepkg)"
	shrt1=($(LC_ALL=C "${makepkg_orig}" -h | sed -En 's/^ +-(.)(,| [^<]).*/\1/p'))
	shrt2=($(LC_ALL=C "${makepkg_orig}" -h | sed -En 's/^ +-(.) <.*/\1/p'))
	long1=($(LC_ALL=C "${makepkg_orig}" -h | sed -En -e 's/^ +(-., )?--(\S*) [^<].*/\2/p'))
	long2=($(LC_ALL=C "${makepkg_orig}" -h | sed -En 's/^ +--(\S*) <.*/\1/p'))

	# librefetch options
	shrt1+=(C D g S M h)
	shrt2+=(p)
	long1+=(geninteg srcbuild makepkg help)
	long2+=()

	# Feed the options through getopt (sanitize them).
	local shrt long args
	shrt="$({
		printf '%s\0' "${shrt1[@]}"
		printf '%s:\0' "${shrt2[@]}"
	} | sort -zu | xargs -0 printf '%s')"
	long="$({
		printf '%s\0' "${long1[@]}"
		printf '%s:\0' "${long2[@]}"
	} | sort -zu | xargs -0 printf '%s,')"
	args="$(getopt -n "$cmd" -o "$shrt" -l "${long%,}" -- "$@")" || mode=errusage
	eval "set -- $args"
	unset shrt long args

	# Parse the options.
	if [[ $mode != errusage ]]; then
		local opt optarg have_optarg
		while [[ $# -gt 0 ]]; do
			opt=$1
			shift
			have_optarg=false

			if in_array "${opt#--}" "${long2[@]}" || in_array "${opt#-}" "${shrt2[@]}"; then
				optarg=$1
				shift
				have_optarg=true
			fi

			case "$opt" in
				-C) [[ $mode == usage ]] || mode=create ;;
				-D) [[ $mode == usage ]] || mode=download ;;
				-g | --geninteg) [[ $mode == usage ]] || mode=checksums ;;
				-S | --srcbuild) [[ $mode == usage ]] || mode=srcbuild ;;
				-M | --makepkg) [[ $mode == usage ]] || mode=makepkg ;;
				-p) BUILDFILE="$(realpath -Lm -- "$optarg")" ;;
				-h | --help) mode=usage ;;
				--) break ;;
				*)
					makepkg_opts+=("$opt")
					if $have_optarg; then makepkg_opts+=("$optarg"); fi
					;;
			esac
		done
		extra_opts+=("$@")
	fi

	# Check the number of extra_opts.
	case "$mode" in
		usage | errusage) # Don't worry about it.
			: ;;
		checksums | srcbuild | makepkg) # Don't take any extra arguments.
			if [[ ${#extra_opts[@]} != 0 ]]; then
				gnuerror 'found extra non-flag arguments: %s' "${extra_opts[*]}"
				mode=errusage
			fi
			;;
		download | create | download-create) # Take 1 or 2 extra arguments.
			if [[ ${#extra_opts[@]} != 1 ]] && [[ ${#extra_opts[@]} != 2 ]]; then
				gnuerror '%d non-flag arguments found, expected 1 or 2: %s' "${#extra_opts[@]}" "${extra_opts[*]}"
				mode=errusage
			fi
			;;
		*) panic 'invalid mode: %q' "$mode" ;;
	esac
}

# Modify makepkg ###############################################################

modified_makepkg() {
	local dir
	dir="$(mktemp --tmpdir --directory "${cmd}.XXXXXXXXXXX.makepkg")"
	make -s -f "$(librelib librefetchdir/Makefile)" new="$dir"
	realpath -es "$dir/makepkg"
}

# Modify PKGBUILD ##############################################################

# `$pkgbuild_append` is a string to be appended to the PKGBUILD.
#
# Any "temporary" variables in it should...
#  - ... be declared with `declare` (so that if it is evaluated within a
#    function they don't become global variables, but still allowing
#    it to be evaluated in the global scope).
#  - ... start with "_librefetch_" to avoid any possible conflicts
#    with makepkg or with the PKGBUILD.
pkgbuild_append='
declare _librefetch_i

######################################################################
# Set the normal PKGBUILD variables
######################################################################

# Do not do split packages.
declare _librefetch_pkgname="${pkgbase:-$pkgname}"
for _librefetch_i in "${pkgname[@]}"; do
	unset -f "package_${_librefetch_i}"
done
unset pkgname
pkgname=$_librefetch_pkgname

# Set sourcecode-related variables.
source=("${mksource[@]}")       ; unset "source_${CARCH}"
noextract=("${mknoextract[@]}")
validpgpkeys=("${mkvalidpgpkeys[@]}")
for _librefetch_i in "${known_hash_algos[@]}"; do
	eval "${_librefetch_i}sums=(\"\${mk${_librefetch_i}sums[@]}\")"
	unset "${_librefetch_i}sums_${CARCH}"
done

# Set dependency-related variables.
depends=()                      ; unset "depends_${CARCH}"
checkdepends=()                 ; unset "checkdepends_${CARCH}"
makedepends=("${mkdepends[@]}") ; unset "makedepends_${CARCH}"

# Clear unused variables.
backup=()
unset install

######################################################################
# Set variables for adjusting makepkg behavior
######################################################################

# See packaging_options in the makepkg source.

options=(!strip docs libtool staticlibs emptydirs !zipman !debug purge)
PURGE_TARGETS=(.bzr/ .cvs/ .git/ .hg/ .svn/ .makepkg/)

######################################################################
# Define the normal PKGBUILD functions
######################################################################

if ! declare -f mksource >/dev/null; then
	mksource() { :; }
fi
prepare() { :; }
build() { mksource; }
check() { :; }
package() { cp -a "$srcdir"/*/ "$pkgdir/"; }
'

modified_pkgbuild() {
	local pkgbuild=$1
	local srcbuild
	srcbuild="$(mktemp "${pkgbuild%/*}/${cmd}.XXXXXXXXXXX.PKGBUILD.to.SRCBUILD")"
	printf '%s' "$pkgbuild_append" | cat "$pkgbuild" - >"$srcbuild"
	printf '%s\n' "$srcbuild"
}

# Modify SRCBUILD ##############################################################

modified_srcbuild() {
	local orig=$1
	local new
	new="$(mktemp "${orig%/*}/${cmd}.XXXXXXXXXXX.SRCBUILD.to.SRCBUILD")"
	sed -e '/PKGDEST=/d' -e '/PKGEXT=/d' <"$orig" >"$new"
	printf '%s\n' "$new"
}

# Utility functions ############################################################

indent() {
	"$_indent" "librefetch: $1: "
}

create_signature() {
	local makepkg library
	makepkg=$(type -p makepkg)
	library=$(
		eval "$(grep MAKEPKG_LIBRARY= "$makepkg")"
		echo "$MAKEPKG_LIBRARY"
	)

	msg 'Signing source...'
	_p bash -c "source ${library@Q}/integrity/generate_signature.sh; create_signature ${1@Q} || exit 1"
}

################################################################################

main "$@"
