#!/bin/bash
#
# pkgmeek - a drop-in replacement for pkgmk
#     based on upkgmk (c) 2018 therealfun
#    and pkgmk (c) 2004--2022 Per Liden et al.
# Distributed under the same license, 
# https://crux.nu/gitweb/?p=tools/pkgutils.git;a=blob_plain;f=COPYING;hb=HEAD

main() {
######################## main routine ################################
local o_ignored pkg_dir src_dir work _local_ here url u f TARGET pkg_utd
local errDL=0; local errUZ=0;
parse_options "$@"
[ "$PKGMK_RECURSIVE" = "yes" ] && recursive "$@"
#
# Exit early if cleaning was requested
#
if [ "$PKGMK_CLEAN" = "yes" ]; then
	[ "$PKGMK_MTIME_ONLY" = "yes" ] && o_ignored="'-utd' "
	[ "$PKGMK_UPDATE_FOOTPRINT" = "yes" ] && o_ignored+="'-uf' "
	[ "$PKGMK_CHECK_SIG" = "yes" ] && o_ignored+="'-cs' "
	[ "$PKGMK_REFRESH_SIG" = "yes" ] && refresh_signature
	[ "$o_ignored" = "" ] || { warning "option -c nullifies these requested options:";
	info "$o_ignored"; }
	exec $PRTWASH_COMMAND -p -s -q "$(pwd)"
fi
#
# Read the Pkgfile to determine what to do next. But first ensure that
# it came from a trusted source (FS#1851)
#
[ "$PKGMK_UPDATE_SIG" = "yes" ] && [ -e ./Pkgfile ] || validate_pkgfile || exit $E_PKGFILE
[ -f .32bit ] && PGKMK_ARCH=32 || PKGMK_ARCH=64
. "Pkgfile"; . "$PKGMK_CONF"

# respect the settings for centralized source and package directories ...
[ -v pkg_dir  ] || pkg_dir="$PKGMK_PACKAGE_DIR"/
[ -w "$pkg_dir" ] || pkg_dir="$(pwd)"/
[ -v src_dir  ] || src_dir="$PKGMK_SOURCE_DIR"
[ -w "$src_dir" ] || src_dir=

# ... and for WORK_DIR in case the user wants to build in RAM
[ -v PKGMK_WORK_DIR ] && work="$PKGMK_WORK_DIR"

# Let the main routine and subsequent subroutines know what filenames
# they should expect to see on disk.
package="${name}#${version}-${release}.pkg.tar.${PKGMK_COMPRESSION_MODE}"
declare -a _local_
for (( s=0; s<${#source[@]}; s++ )); do
	[[ ${source[$s]} =~ ^(http|https|ssh|ftp|git)://.*/(.+)$ ]] && \
		_local_[$s]="${BASH_REMATCH[2]%.git*}" || _local_[$s]="${source[$s]}"
	[ -z "${renames[$s]}" ] || [ "${renames[$s]}" = "SKIP" ] || \
		_local_[$s]="${renames[$s]}"
done
# Example: source = ( https://gitlab.com/demo-user/cool-project.git#0.4.9
#                     https://dev.big-corp.com/src/needed-library.tgz
#                     random.patch )
#         _local_ = ( cool-project
#                     needed-library.zgt  <-- renamed to prevent automatic unpacking
#                     random.patch )

# The effective user should at least have write permissions on $PWD
[ -w "$(dirname "$work")" ] || work="$(pwd)"/work
rm -rf "$work"; mkdir -p "$work"/{src,pkg} && cd "$work"

# Skip the retrieval of sources if the user only asked for '-utd'
[ "$PKGMK_MTIME_ONLY" = "yes" ] || { for (( u=0; u<${#_local_[@]}; u++ )); do
here="${_local_[$u]}"; url="${source[$u]}";
# at least one of the following commands should put a file of the
# appropriate name in the current directory
[ -e "$here" ] || [ "$src_dir" = "" ] || [ ! -e "$src_dir/$here" ] || ln -sf "$src_dir/$here" ;
[ -e "$here" ] || [ ! -e "$PKGMK_ROOT/$here" ] || ln -sf "$PKGMK_ROOT/$here" . ;
[ -e "$here" ] || fetch_url "$url" "$here"; # <-- should also be able to resume partial downloads
[ -e "$here" ] || { error "failed to download $here. Check connection and try again."; errDL+=1; }
done ; }

[ $errDL = 0 ] && { [ "$PKGMK_MTIME_ONLY" = "yes" ] || \
	info "Successfully obtained all needed source files."; } || exit $E_DOWNLOAD

# Can stop here if the user asked for '-do', but honor any requests for '-um', '-uf',
# '-us', '-eo', or '-utd' by proceeding to those steps
[ "$PKGMK_DOWNLOAD_ONLY" = "no" ] || [ "$PKGMK_UPDATE_MD5" = "yes" ] || \
	[ "$PKGMK_UPDATE_FOOTPRINT" = "yes" ] || [ "$PKGMK_UPDATE_SIG" = "yes" ] || \
	[ "$PKGMK_EXTRACT_ONLY" = "yes" ] || [ "$PKGMK_MTIME_ONLY" = "yes" ] || exit 0

# If the user only asked for '-utd', perform the check using the sources that do exist.
check_pkg_mtime; pkg_utd=$?
[ "$PKGMK_MTIME_ONLY" = "no" ] || [ "$PKGMK_CHECK_SIG" = "yes" ] || \
		[ "$PKGMK_FORCE" = "yes" ] || exit $pkg_utd

# Take into account all the actions that can be done with a previously built package,
# or with a full set of sources
[ "$pkg_utd" = 0 ] || [ "$PKGMK_FORCE" = "yes" ] || [ "$PKGMK_UPDATE_FOOTPRINT" = "yes" ] \
	|| [ "$PKGMK_UPDATE_SIG" = "yes" ] || [ "$PKGMK_CHECK_SIG" = "yes" ] \
	|| [ "$PKGMK_UPDATE_MD5" = "yes" ] || [ "$PKGMK_EXTRACT_ONLY" = "yes" ] \
	|| { info "$package is up to date, use '-f' to force a rebuild."; exit 0; }

# Silence the progress report if the user never intended to proceed with unpacking
[ "$pkg_utd" = 1 ] || [[ "$PKGMK_CHECK_SIG $PKGMK_UPDATE_SIG" =~ yes ]] || \
	[[ "$PKGMK_UPDATE_FOOTPRINT $PKGMK_UPDATE_MD5" =~ yes ]] || \
	[[ "$PKGMK_IGNORE_SIG" = "yes" ]] || echo "Checking signatures before unpacking..."

# The option -uf is meant to be used AFTER a previous invocation of pkgmeek has
# alerted the user to a footprint mismatch. The options -us|-um are likewise meant
# to update the signature|md5sum, which only requires the sources and the footprint.
if [ "$PKGMK_UPDATE_FOOTPRINT" = "yes" ]; then
	[ -f "$pkg_dir$package" ] || \
		{ error "unable to update footprint. File '$package' not found.";
		exit "$E_MANIFEST"; }
	[ "$pkg_utd" = 1 ] || [ "$PKGMK_FORCE" = "yes" ] || \
		{ error "outdated package. Use '-f' to force the footprint update."; 
		exit "$E_MANIFEST"; }
	cat_manifest footprint > "$PKGMK_ROOT/.footprint" && info "footprint created." \
	|| { error "Failed to write the footprint."; exit "$E_DIRPERM"; }
fi
[ "$PKGMK_UPDATE_SIG" = "no" ] || \
	{ cat_signature > "$PKGMK_ROOT/.signature" &&
		info "Signature successfully created."; } || \
	{ cat_manifest md5sum > "$PKGMK_ROOT"/.md5sum &&
		warning "Signature creation failed, falling back to md5sums."; } \
	|| { info "Could not create signatures or md5sums."; exit "$E_DIRPERM"; }
[ "$PKGMK_UPDATE_MD5" = "no" ] || \
	{ cat_manifest md5sum > "$PKGMK_ROOT"/.md5sum && info "md5sum updated."; } || \
	{ info "Could not create requested md5sums."; exit "$E_DIRPERM"; }

# Exit after fulfilling any *explicit* requests for (signed) manifests
# (decision regarding the work directory is retained from the first run of pkgmeek)
[ "$PKGMK_UPDATE_FOOTPRINT" = "yes" ] || [ "$PKGMK_UPDATE_SIG" = "yes" ] || \
	[ "$PKGMK_UPDATE_MD5" = "yes" ] && exit 0

# All the sources should be here by now, let's verify that we can trust them.
readonly cs_fail_msg="Use '--ignore-signature' to override, if you have determined integrity by other means."
[ "$PKGMK_IGNORE_SIG" = "yes" ] || { check_signature "pre-build" | parse_signify_output;
case $? in
	0) info "Sources successfully authenticated." ;;
	1) error "Signature file missing or corrupted." ; echo "$cs_fail_msg" ;;
	2) error "Failed to authenticate remote sources using signify." ; echo "$cs_fail_msg" ;;
esac; }
[ "$PKGMK_CHECK_SIG" = "no" ] || exit 0 # no need to continue if the user only requested -cs

if [ "$pkg_utd" = 0 ] || [ "$PKGMK_FORCE" = "yes" ]; then
	for (( u=0; u<${#_local_[@]}; u++ )) ; do
	here="${_local_[$u]}"
	case "$here" in
		*.tar|*.tar.gz|*.tar.Z|*.tgz|*.tar.bz2|*.tbz2|*.tar.xz|*.txz|*.tar.lzma|*.tar.lz|*.7z|*.zip|*.rpm)
			bsdtar -p -o -C src -xf "$here" || errUZ+=1 ;;
		*)
			cp -r "$here" src/ ;;
	esac
	done

	[ $errUZ = 0 ] && info "Sources successfully unpacked." || \
	{ error "Failed to unpack all sources."; exit "$E_UNPACK"; }
	[ "$PKGMK_EXTRACT_ONLY" = "no" ] || exit 0

# The actual build step! (use fakeroot when building daemon ports as an ordinary user,
#  otherwise the owner and group might not be correct)
	(SRC=$(pwd)/src; PKG=$(pwd)/pkg; cd src; set -e -x; build)
	if [ $? = 0 ]; then
		echo "Build successful. Moving on to compression."
	else
		error "Unsuccessful build!"; cleanup_work; exit "$E_BUILD"
	fi

	[ -f "$PKGMK_ROOT/.nostrip" ] && ns_filter="| grep -v -f $PKGMK_ROOT/.nostrip"
	find pkg -type f $ns_filter | while read -r f; do
		case $(file -b "$f") in
		*ELF*executable*not\ stripped*)     strip --strip-all      "$f" ;;
		*ELF*shared\ object*not\ stripped*) strip --strip-unneeded "$f" ;;
		current\ ar\ archive)               strip --strip-debug    "$f" ;;
		esac
	done

	find pkg -type f -path "*/man/man*/*" | grep -v '.gz$' | xargs -r -I{} gzip -9 '{}'
	find pkg -xtype l -path "*/man/man*/*" | while read f; do
		TARGET="$(basename -s .gz $(readlink -n $f)).gz"; DIR="$(dirname ${f%%.gz}.gz)";
		rm -f $f; [ -e $DIR/$TARGET ] && ln -sf $TARGET ${f%%.gz}.gz
	done

	[ $UID = 0 ] || fake_uid="--uid 0 --gid 0"
	if (cd pkg; bsdtar --format=gnutar $fake_uid -cf "$pkg_dir$package" *); then
		info "Package creation successful."
	else
		error "Unable to create the compressed package $package."
		cleanup_work; exit "$E_BUILD"
	fi

	# Check the footprint of the built package, unless '-if' was given
	[ "$PKGMK_IGNORE_FOOTPRINT" = "yes" ] || check_manifest footprint

	# Clean up the work directory
	find . -maxdepth 1 -mindepth 1 -type l -delete; cleanup_work
fi  # Continue from here if the extract and build were skipped

# Install if requested. For non-root builds, only sudo and doas are supported.
# Avoid falling back on su -c! It gobbles up the options intended for pkgadd.
[ $UID = 0 ] || PKGMK_SU="sudo";
[ -z "$PKGMK_SU" ] || [ -x "$(command -v $PKGMK_SU)" ] || PKGMK_SU="/usr/bin/doas";
[ -z "$PKGMK_SU" ] || [ -x "$(command -v $PKGMK_SU)" ] \
		|| { error "Cannot run pkgadd as a non-root user."; exit "$E_INSTALL"; }

[ -z "$PKGMK_INSTALL_COMMAND" ] || { $PKGMK_SU $PKGMK_INSTALL_COMMAND "$pkg_dir$package" \
	&& info "$(basename $PKGMK_INSTALL_COMMAND) $package succeeded."; } || \
	{ error "$(basename $PKGMK_INSTALL_COMMAND) $package failed."; exit "$E_INSTALL"; }

# Done!
}

#######################  error codes  ##############################
E_PKGFILE=2    # invalid Pkgfile
E_DIRPERM=3    # (source/build) directory missing or missing read/write permission
E_DOWNLOAD=4   # error during download
E_UNPACK=5     # error during unpacking of source file(s)
E_MANIFEST=6   # footprint or md5sum failure
E_BUILD=7      # error while running 'build()'
E_INSTALL=8    # error while installing the package via 'pkgadd'
E_SIGNATURE=9  # error verifying the signature

########################  settings  ################################
readonly PKGMK_VERSION="#VERSION#"
readonly PKGMK_COMMAND="$0"
readonly PKGMK_ROOT="$PWD"
readonly PRTWASH_COMMAND="/usr/bin/prtwash"
PKGMK_DOWNLOAD_PROG="/usr/bin/wget"
PKGMK_GIT_COMMAND="/usr/bin/git"
PKGMK_CONF="/etc/pkgmk.conf"

PKGMK_SOURCE_DIR="$PWD";          PKGMK_WORK_DIR="$PWD/work"
PKGMK_PACKAGE_DIR="$PWD";         PKGMK_COMPRESSION_MODE="gz"

PKGMK_INSTALL_COMMAND="";         PKGMK_FORCE="no"
PKGMK_CLEAN="no";                 PKGMK_KEEP_WORK="no"
PKGMK_RECURSIVE="no";             PKGMK_UPDATE_MD5="no"
PKGMK_DOWNLOAD_ONLY="no";         PKGMK_IGNORE_MD5="no"
PKGMK_EXTRACT_ONLY="no";          PKGMK_UPDATE_SIG="no"
PKGMK_MTIME_ONLY="no";            PKGMK_IGNORE_SIG="no"
PKGMK_UPDATE_FOOTPRINT="no";      PKGMK_REFRESH_SIG="no"
PKGMK_IGNORE_FOOTPRINT="no";      PKGMK_CHECK_SIG="no"
PKGMK_IGNORE_NEW="no";            PKGMK_PRIVATEKEY=""

######################## subroutines ################################
parse_options() {
	while [ "$1" ]; do
		case $1 in
			-r|--recursive) PKGMK_RECURSIVE="yes" ;;
			-c|--clean) [ -x "$PRTWASH_COMMAND" ] && PKGMK_CLEAN="yes" || \
				{ error "option '-c' not supported ($PRTWASH_COMMAND not installed)";
						exit 1; } ;;
			-uf|--update-footprint) PKGMK_UPDATE_FOOTPRINT="yes" ;;
			-um|--update-md5sum) PKGMK_UPDATE_MD5="yes" ;;
			-us|--update-signature) PKGMK_UPDATE_SIG="yes" ;;
			-rs|--refresh-signature) PKGMK_REFRESH_SIG="yes" ;;
			-cs|--check-signature) PKGMK_CHECK_SIG="yes" ;;
			-d|-cm|--download|--check-md5sum) ;; # deprecated flags, but at least they won't trigger an error
			-do|--download-only) PKGMK_DOWNLOAD_ONLY="yes" ;;
			-eo|--extract-only) PKGMK_EXTRACT_ONLY="yes" ;;
			-utd|--up-to-date) PKGMK_MTIME_ONLY="yes" ;;
			-if|--ignore-footprint) PKGMK_IGNORE_FOOTPRINT="yes" ;;
			-in|--ignore-new) PKGMK_IGNORE_NEW="yes" ;;
			-im|--ignore-md5sum) PKGMK_IGNORE_MD5="yes" ;;
			-is|--ignore-signature) PKGMK_IGNORE_SIG="yes" ;;
			-ns|--no-strip) echo ".*" >> "$PKGMK_ROOT/.nostrip" ;;
			-f|--force) PKGMK_FORCE="yes" ;;
			-kw|--keep-work) PKGMK_KEEP_WORK="yes" ;;
			-i|--install) PKGMK_INSTALL_COMMAND="/usr/bin/pkgadd" ;;
			-u|--upgrade) PKGMK_INSTALL_COMMAND="/usr/bin/pkgadd -u" ;;
			-pk|--public-key)
				[ "$2" ] && PKGMK_PUBLICKEY="$2" || { echo "$(basename "$PKGMK_COMMAND"): option $1 requires an argument";
					exit 1; }
				shift ;;
			-sk|--secret-key)
				[ "$2" ] && PKGMK_PRIVATEKEY="$2" || { echo "$(basename "$PKGMK_COMMAND"): option $1 requires an argument";
					exit 1; }
				shift ;;
			-cf|--config-file)
				[ "$2" ] && PKGMK_CONF="$2" || { echo "$(basename "$PKGMK_COMMAND"): option $1 requires an argument";
					exit 1; }
				shift ;;
			-v|--version)
				echo "$(basename "$PKGMK_COMMAND") (pkgutils) $PKGMK_VERSION"; exit 0 ;;
			-h|--help)
				print_help; exit 0 ;;
			*)
				echo "$(basename "$PKGMK_COMMAND"): invalid option $1"; exit 1 ;;
		esac
		shift
	done
}

print_help() {
	echo "usage: $(basename "$PKGMK_COMMAND") [options]"
	echo "options: "
	echo "  -r,   --recursive           search for Pkgfiles under $PWD, and run $(basename "$PKGMK_COMMAND")"
	echo "                              with the other given options inside each directory found"
	echo "  -c,   --clean               remove package and downloaded files"
	echo "  -i,   --install             build and install package"
	echo "  -u,   --upgrade             build and install package (as upgrade)"
	echo "  -do,  --download-only       stop after downloading all the necessary source file(s)"
	echo "  -eo,  --extract-only        stop after downloading and extracting source file(s)"
	echo "  -utd, --up-to-date          report whether the built package is up to date, then exit"
	echo "  -um,  --update-md5sum       update md5sum of the downloaded sources, do not build"
	echo "  -us,  --update-signature    update signature of Pkgfile and sources, do not build"
	echo "  -cs,  --check-signature     verify the signatures, do not build"
	echo "  -if,  --ignore-footprint    build package without checking footprint"
	echo "  -in,  --ignore-new          build package, ignoring new files in a footprint mismatch"
	echo "  -im,  --ignore-md5sum       build package without checking md5sum"
	echo "  -is,  --ignore-signature    build package without checking signature"
	echo "  -uf,  --update-footprint    update footprint using result from last build"
	echo "  -rs,  --refresh-signature   create new signature and keep existing sha256 checksums"
	echo "  -sk,  --secret-key <file>   use <file> to sign the port"
	echo "  -pk,  --public-key <file>   check the port signature using public-key <file>"
	echo "  -ns,  --no-strip            do not strip executable binaries or libraries"
	echo "  -f,   --force               build package even if it appears to be up to date"
	echo "  -kw,  --keep-work           keep temporary working directory"
	echo "  -cf,  --config-file <file>  use alternative configuration file"
	echo "  -v,   --version             print version and exit"
	echo "  -h,   --help                print help and exit"
}

validate_pkgfile() { # called from within PKGMK_ROOT
	local errcode kv
	[ -f Pkgfile ] || { error "no Pkgfile found. $PKGMK_ROOT is not a valid ports directory."; return "$E_PKGFILE"; }
	check_signature "pre-Pkgfile" | parse_signify_output
	errcode=$?
	[ "$errcode" = 0 ] || info "Use '-is' if you have independent confirmation of the port's integrity."
	[ "$errcode" = 0 ] || [ "$PKGMK_IGNORE_SIG" = "yes" ] || return $E_SIGNATURE

	# the environment should not be affected by sourcing the Pkgfile both here
	# and in the later build, but to be safe we use a nested subshell
	( . Pkgfile; kv=0; [ -n "$name" ] || kv+=1; [ -n "$version" ] || kv+=2; \
	[ -n "$release" ] || kv+=4; [ "$(type -t build)" = "function" ] || kv+=8; \
	echo $kv ) | check_reqvars
}

check_reqvars () {
	local checksum nullvars;
	read -r checksum
	[ $((checksum & 1)) = 1 ] && nullvars=" 'name'"
	[ $((checksum & 2)) = 2 ] && nullvars+=" 'version'"
	[ $((checksum & 4)) = 4 ] && nullvars+=" 'release'"
	[ $((checksum & 8)) = 8 ] && nullvars+=" 'build()'"
	[ "$checksum" = 0 ] || { error "Pkgfile does not specify these required variables:"; \
	info "$nullvars"; return "$E_PKGFILE"; }
}

check_pkg_mtime() { # can be called even if some sources are missing
	local li=0; local utd=0; local msg="$package is not up to date."

	if [ -f "$pkg_dir$package" ]; then
		utd=1
		while [ $li -lt ${#_local_[@]} ] && [ "$utd" = 1 ]; do
			 [ ! -e "${_local_[$li]}" ] || \
			[ "$pkgdir$package" -nt $(realpath "${_local_[$li]}") ] || utd=0
			 li=$(( li+1 ))
		done
		[ ! -e Pkgfile ] || [ "$pkg_dir$package" -nt Pkgfile ] || utd=0
	fi

	[ $utd = 0 ] || msg="$package is up to date."
	info "$msg"; return $utd
}

fetch_url() {
	local u="$1"; local h="$2"; local finished=0; local gitsrc tag CLONE_ARGS
	local REPO SAVE_AS OCONTINUE OOUT; local m=0
	[ -x "$PKGMK_GIT_COMMAND" ] || PKGMK_GIT_COMMAND="/bin/false"

	# Is this a url that requires git?
	if [[ $u =~ ^(https|http|ssh|git)://.+/(.+)\.git($|#.*) ]]; then
		# Did the port maintainer specify a branch other than 'master'?
		tag=${BASH_REMATCH[3]#\#}; gitsrc="${u%.git*}.git"
		[ -z "$tag" ] || CLONE_ARGS="--branch $tag"

		# If git is not installed, this source cannot be obtained at present
		[ "$PKGMK_GIT_COMMAND" != "/bin/false" ] || return 1;

		# Is it our first attempt to download this repository?
		if [ ! -d "$src_dir/$h.partial" ]; then
			"$PKGMK_GIT_COMMAND" clone "$gitsrc" $CLONE_ARGS "$h.partial"
			finished=$?
			[ "$src_dir" = "" ] || { mv "$h.partial" "$src_dir";
					ln -s "$src_dir/$h.partial" "$h"; }
		else
			ln -s "$src_dir/$h.partial" "$h"; cd "$h"
			"$PKGMK_GIT_COMMAND" pull $gitsrc $tag
			finished=$?; cd ..
		fi
		return $finished;
	fi

	# haven't returned yet, so a different transport protocol must be in effect
	[[ "$PKGMK_DOWNLOAD_PROG" =~ wget$ ]] && { OCONTINUE="-c"; OOUT="--compression=none --passive-ftp --no-directories --tries=3 --waitretry=3 $PKGMK_WGET_OPTIONS -O"; }
	[[ "$PKGMK_DOWNLOAD_PROG" =~ curl$ ]] && { OCONTINUE="-C -"; OOUT="-L --fail --ftp-pasv --retry 3 --retry-delay 3 $PKGMK_CURL_OPTIONS -o"; }
	[[ "$PKGMK_DOWNLOAD_PROG" =~ (wget|curl)$ ]] || SAVE_AS=/bin/false
	
	# start with the mirrors defined in pkgmk.conf, then go to the url found in the Pkgfile
	while [ $m -le ${#PKGMK_SOURCE_MIRRORS[@]} ] && [ $finished = 0 ] && [[ ! $SAVE_AS =~ false$ ]]; do
		[ "${PKGMK_SOURCE_MIRRORS[m]}" = "" ] && um=$u || \
			{ REPO=${PKGMK_SOURCE_MIRRORS[m]%/}; um=$REPO/${u##*/}; }
		m=$(( m+1 ))

	# interrupted downloads from a previous run should be put where wget or curl will find them
		[ -f "$src_dir/$h.partial" ] && { ln -s "$src_dir/$h.partial" . ;
			SAVE_AS="$PKGMK_DOWNLOAD_PROG $um $OCONTINUE $OOUT"; } \
			|| SAVE_AS="$PKGMK_DOWNLOAD_PROG $um $OOUT"

		if $SAVE_AS "$h.partial"; then
			finished=1
			[ "$src_dir" = "" ] || [ ! -w "$src_dir"/ ] || \
			{ mv "$h.partial" "$src_dir/$h"; ln -sf "$src_dir/$h" . ; }
		else # an interrupted download should not have its efforts destroyed by cleanup_work()
			[ ! -s "$h.partial" ] || [ "$src_dir" = "" ] || [ ! -w "$src_dir"/ ] \
				|| mv "$h.partial" "$src_dir"
		fi
	done
}

cat_manifest() {
	case "$1" in
		footprint)
			pkginfo --footprint "$pkg_dir$package" \
			| sed "s|\tlib/modules/$(uname -r)/|\tlib/modules/<kernel-version>/|g" \
			| sort -k 3
		;;
		md5sum)
			md5sum "${_local_[@]}" 2>&1 \
			| grep -v "is a directory" | sed 's,  .*/,  ,' | sort -k 2
		;;
	esac
}

check_manifest() {
	local FILTER CN CM
	local TRUTH="$PKGMK_ROOT/.$1"; local diffs=0; local severity=error;
	[ -f "$pkg_dir$package" ] || [ "$1" = "md5sum" ] || \
			{ error "$package not found. Cannot check $1."; exit "$E_MANIFEST"; }
	[ "$1" = "md5sum" ] && FILTER="-k 3" || FILTER=""

	if [ -f "$TRUTH" ]; then
		diff -w -t -U 0 <(sort $FILTER "$TRUTH") <(cat_manifest $1 | sort $FILTER) | \
		sed '/^@@/d; /^+++/d; /^---/d; s/^+/NEW    /g; s/^-/MISSING /g' > ".$1.diff"
		if [ -s ".$1.diff" ]; then
			CN=$(grep -c ^NEW ".$1.diff"); CM=$(grep -c ^MISSING ".$1.diff")
			if [ "$1" = "footprint" ]; then
				[ "$PKGMK_IGNORE_MISSING" = "yes" ] || diffs=$CM
				[ "$PKGMK_IGNORE_NEW" = "yes" ] || diffs=$(( diffs+CN ))
				[ $diffs = 0 ] && severity=warning
			fi
			$severity "$1 mismatch found:"; cat ".$1.diff" >&2
		fi
		rm ".$1.diff"
	else
		warning ".$1 not found, creating new."; cat_manifest $1 > "$TRUTH"
	fi
	[ $diffs = 0 ] || exit $E_MANIFEST
}

parse_signify_output() { # chomps the output of check_signature()
	local signout signerr
	[ "$PKGMK_IGNORE_SIG" = "yes" ] && return 0
	while read -r signout; do
	case "$signout" in 
		*"Pkgfile verification failed")
			signerr=-1; error "Signature missing! Unable to authenticate the Pkgfile."
			;;
		*"verification failed")
			signerr=1; error "Signature file corrupted or unreadable."
			;;
		*"FAIL")
			signerr=2; error "Signature mismatch found:"
			echo "$signout" | awk -F: '/FAIL/ {printf "MISMATCH   %s\n", $1}'
			;;
		*"OK") signerr=0 ;;
	esac
	done
	return $signerr
}

check_signature() { # called from $PKGMK_ROOT in the case "when"="pre-Pkgfile",
	# otherwise called from within $work. Pass control to
	# check_manifest() if the signature is missing and "ignore-md5"
	# has not been requested.
	local reqfiles=(Pkgfile); local s=0; local when="$1";

	if [ -f "$PKGMK_ROOT/.signature" ]; then
		[ "$when" = "pre-Pkgfile" ] || reqfiles=(.footprint)
		while [ "$when" = "pre-build" ] && [ "$s" -lt ${#_local_[@]} ]; do 
			[[ "${source[$s]}" =~ ^(http|https|ssh|git)://(.+)\.git ]] || \
					reqfiles+=("${_local_[$s]}")
			s=$(( s+1 ))
		done
		for FILE in "${reqfiles[@]}"; do
			[ -e "$FILE" ] || ln -sf "$PKGMK_ROOT/$FILE" .
		done
		if [ -f "$PKGMK_PUBLICKEY" ]; then
			/usr/bin/signify -C -p "$PKGMK_PUBLICKEY" -x "$PKGMK_ROOT/.signature" \
				"${reqfiles[@]}" 2>&1
		else
			/usr/bin/signify -C -x "$PKGMK_ROOT/.signature" \
				"${reqfiles[@]}" 2>&1
		fi
	else
		[ "$when" != "pre-Pkgfile" ] || echo "Pkgfile verification failed"
		[ "$when" = "pre-Pkgfile" ] || [ "$PKGMK_IGNORE_MD5" = "yes" ] || \
		{ info "Signature not found, falling back to old md5sum checking.";
			check_manifest md5sum ; }
	fi
}

cat_signature() {
	local ordered si key pub
	[ -e "$PKGMK_ROOT/.footprint" ] || warning "Footprint not found, signature will be incomplete."
	for key in ~/.ssh/*.sec /etc/ports/*.sec; do
		[ -e "$key" ] || continue  # workaround for brain-dead shell globbing
		pub=/etc/ports/$(basename "${key/%.sec/.pub}")
		if [ -e "$pub" ]; then
			pub=$(readlink -f "$pub")
			for f in "$PKGMK_ROOT/Pkgfile" "$PKGMK_ROOT/.footprint"; do
					[ -e $f ] && ordered+=( "$f" )
			done
			for ((si=0; si < ${#source[@]}; si++)); do
	[[ ${source[$si]} =~ ^(http|https|ssh|git)://.+/.+\.git($|#.*) ]] || ordered+=("${_local_[$si]}"); done
			sha256sum --tag "${ordered[@]}" \
					| sed 's|^SHA256 (.*/\(.*\))\(.* = .*\)|SHA256 (\1)\2|' \
					| /usr/bin/signify -S -e -x - -q -s "$key" -m -                  \
					| sed "s|${key/%.sec/.pub}|$pub|"
			break
		fi
	done
}

refresh_signature() {
    if [ -e ".signature" ] && [ ! -w ".signature" ]; then
		echo "error: .signature not writable."; return $E_DIRPERM
	fi

	local REPO
	[ -n "$PKGMK_PRIVATEKEY" ] || REPO=$(dirname "$PWD" | sed 's|^.*/||; s|\.git$||;')
	[ -n "$REPO" ] && PKGMK_PRIVATEKEY="/etc/ports/${REPO}.sec"

	if ! tail -n +3 ".signature" | /usr/bin/signify -S -e -x - -q \
			-s "$PKGMK_PRIVATEKEY" -m - > .signature.tmp ; then
		rm .signature.tmp
		error "Refreshing signature failed"
		return $E_SIGNATURE
	else
		mv .signature.tmp .signature
	fi
}

interrupted() {
	error "Interrupted."; cleanup_work; exit 1
}

cleanup_work() {
	[ "$PKGMK_KEEP_WORK" = "yes" ] || { cd "$PKGMK_ROOT"; rm -rf "$work"; }
}

recursive() {
	local ARGS DIR FILE
	[ "$PKGMK_CLEAN" = "no" ] || { find "$PKGMK_ROOT" -name Pkgfile -printf "%h\n" \
			| xargs "$PRTWASH_COMMAND" -s -p -b -q; exit $? ; }

	ARGS=$(echo "$@" | sed "s/--recursive//g; s/\s*-r\s*/ /g")
	for FILE in $(find "$PKGMK_ROOT" -name Pkgfile | sort); do
		DIR=$(dirname "$FILE")
		[ -d $DIR ] && { info "Entering directory '$DIR'.";
			(cd "$DIR" && "$PKGMK_COMMAND" $ARGS);
			info "Leaving directory '$DIR'."; }
	done
	exit
}

info() {
	echo "=======> $1"
}

warning() {
	info "WARNING: $1" >&2
}

error() {
	info "ERROR: $1" >&2
}

######################## end of subroutines ###########################
## Now ensure that they cannot be overwritten when sourcing Pkgfile  ##
readonly -f main info warning error print_help parse_options validate_pkgfile \
	check_reqvars check_pkg_mtime fetch_url cat_manifest check_manifest \
	cat_signature check_signature parse_signify_output refresh_signature \
	cleanup_work recursive
trap "interrupted" SIGHUP SIGINT SIGQUIT SIGTERM

main "$@"