#!/bin/sh -efu
#
# Copyright (C) 2006, 2007 Securedog
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# $Id: pkg_replace.sh,v 1.16 2007/01/25 12:55:46 securedog Exp $

PKG_REPLACE_VERSION=20070125
PKG_REPLACE_CONFIG=FreeBSD

show_version() {
	echo "${PKG_REPLACE_VERSION} [${PKG_REPLACE_CONFIG}]"
	exit 0
}

usage() {
	echo "usage: ${0##*/} [-habBcCfFinNpPqrRuvVwW] [-l file] [-L prefix]"
	echo "        [-m make_args] [-x pkgname] [[pkgname[=package]] [package] ...]"
	exit 1
}

empty() {
	case ${@:+1} in
	'')	return 0 ;;
	*)	return 1 ;;
	esac
}

is_yes() {
	case ${1-} in
	[Yy][Ee][Ss]|[Yy]|[Tt][Rr][Uu][Ee])
		return 0 ;;
	*)	return 1 ;;
	esac
}

warn() {
	echo "** ${@-}" >&2
}

info() {
	echo "--->  ${@-}"
}

prompt_yesno() {
	local prompt default input

	prompt=${1-"OK?"}
	default=${2-"yes"}

	echo -n "${prompt} [${default}] " >&2
	read input

	is_yes ${input:-"${default}"} || return 1
}

init_options() {
	opt_all=NO
	opt_backup=YES
	opt_keep_backup=NO
	opt_beforeclean=NO
	opt_afterclean=YES
	opt_force=NO
	opt_fetch=NO
	opt_interactive=NO
	opt_result=
	opt_log_prefix=
	opt_make_args=
	opt_noexecute=NO
	opt_new=NO
	opt_package=NO
	opt_use_packages=NO
	opt_noconf=NO
	opt_depends=NO
	opt_required_by=NO
	opt_preserve_libs=YES
	opt_verbose=NO
	opt_version=NO
	opt_build=NO # XXX: is not assigned
	opt_exclude=
	opt_replace=
	MAKE_ARGS=
	BEFOREBUILD=
	BEFOREDEINSTALL=
	AFTERINSTALL=
	IGNORE=
	USE_PKGS=
}

init_variables() {
	: ${PORTSDIR="/usr/ports"}
	: ${PKGREPOSITORY="${PORTSDIR}/packages/All"}
	: ${PACKAGEROOT="ftp://ftp.FreeBSD.org"}
	: ${PKG_SUFX=".tbz"}
	: ${PKG_BACKUP_DIR=${PKGREPOSITORY}}
	: ${PKG_DBDIR="/var/db/pkg"}
	: ${PKG_TMPDIR=${TMPDIR:-"/var/tmp"}}
	: ${PKGCOMPATDIR="/usr/local/lib/compat/pkg"}

	tmpdir=
	init_pkgtools

	export PORTSDIR PKG_DBDIR PKG_TMPDIR PKG_SUFX PKGCOMPATDIR
}

init_pkgtools() {
	PKG_ADD="pkg_add"
	PKG_CREATE="pkg_create"
	PKG_DELETE="pkg_delete"
	PKG_INFO="pkg_info"
	MAKE="make"
}

parse_options() {
	local OPT OPTARG OPTIND

	case ${1-} in
	--version)	show_version ;;
	esac

	while getopts habBcCfFil:L:m:nNpPqrRuvVwWx: OPT; do
		case ${OPT} in
		a)	opt_all=YES ;;
		b)	opt_keep_backup=YES ;;
		B)	opt_backup=NO ;;
		c)	opt_beforeclean=YES ;;
		C)	opt_afterclean=YES ;;
		h)	usage ;;
		f)	opt_force=YES ;;
		F)	opt_fetch=YES ;;
		i)	opt_interactive=YES ;;
		l)	expand_path 'opt_result' "${OPTARG}" ;;
		L)	expand_path 'opt_log_prefix' "${OPTARG}" ;;
		m)	opt_make_args="${opt_make_args} ${OPTARG}" ;;
		n)	opt_noexecute=YES ;;
		N)	opt_new=YES ;;
		p)	opt_package=YES ;;
		P)	opt_use_packages=YES ;;
		q)	opt_noconf=YES ;;
		r)	opt_required_by=YES ;;
		R)	opt_depends=YES ;;
		u)	opt_preserve_libs=NO ;;
		v)	opt_verbose=YES ;;
		V)	opt_version=YES ;;
		w)	opt_beforeclean=NO ;;
		W)	opt_afterclean=NO ;;
		x)	opt_exclude="${opt_exclude} ${OPTARG}" ;;
		\?)	usage ;;
		esac
	done

	OPTC=$((${OPTIND}-1))
}

parse_args() {
	local p file

	pkgs=
	for p in ${1+"$@"}; do
		file=
		case $p in
		\\*)
			p=${p#\\} ;;
		*=*)
			expand_path 'file' "${p##*=}"
			file_exist "${file}" || continue
			p=${p%=*} ;;
		*.t[bg]z)
			expand_path 'file' "$p"
			get_pkgname_for_binary 'p' "${file}" || continue
			p=${p%-*} ;;
		esac

		pkgs="${pkgs} $p"
		opt_replace="${opt_replace}${file:+ $p=${file}}"
	done
}

parse_config() {
	local X _line _var _val _array

	if [ ! -r "$1" ]; then
		return 0
	fi

	_line=0
	_array=

	while read X; do
		_line=$((${_line}+1))

		case $X in
		''|\#*) continue ;;
		esac

		if empty ${_array}; then
			case $X in
			?*=*)
				_var=${X%%=*}
				_val=${X#*=} ;;
			*)	_var="syntax-error" ;;
			esac

			case ${_var} in
			*[!0-9A-Za-z_]*|[0-9]*)
				warn "Syntax error at line ${_line}: $X"
				return 1 ;;
			esac

			case ${_val} in
			*[\({])
				eval ${_var}=; _array=${_var} ;;
			*)	eval ${_var}=${_val} ;;
			esac
		else
			case $X in
			*[\'\"]?*[\'\"]*)
				_var=${X#*[\'\"]}
				_var=${_var%%[\'\"]*}

				case $X in
				*=\>*[\'\"]*[\'\"]*)
					_val=${X#*=>*[\'\"]}
					_val=${_val%[\'\"]*}
					eval ${_array}=\"\${${_array}:+\$${_array}'
'}\${_var}=\${_val}\" ;;
				*)
					eval ${_array}=\"\$${_array} \${_var}\" ;;
				esac ;;
			*[\)}]|*[\)}]\;)
				_array= ;;
			*)
				warn "Syntax error at line ${_line}: $X"
				return 1 ;;
			esac
		fi
	done < "$1"
}

get_config() {
	local IFS i hash

	IFS='
'
	eval hash=\$$2
	eval $1=

	for i in ${hash}; do
		if pkg_match "${i%%=*}"; then
			eval $1=\"\${$1:+\$$1\${3-' '}}\${i#*=}\"
		fi
	done
}

exec_script() {
	local key command

	key=$1; shift
	get_config 'command' "${key}" '; '

	if ! empty ${command}; then
		info "Executing the ${key} command: ${command}"
		( set +efu -- ${1+"$@"}; eval "${command}" ) || :
	fi
}

exec_rc_script() {
	local f file

	for file in $(${PKG_INFO} -qL "$1"); do
		case ${file} in
		*.sample) ;;
		*/etc/rc.d/*)
			if [ -x "${file}" ]; then
				info "Executing '${file} $2'"
				try "${file}" "$2" || :
			fi ;;
		esac
	done
}

cmd_start_rc() {
	exec_rc_script "$1" start
}

cmd_stop_rc() {
	exec_rc_script "$1" stop
}

cmd_restart_rc() {
	exec_rc_script "$1" restart
}

load_config() {
	if ! empty ${1-} && ! is_yes ${opt_noconf}; then
		is_yes ${opt_verbose} && info "Loading $1"

		if ! parse_config "$1"; then
			warn "Fatal error in $1."
			exit 1
		fi
	fi
}

load_bsd_make() {
	get_config 'PKG_MAKE_ARGS' 'MAKE_ARGS'
	PKG_MAKE_ARGS="${opt_make_args:+${opt_make_args} }${PKG_MAKE_ARGS}"
	PKG_MAKE="${MAKE}${PKG_MAKE_ARGS:+ ${PKG_MAKE_ARGS}}"
}

load_upgrade_vars() {
	PKG_MAKE="${PKG_MAKE} UPGRADE_PKG=$1 UPGRADE_PKG_VER=${1##*-}"
}

load_uname() {
	set -- ${@:-${PKG_REPLACE_ENV=$(uname -smr)}}

	OPSYS=$1
	OS_VERSION=$2
	OS_REVISION=${OS_VERSION%%-*}
	OS_MAJOR=${OS_REVISION%%.*}
	ARCH=$3
}

get_pkgname() {
	local X

	if {
		file_exist "$2" &&
		cd "$2" &&
		X=$(${PKG_MAKE} -VPKGNAME) &&
		! empty $X
	}; then
		eval $1=\$X
	else
		return 1
	fi
}

get_pkgname_for_binary() {
	local X _opt

	file_exist "$2" || return 1

	case $2 in
	*.tbz)	_opt=j ;;
	*.tgz)	_opt=z ;;
	*)	return 1 ;;
	esac

	X=`tar x${_opt}f "$2" -O --fast-read "+CONTENTS" |
	while read X; do
		case $X in
		@name\ *)	echo "${X#@name }"; break ;;
		[!@]*)		break ;;
		esac
	done`

	if empty $X; then
		warn "'$2' has no CONTENTS file - not a package?"
		return 1
	else
		eval $1=\$X
	fi
}

get_pkgpath() {
	local X

	eval $1=
	if [ -r "${PKG_DBDIR}/$2/+CONTENTS" ]; then
		while read X; do
			case $X in
			@comment\ ORIGIN:*)
				eval $1=\${X#@comment ORIGIN:}
				break ;;
			[!@]*)	break ;;
			esac
		done < "${PKG_DBDIR}/$2/+CONTENTS"
	fi
}

set_pkg_vars() {
	[ -d "${PKG_DBDIR}/$1" ] || return 1

	pkg_name=$1
	pkg_pkgdir=${PKG_DBDIR}/$1
	get_pkgpath 'pkg_origin' "$1"
	pkg_portdir=${PORTSDIR}/${pkg_origin}
	pkg_binary=
}

set_port_vars() {
	pkg_origin=${1#${PORTSDIR}/}
	pkg_portdir=$1
	pkg_binary=

	load_bsd_make ${pkg_name=XXX}
	get_pkgname 'pkg_name' "$1" || return 1
	pkg_pkgdir=${PKG_DBDIR}/${pkg_name}

	load_bsd_make
}

set_pkg_vars_for_binary() {
	get_pkgname_for_binary 'pkg_name' "$1" || return 1
	pkg_pkgdir=${PKG_DBDIR}/${pkg_name}
	pkg_origin=
	pkg_portdir=
	pkg_binary=$1
}

pkg_glob() {
	local p _var _arg _pattern _deps _req_by

	_var=$1; shift
	eval ${_var}=
	set +f

	for _arg in ${1+"$@"}; do
		_pattern=${_arg#${PKG_DBDIR}/}

		case ${_pattern} in
		*\**|*-pl[0-9]*|*-[0-9]*[0-9.][a-z]|*-[0-9]*[0-9]) ;;
		*)		_pattern="${_pattern}-[0-9]*[0-9a-z]" ;;
		esac

		for p in ${PKG_DBDIR}/${_pattern}/+CONTENTS; do
			if [ -e "$p" ]; then
				p=${p#${PKG_DBDIR}/}
				p=${p%/+CONTENTS}

				if is_yes ${opt_depends}; then
					pkg_depends '_deps' "$p"
					eval ${_var}=\"\$${_var} \${_deps}\"
				fi

				eval ${_var}=\"\$${_var} \$p\"

				if is_yes ${opt_required_by}; then
					pkg_required_by '_req_by' "$p"
					eval ${_var}=\"\$${_var} \${_req_by}\"
				fi
			else
				warn "No such installed package: ${_arg}"
			fi
		done
	done

	set -f
}

pkg_depends() {
	local X

	eval $1=
	if [ -r "${PKG_DBDIR}/$2/+CONTENTS" ]; then
		while read X; do
			case $X in
			@pkgdep\ *)
				eval $1=\"\$$1 \${X#@pkgdep }\" ;;
			[!@]*)	break ;;
			esac
		done < "${PKG_DBDIR}/$2/+CONTENTS"
	fi
}

pkg_required_by() {
	local X

	eval $1=
	if [ -r "${PKG_DBDIR}/$2/+REQUIRED_BY" ]; then
		while read X; do
			eval $1=\"\$$1 \$X\"
		done < "${PKG_DBDIR}/$2/+REQUIRED_BY"
	fi
}

pkg_sort() {
	local p _var _sorted _unsorted _arg _deps

	_var=$1; shift
	_sorted=
	_unsorted=

	case $# in
	1)	eval ${_var}=\"\$@\"; return 0 ;;
	esac

	for _arg in ${1+"$@"}; do
		case " ${_sorted}${_unsorted} " in
		*" ${_arg} "*)	continue ;;
		esac

		if [ -s "${PKG_DBDIR}/${_arg}/+REQUIRED_BY" ]; then
			pkg_depends '_deps' "${_arg}"

			for p in ${_deps}; do
				case " ${_sorted}${_unsorted} " in
				*" $p "*)	continue ;;
				esac
				case " $* " in
				*" $p "*)	_sorted="${_sorted} $p" ;;
				esac
			done

			_sorted="${_sorted} ${_arg}"
		else
			_unsorted="${_unsorted} ${_arg}"
		fi
	done

	eval ${_var}=\"\${_sorted}\${_unsorted}\"
}

pkg_match() {
	local i

	for i in ${1+"$@"}; do
		case "|${pkg_name}|${pkg_name%-*}|${pkg_origin}|" in
		*\|$i\|*) return 0 ;;
		esac
	done

	return 1
}

create_tmpdir() {
	if ! tmpdir=$(mktemp -d "${PKG_TMPDIR}/${0##*/}.XXXXXX"); then
		warn "Couldn't create the working directory."
		return 1
	fi
}

clean_tmpdir() {
	if ! try rmdir "${tmpdir}"; then
		warn "Couldn't remove the working direcotry: ${tmpdir}"
	fi
	tmpdir=
}

create_file() {
	if ! true > "$1"; then
		warn "Failed to create file: $1"
		return 1
	fi
}

update_file() {
	local file obj

	file=$1; shift
	obj=${file}.$$

	is_yes ${opt_verbose} && info "Updating ${file}: $@"

	sed "$@" "${file}" > "${obj}" && try mv -f "${obj}" "${file}"
}

file_exist() {
	if [ ! -e "$1" ]; then
		warn "No such file or directory: $1"
		return 1
	fi
}

create_dir() {
	if [ ! -d "$1" ]; then
		try mkdir -p "$1" || return 1
	fi
}

expand_path() {
	case $2 in
	[!/]*)	eval $1=\"${PWD:-$(pwd)}/\$2\" ;;
	*)	eval $1=\$2 ;;
	esac
}

try() {
	local exit

	"$@" || {
		exit=$?
		warn "Command failed (exit code ${exit}): $@"
		return ${exit}
	}
}

xtry() {
	local _cookie _log exit

	_log=$1; shift

	if empty ${_log}; then
		try "$@" || return $?
	else
		_cookie="${tmpdir}/.$1_failed"

		{ try "$@" || echo $? > "${_cookie}"; } | tee -a "${_log}" ||
			warn "tee(1) failed."

		if [ -e "${_cookie}" ]; then
			read exit < "${_cookie}"
			try rm -f "${_cookie}" || :
			return ${exit:-1}
		fi
	fi
}

build_package() {
	local build_args

	build_args=
	is_yes ${opt_package} && build_args="DEPENDS_TARGET=package"

	info "Building '$1'${PKG_MAKE_ARGS:+ with make flags: ${PKG_MAKE_ARGS}}"

	cd "$1" || return 1

	exec_script BEFOREBUILD ${pkg_name:+"${pkg_name}"}

	if is_yes ${opt_beforeclean}; then
		clean_package "$1" || return 1
	fi

	xtry "${2-}" ${PKG_MAKE} ${build_args} || return 1
}

install_package() {
	local install_args

	install_args=

	info "Installing '$1'"

	case $1 in
	*.t[bg]z)
		is_yes ${opt_force} && install_args="-f"
		is_yes ${opt_verbose} && install_args="${install_args} -v"

		xtry "${2-}" ${PKG_ADD} ${install_args} "$1" || return 1
		;;
	*)
		is_yes ${opt_force} && install_args="-DFORCE_PKG_REGISTER"

		if is_yes ${opt_package}; then
			install_args="${install_args} DEPENDS_TARGET=package package"
		else
			install_args="${install_args} install"
		fi

		cd "$1" || return 1

		xtry "${2-}" ${PKG_MAKE} ${install_args} || return 1
		get_pkgname 'pkg_name' "$1" || return 1

		if is_yes ${opt_afterclean}; then
			clean_package "$1" || :
		fi
		;;
	esac

	exec_script AFTERINSTALL ${pkg_name:+"${pkg_name}"}
}

deinstall_package() {
	local deinstall_args

	deinstall_args=
	is_yes ${opt_force} && deinstall_args="-f"
	is_yes ${opt_verbose} && deinstall_args="${deinstall_args} -v"

	info "Deinstalling '$1'"

	if [ ! -w "${PKG_DBDIR}" ]; then
		warn "You do not own ${PKG_DBDIR}."
		return 1
	fi

	exec_script BEFOREDEINSTALL "$1"

	try ${PKG_DELETE} ${deinstall_args} "$1" || return 1
}

clean_package() {
	local clean_args

	clean_args=

	info "Cleaning '$1'"

	cd "$1" || return 1
	try ${PKG_MAKE} ${clean_args} clean || return 1
}

fetch_distfiles() {
	info "Fetching '$1'"

	cd "$1" || return 1
	try ${PKG_MAKE} -DBATCH checksum || return 1
}

fetch_uri() {
	local path fetch_cmd fetch_args

	path=${2:-${1##*/}}
	fetch_cmd=
	fetch_args=

	info "Fetching '$1'"

	for fetch_cmd in fetch ftp wget curl; do
		type "${fetch_cmd}" >/dev/null || continue

		case ${fetch_cmd} in
		curl|fetch|ftp)
			fetch_args="-o ${path}" ;;
		wget)	fetch_args="-O ${path}" ;;
		esac

		if try ${fetch_cmd} ${fetch_args} "$1"; then
			return 0
		else
			return 1
		fi
	done

	warn "Couldn't find the file transfer program," \
	"please install either '${PORTSDIR}/ftp/wget' or '${PORTSDIR}/ftp/curl'."
	return 1
}

fetch_package() {
	local pkg uri uri_path subdir OPSYS OS_VERSION OS_REVISION OS_MAJOR ARCH

	pkg=$1${PKG_SUFX}

	if [ -e "${PKGREPOSITORY}/${pkg}" ]; then
		return 0
	elif ! create_dir "${PKGREPOSITORY}" || [ ! -w "${PKGREPOSITORY}" ]; then
		warn "You do not own ${PKGREPOSITORY}."
		return 1
	fi

	load_uname ${2+"$2"}

	if ! empty ${PACKAGESITE-}; then
		uri="${PACKAGESITE}${pkg}"
	else
		case ${OS_VERSION} in
		*-CURRENT)	subdir="${OS_MAJOR}-current" ;;
		*-RELEASE*)	subdir="${OS_REVISION}-release" ;;
		*)		subdir="${OS_MAJOR}-stable" ;;
		esac

		uri_path="/pub/FreeBSD/ports/${ARCH}/packages-${subdir}/All/"
		uri="${PACKAGEROOT}${uri_path}${pkg}"
	fi

	fetch_uri "${uri}" "${PKGREPOSITORY}/${pkg}" || return 1
}

find_package() {
	local X

	X="${PKGREPOSITORY}/$2${PKG_SUFX}"

	if [ -e "$X" ]; then
		info "Found a package of '$2': $X"
		eval $1=\$X
	else
		return 1
	fi
}

create_package() {
	try ${PKG_CREATE} -b "$1" "$2" || return 1
}

backup_package() {
	if is_yes ${opt_backup} && [ ! -e "$2" ]; then
		info "Backing up the old version"
		create_package "$1" "$2" || return 1
	fi
}

backup_file() {
	if [ -e "$1" ]; then
		info "Backing up the ${1##*/} file"
		try cp -f "$1" "$2" || return 1
	fi
}

restore_package() {
	if [ -e "$1" ]; then
		info "Restoring the old version"
		opt_force=YES install_package "$1" || return 1
	else
		return 1
	fi
}

restore_file() {
	if [ -e "$1" ] && [ ! -e "$2" ]; then
		info "Restoring the ${1##*/} file"
		try mv -f "$1" "$2" || return 1
	fi
}

process_package() {
	if is_yes ${opt_keep_backup} && [ -e "$1" ] && [ ! -e "${PKG_BACKUP_DIR}/${1##*/}" ]; then
		info "Keeping the old version in '${PKG_BACKUP_DIR}'"
		create_dir "${PKG_BACKUP_DIR}" || return 1
		try mv -f "$1" "${PKG_BACKUP_DIR}" || return 1
	fi
}

preserve_libs() {
	local file

	is_yes ${opt_preserve_libs} || return 0

	preserved_files=
	for file in $(${PKG_INFO} -qL "$1"); do
		case ${file##*/} in
		lib*.so.[0-9]*)
			if [ -f "${file}" ]; then
				preserved_files="${preserved_files} ${file}"
			fi ;;
		esac
	done

	if ! empty ${preserved_files}; then
		info "Preserving the shared libraries"
		create_dir "${PKGCOMPATDIR}" || return 1
		try cp -f ${preserved_files} "${PKGCOMPATDIR}" || return 1
	fi
}

clean_libs() {
	local delete_files file dest

	if empty ${preserved_files} || ! is_yes ${opt_preserve_libs}; then
		return 0
	fi

	info "Cleaning the preserved shared libraries"

	delete_files=
	for file in ${preserved_files}; do
		dest="${PKGCOMPATDIR}/${file##*/}"

		if [ -e "${file}" ]; then
			delete_files="${delete_files} ${dest}"
		else
			info "Keeping ${file} as ${dest}"
		fi
	done

	if ! empty ${delete_files}; then
		try rm -f ${delete_files} || return 1
	fi
}

fix_dependencies() {
	local p deps newdep opt_depends opt_required_by

	opt_depends=NO
	opt_required_by=NO
	pkg_depends 'deps' "$1"

	for p in ${deps}; do
		if [ ! -d "${PKG_DBDIR}/$p" ]; then
			pkg_glob 'newdep' "${p%-*}" 2>/dev/null

			if empty ${newdep}; then
				warn "'$1' depends on '$p', but it is NOT installed!"
			else
				update_pkgdep "$1" "$p" "${newdep##* }" || return 1
			fi
		fi
	done
}

update_dependencies() {
	local p req_by

	pkg_required_by 'req_by' "$2"

	if ! empty ${req_by}; then
		info "Updating the dependencies"

		for p in ${req_by}; do
			have_pkgdep "$p" "$1" || continue
			update_pkgdep "$p" "$1-[^-]*" "$2" || return 1
		done
	fi
}

have_pkgdep() {
	local X

	if [ -r "${PKG_DBDIR}/$1/+CONTENTS" ]; then
		while read X; do
			case $X in
			@pkgdep\ $2-[0-9]*|@pkgdep\ $2-pl[0-9]*)
				return 0 ;;
			[!@]*)	break ;;
			esac
		done < "${PKG_DBDIR}/$1/+CONTENTS"
	fi

	return 1
}

update_pkgdep() {
	update_file "${PKG_DBDIR}/$1/+CONTENTS" \
	"s/^@pkgdep $2\$/@pkgdep $3/" || return 1
}

delete_pkgdep() {
	update_file "${PKG_DBDIR}/$1/+CONTENTS" \
	"/^@pkgdep $2\$/,/^@comment DEPORIGIN:/ { s/^@comment /@/; s/^@/@comment DELETED:/; }" || return 1
}

restore_pkgdep() {
	update_file "${PKG_DBDIR}/$1/+CONTENTS" \
	"/^@comment DELETED:pkgdep $2\$/,/^@comment DELETED:DEPORIGIN:/ { s/^@comment DELETED:/@/; s/^@\\(DEPORIGIN:\\)/@comment \\1/; }" || return 1
}

trace_moved() {
	local IFS current traced moved

	file_exist "${PORTSDIR}/MOVED" || return 1

	current=${1#${PORTSDIR}/}
	traced=
	IFS='|'

	while moved=$(grep "^${current}|" "${PORTSDIR}/MOVED"); do
		set -- ${moved}

		if [ $# != 4 ]; then
			warn "'${PORTSDIR}/MOVED' may be broken. (format error)"
			return 1
		elif empty $2; then
			warn "'$1' has been removed from ports tree:"
			warn "  \"$4\" ($3)"
			return 1
		else
			case " ${traced} " in
			*" $2 "*)
				warn "'${PORTSDIR}/MOVED' may be broken - caused an infinite loop!"
				return 1 ;;
			esac

			current=$2
			traced="${traced} $1"

			warn "'$1' has moved to '$2':"
			warn "  \"$4\" ($3)"

			if [ -e "${PORTSDIR}/$2/Makefile" ]; then
				pkg_origin=$2
				pkg_portdir=${PORTSDIR}/$2
				return 0
			fi
		fi
	done

	return 1
}

init_result() {
	local i

	log_file=$1
	log_format=${2-"+:done -:ignored *:skipped !:failed"}
	log_length=0
	log_summary=

	for i in ${log_format}; do
		eval cnt_${i#*:}=0
		eval log_sign_${i#*:}=${i%%:*}
		log_summary="${log_summary:+${log_summary}, }\${cnt_${i#*:}} ${i#*:}"
	done

	if ! empty ${log_file}; then
		create_file "${log_file}" || log_file=
	fi
}

set_result() {
	local X

	log_length=$((log_length+1))
	eval cnt_$2=$((cnt_$2+1))

	if ! empty ${log_file}; then
		eval X=\${log_sign_$2:-?}
		echo "$X $1${3:+ ($3)}" >> "${log_file}"
	fi
}

show_result() {
	local X _descr

	if ! empty "${log_file}" && [ -s "${log_file}" ]; then
		_descr=
		for X in ${log_format}; do
			_descr="${_descr:+${_descr} / }$X"
		done

		info "Listing the results (${_descr})"

		while read X; do
			echo "        $X"
		done < "${log_file}"
	fi

	eval info \"Processed ${log_length}: ${log_summary}\"
}

trap_exit() {
	warn "Interrupted."

	if ! empty ${_proc-}; then
		if ! empty ${_arg-}; then
			set_result "${_arg}" failed aborted
		fi
		show_result
	fi

	if ! empty ${tmpdir}; then
		clean_tmpdir
	fi

	exit 1
}

perform() {
	local _proc _arg log status

	_proc=$1; shift
	init_result "${opt_result}"

	for _arg in ${1+"$@"}; do
		log=
		status=

		do_${_proc} "${_arg}" || status=failed
		set_result "${_arg}" "${status:-ignored}" "${log}"

		case ${status} in
		failed)
			warn "Fix the problem and try again."
			echo ;;
		skipped)
			warn "Skipping '${_arg}'${log:+ (${log})}." ;;
		esac

		if is_yes ${opt_verbose}; then
			eval info \"** [${log_length}/$#]: ${log_summary}\" >&2
		fi
	done
}

init_done() {
	if pkg_match ${opt_exclude}; then
		log="excluded"
		return 1
	fi

	if ! is_yes ${opt_force} && pkg_match ${IGNORE}; then
		log="ignored"
		return 1
	fi
}

init_pkg() {
	set_pkg_vars "$1" || return 1
	init_done || return 1
}

init_install() {
	case $1 in
	*.t[bg]z)
		set_pkg_vars_for_binary "$1" || return 1 ;;
	/*)	set_port_vars "$1" || return 1 ;;
	*)	set_port_vars "${PORTSDIR}/$1" || return 1 ;;
	esac
	init_done || return 1
}

init_replace() {
	local i replace_with

	init_pkg "$1" || return 1

	cur_pkg_name=${pkg_name}
	cur_pkg_pkgdir=${pkg_pkgdir}
	replace_with=

	for i in ${opt_replace}; do
		case "|${pkg_name}|${pkg_name%-*}|" in
		*\|${i%=*}\|*)
			replace_with=${i##*=}
			break ;;
		esac
	done

	case ${replace_with} in
	*.t[bg]z)
		pkg_binary=${replace_with} ;;
	?*)
		pkg_portdir=${replace_with}
		pkg_origin=${replace_with#${PORTSDIR}/} ;;
	esac

	if empty ${pkg_binary}; then
		if empty ${pkg_origin}; then
			log="no origin recorded"
			return 1
		elif [ ! -e "${pkg_portdir}/Makefile" ]; then
			trace_moved "${pkg_portdir}" || { log="removed"; return 1; }
		fi
	fi

	init_install "${pkg_binary:-${pkg_portdir}}" || return 1
}

do_install() {
	local pkg_log

	init_install "$1" || { status=skipped; return 0; }

	info "Installing '${pkg_name}'"

	if is_yes ${opt_noexecute}; then
		status=done
		return 0
	elif is_yes ${opt_interactive}; then
		prompt_yesno || return 0
	fi

	pkg_log=${opt_log_prefix:+"${opt_log_prefix}${pkg_name}.log"}

	if empty ${pkg_binary} && pkg_match ${USE_PKGS}; then
		if fetch_package "${pkg_name}"; then
			find_package 'pkg_binary' "${pkg_name}" || return 1
		elif is_yes ${opt_force}; then
			warn "Using the source instead of the binary package."
		else
			log="package not found"
			return 1
		fi
	fi

	if is_yes ${opt_fetch}; then
		if ! empty ${pkg_binary} || fetch_distfiles "${pkg_portdir}"; then
			status=done
			return 0
		else
			log="fetch error"
			return 1
		fi
	fi

	if empty ${pkg_binary}; then
		build_package "${pkg_portdir}" "${pkg_log}" || {
			log="build error"; return 1; }
	fi

	if is_yes ${opt_build}; then
		status=done
		return 0
	fi

	install_package "${pkg_binary:-${pkg_portdir}}" "${pkg_log}" || {
		log="install error"; return 1; }

	status=done

	fix_dependencies "${pkg_name}" || return 1
}

do_replace() {
	local pkg_log pkg_tmpdir old_package old_required_by preserved_files

	init_replace "$1" || { status=skipped; return 0; }

	if [ "${cur_pkg_name}" != "${pkg_name}" ]; then
		info "Replacing '${cur_pkg_name}' with '${pkg_name}'"
	elif is_yes ${opt_force}; then
		info "Reinstalling '${pkg_name}'"
	else
		warn "No need to replace '${pkg_name}'. (specify -f to force)"
		return 0
	fi

	if is_yes ${opt_noexecute}; then
		status=done
		return 0
	elif is_yes ${opt_interactive}; then
		prompt_yesno || return 0
	fi

	pkg_log=${opt_log_prefix:+"${opt_log_prefix}${pkg_name}.log"}

	if empty ${pkg_binary} && pkg_match ${USE_PKGS}; then
		if fetch_package "${pkg_name}"; then
			find_package 'pkg_binary' "${pkg_name}" || return 1
		elif is_yes ${opt_force}; then
			warn "Using the source instead of the binary package."
		else
			log="package not found"
			return 1
		fi
	fi

	if is_yes ${opt_fetch}; then
		if ! empty ${pkg_binary} || fetch_distfiles "${pkg_portdir}"; then
			status=done
			return 0
		else
			log="fetch error"
			return 1
		fi
	fi

	if empty ${pkg_binary}; then
		load_upgrade_vars "${cur_pkg_name}"

		build_package "${pkg_portdir}" "${pkg_log}" || {
			log="build error"; return 1; }
	fi

	if is_yes ${opt_build}; then
		status=done
		return 0
	fi

	pkg_tmpdir="${tmpdir}/${cur_pkg_name}"
	old_required_by="${pkg_tmpdir}/+REQUIRED_BY"

	if ! find_package 'old_package' "${cur_pkg_name}"; then
		old_package="${pkg_tmpdir}/${cur_pkg_name}${PKG_SUFX}"
	fi

	if ! {
		create_dir "${pkg_tmpdir}" &&
		backup_package "${cur_pkg_name}" "${old_package}" &&
		backup_file "${cur_pkg_pkgdir}/+REQUIRED_BY" "${old_required_by}" &&
		preserve_libs "${cur_pkg_name}"
	}; then
		log="backup error"
		try rm -rf "${pkg_tmpdir}" || :
		return 1
	fi

	if opt_force=YES deinstall_package "${cur_pkg_name}"; then
		if install_package "${pkg_binary:-${pkg_portdir}}" "${pkg_log}"; then
			status=done
			cur_pkg_pkgdir=${PKG_DBDIR}/${pkg_name}
		else
			log="install error"
			restore_package "${old_package}" || {
				warn "Failed to restore the old version," \
				"please reinstall '${old_package}' manually."
				return 1
			}
		fi
	else
		log="deinstall error"
	fi

	restore_file "${old_required_by}" "${cur_pkg_pkgdir}/+REQUIRED_BY" ||
		warn "Failed to restore the +REQUIRED_BY file."
	process_package "${old_package}" ||
		warn "Failed to keep the old version."
	clean_libs ||
		warn "Failed to remove the preserved shared libraries."
	try rm -rf "${pkg_tmpdir}" ||
		warn "Couldn't remove the working direcotry."

	case ${status} in
	done)
		fix_dependencies "${pkg_name}" || return 1
		update_dependencies "${cur_pkg_name%-*}" "${pkg_name}" || return 1 ;;
	*)
		return 1 ;;
	esac
}

do_version() {
	init_replace "$1" || { status=skipped; return 0; }

	case ${cur_pkg_name} in
	"${pkg_name}")	return 0 ;;
	esac

	status=done
	echo "${cur_pkg_name} -> ${pkg_name} (${pkg_origin:-${pkg_binary##*/}})"
}

main() {
	init_variables
	init_options
	parse_options ${1+"$@"}
	shift ${OPTC}

	load_config "/usr/local/etc/pkg_replace.conf"

	if ! empty ${PKG_REPLACE-}; then
		parse_options ${PKG_REPLACE}
	fi

	if is_yes ${opt_all} || { is_yes ${opt_version} && empty ${@+1}; }; then
		opt_depends=NO
		opt_required_by=NO
		set -- '*'
	elif empty ${@+1}; then
		usage
	fi

	if is_yes ${opt_use_packages}; then
		USE_PKGS='*'
	fi

	if ! is_yes ${opt_new}; then
		parse_args "$@"

		pkg_glob 'pkgs' ${pkgs}
		is_yes ${opt_version} || pkg_sort 'pkgs' ${pkgs}

		set -- ${pkgs}; unset pkgs
	fi

	if empty ${@+1}; then
		exit 1
	fi

	if is_yes ${opt_version}; then
		perform version "$@"
	else
		create_tmpdir || exit 1

		trap trap_exit 1 2 3 15

		if is_yes ${opt_new}; then
			perform install "$@"
		else
			perform replace "$@"
		fi

		show_result

		clean_tmpdir
	fi

	case ${cnt_done:-0},${cnt_failed:-0} in
	[!0],0)
		exit 0 ;;
	*)	exit 1 ;;
	esac
}

IFS=' 
'

case ${0##*/} in
pkg_replace)
	export UPGRADE_TOOL=pkg_replace
	main ${1+"$@"} ;;
esac
