#!/bin/bash
#
# fakepkg: Reassemble installed packages from its delivered files
#

# Strictly disallow uninitialized variables and exit on command failures (even in pipes)
set -Eeuo pipefail

unset LC_ALL
export LANG=C

declare -r myname='fakepkg'
declare -r myver='1.42.2'

# Directory in /tmp on which basis mktemp will create an unique tmp for each fakebuild
# Make this variable something rather unique since on cleanup anything starting with this will be erased
declare -r tmp_root='fakepkg'

# Standard path where pacman db resides
declare -r db_path='/var/lib/pacman'

# Error codes
declare -r ERROR_USAGE=1
declare -r ERROR_OPERATION=2

# Actual assembly function
# $1: pkgname , $2: destination dir , $3 verbosity
function fakebuild() {
	PKG_VER=$(pacman --dbpath "${ROOT_DIR:-/}${db_path}" -Qi "${1}" 2>/dev/null | awk '/^Version/{print $3}')
	# Setting global vars
	PKG_NAME="${1}-${PKG_VER}"
	PKG_DB_ENTRY="${ROOT_DIR:-/}${db_path}/local/${PKG_NAME}"
	PKGEXT="${PKGEXT:-.pkg.tar.zst}" # implicitly sets the compression format
	local _MTREE="${PKG_DB_ENTRY#/}/mtree"
	local _MIMETYPE

	# Skip if the package already exists in the destination dir
	if [[ -f "${2}/${PKG_NAME}${PKGEXT}" ]]; then
		>&2 echo -e "\e[39;1m${PKG_NAME}${PKGEXT}\e[0m already exists in ${2}! Skipping..."
		return ${ERROR_USAGE}
	fi

	# Create and enter a temporary dir
	TMPDIR=$(mktemp -q -p /tmp -t -d "${tmp_root}".XXX)
	cd "${TMPDIR}" || return ${ERROR_OPERATION}

	# Fetching the .PKGINFO, .Changelog, .MTREE and .INSTALL file
	if [[ -f "${PKG_DB_ENTRY}/desc" ]]; then
		cp -a "${PKG_DB_ENTRY}/desc" pre_PKGINFO
	else
		>&2 echo -e "Could not find a database entry for \e[39;1m${PKG_NAME}\e[0m"
		return ${ERROR_OPERATION}
	fi
	[[ -f "${PKG_DB_ENTRY}/install" ]] && cp -a "${PKG_DB_ENTRY}/install" .INSTALL

	if [[ -f "${_MTREE}" ]]; then
		_MIMETYPE=$(file --mime-type "${PKG_DB_ENTRY}/mtree")
		if [[ "${_MIMETYPE}" == *"application/gzip"* ]]; then
			gzip -dc "${_MTREE}" > .MTREE
		elif [[ "${_MIMETYPE}" == *"text/plain"* ]]; then
			cp "${_MTREE}" .MTREE
		fi
	fi
	[[ -f "${PKG_DB_ENTRY}/changelog" ]] && cp -a "${PKG_DB_ENTRY}/changelog" .Changelog

	# Hacking together the .PKGINFO
	sed -n -e '/%BACKUP%/,$p' "${PKG_DB_ENTRY}/files" | awk '{print $1}' >> pre_PKGINFO
	sed -i '/^$/d' pre_PKGINFO
	while read -r line; do
		if [[ $line =~ %[A-Z]+% ]]; then
			PKG_VAR=$(echo "${line,,}" | tr -d '%')
			case "$PKG_VAR" in
				"name")     PKG_VAR=pkgname;;
				"version")  PKG_VAR=pkgver;;
				"desc")     PKG_VAR=pkgdesc;;
				"groups")   PKG_VAR=group;;
				"depends")  PKG_VAR=depend;;
				"optdepends")   PKG_VAR=optdepend;;
				"conflicts")    PKG_VAR=conflict;;
			esac
		else
			echo -e "$PKG_VAR = $line" >> .PKGINFO
		fi
	done < pre_PKGINFO
	sed -i '/installdate/d' .PKGINFO
	sed -i '/validation/d' .PKGINFO

	# Assembling a list of all files belonging to the package
	# The tar command is unable to handle very many arguments therefore create a file
	pacman --dbpath "${ROOT_DIR:-/}${db_path}" -Qlq "$1" | sed 's%^/%%' > ./package_file_list
	aux_files=(".PKGINFO")
	[[ -f ".INSTALL" ]] && aux_files+=(".INSTALL")
	[[ -f ".Changelog" ]] && aux_files+=(".Changelog")
	[[ -f ".MTREE" ]] && aux_files+=(".MTREE")

	# Taring things together
	if [[ $3 -ge "1" ]]; then
		#XZ_OPT="-T 0" \
		tar --ignore-failed-read --owner=0 --group=0 --no-recursion -c -a \
			--force-local -f "${PKG_NAME}${PKGEXT}" \
			-C "${ROOT_DIR:-/}" -T ./package_file_list -C "$TMPDIR" \
			"${aux_files[@]}"
	else
		#XZ_OPT="-T 0" \
		if tar --ignore-failed-read --owner=0 --group=0 --no-recursion -c -a \
			--force-local -f "${PKG_NAME}${PKGEXT}" \
			-C "${ROOT_DIR:-/}" -T ./package_file_list -C "$TMPDIR" \
			"${aux_files[@]}" 2>&1 >/dev/null \
			| grep -i "permission denied" >/dev/null -; then

			>&2 echo -e "The permission to open one or more directories was denied for \e[39;1m${PKG_NAME}\e[0m. The package may be incomplete!"
		fi
	fi

	# Cleanup
	if [[ -f ${PKG_NAME}${PKGEXT} ]]; then
		mv "${PKG_NAME}${PKGEXT}" "$2"
		>&2 echo -e "Reassembled \e[39;1m${PKG_NAME}\e[0m successfully!"
	else
		>&2 echo -e "Internal \e[39;1merror\e[0m occurred while processing \e[39;1m${PKG_NAME}\e[0m!"
	fi
	rm -rf "${TMPDIR}"
}

# Run fakebuild in parallel with a limit of $MAX_JOBS jobs
# By default only run one job
MAX_JOBS=1
function parallelize() {
	while [[ $# -gt 0 ]] ; do
		mapfile -t job_count < <(jobs -p)
		if [[ ${#job_count[@]} -lt $MAX_JOBS ]] ; then
			fakebuild "$1" "$DEST_DIR" "$VERBOSITY" &
			shift
		fi
	done
	wait
}

# Print usage information, no arguments required
function usage() {
	cat <<-EOF
	$myname $myver
	$myname reassembles installed packages from its delivered files. It comes in
	handy if there is no internet connection available and you have no access to
	an up-to-date package cache.

	Usage: $myname [-v] [-j <jobs>] [-o <dir>] <package(s)>
	    -h, --help              Display this help message and exit
	    -v, --verbose           Increase verbosity
	    -j, --jobs <jobs>       Build in parallel - you may want to set XZ_OPT
	    -o, --out  <dir>        Write output to <dir>
	    -r, --root <dir>        Use an alternate root directory containing pacman db

	$myname honors the environment variable \`PKGEXT\`. Use it to set the
	compression scheme.

	Examples:   # $myname slurm-llnl
	            # $myname gzip munge binutils -o ~/Downloads
	            # $myname -o /tmp -j 5 gzip munge binutils
	            # PKGEXT=".pkg.tar.xz" $myname -o /tmp -r /old/root legacy-package
	            # $myname \$(pacman -Qsq)

	Copyright (C) Gordian Edenhofer <gordian.edenhofer@gmail.com>
	The script requires bash>=4.2, pacman, tar and gzip.
	EOF
}

function version() {
	echo "$myname $myver"
	echo "Copyright (C) Gordian Edenhofer <gordian.edenhofer@gmail.com>"
	echo "Copyright (C) 2008-2016 Pacman Development Team <pacman-dev@archlinux.org>"
}

# Clean up temporary dirs recursively
function clean_up {
	rm -r /tmp/"${tmp_root}".*
	echo
	exit
}

# Trap termination signals
trap clean_up SIGHUP SIGINT SIGTERM

# Show usage and quit if invoked with no parameters
[[ $# -eq 0 ]] && usage && exit ${ERROR_USAGE}
for ARG in "$@"; do
	[[ $ARG == "-h" || $ARG == "--help" ]] && usage && exit 0
done

# Assembling PKG_LIST and DEST_DIR
if ! PARAMS=$(getopt -o o:j:r:vV --long out:,jobs:,root:,verbose,version -n "$myname" -- "$@"); then
	>&2 echo "Try '$myname --help' for more information."
	exit ${ERROR_USAGE}
fi
eval set -- "$PARAMS"
DEST_DIR=$PWD	# Default value
VERBOSITY=0		# Default value
ROOT_DIR=""  # Default value

while true ; do
	case "$1" in
		-o|--out)
		if ! DEST_DIR="$(readlink -e "$2")"; then
			>&2 echo -e "The directory \e[39;1m$2\e[0m does not exist!"
			exit ${ERROR_USAGE}
		fi
		shift
		;;

		-j|--jobs)
		if [[ $2 =~ ^-?[0-9]+$ ]]; then
			MAX_JOBS=$2
		else
			>&2 echo -e "\e[39;1m$2\e[0m is not a valid integer!"
			exit ${ERROR_USAGE}
		fi
		shift
		;;

		-r|--root)
		ROOT_DIR=$2
		[[ ! -d "${ROOT_DIR}" ]] && >&2 echo -e "The specified alternate root directory does not exist!" && exit ${ERROR_USAGE}
		ROOT_DIR=$(readlink -e "${ROOT_DIR}")
		shift
		;;

		-v|--verbose)
		VERBOSITY=$((VERBOSITY+1))
		;;

		-V|--version)
		version
		exit 0
		;;

		--)
		shift
		break
		;;

		*)
		usage
		exit ${ERROR_USAGE}
		;;
	esac
	shift
done
PKG_LIST=("$@")

# Checking the PKG_LIST
PKG_NOTFOUND=()
[[ ${#PKG_LIST[@]} == 0 ]] && usage && exit ${ERROR_USAGE}
for package in "${PKG_LIST[@]}"; do
	ver=$(pacman --dbpath "${ROOT_DIR:-/}${db_path}" -Qi "${package}" 2>/dev/null | awk '/^Version/{print $3}')
	if [[ ! -d "${ROOT_DIR:-/}${db_path}/local/${package}-${ver}" ]]; then
		PKG_NOTFOUND+=("\"${package}\" ")
	fi
done
if [[ ${#PKG_NOTFOUND[@]} -ne 0 ]]; then
	>&2 echo -e "The following package(s) could not be found on your system: \e[39;1m${PKG_NOTFOUND[*]}\e[0m"
	>&2 echo -e "Keep in mind that the package(s) have to be installed!"
	exit ${ERROR_USAGE}
fi

# Assembling the packages
parallelize "${PKG_LIST[@]}"

exit 0
