#!/bin/sh

readonly XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
readonly CONFIG="$XDG_CONFIG_HOME/upgrade-ports"
export readonly PORTSDIR=${PORTSDIR:-/usr/ports}

NCPU=$(sysctl -n hw.ncpu)
[ "$NCPU" ] || NCPU=1

answer_yes=0	# answer yes to all of the questions
build_started=0 # ports build has started
force_recompile=0 # recompile packages with stale dependencies
interactive=0   # ask for confirmation for each package operation
interrupted=0   # build interrupted by ctrl+c
make_args="BATCH=yes DISABLE_LICENSES=yes DISABLE_VULNERABILITIES=yes"
ports_config=0	# set port(s) options before build
pre_build_clean=1 # run make clean before each build
skip_update=0	# don't update ports tree
tmpfile=""	# all purpose, global temporary file
user_selection=0 # user package selection was activated
wait_for_key=0	# wait for key press after upgrade finish

ask_yesno() {
	local answer

	[ $answer_yes -eq 0 ] || return 0
	while true; do
		printf "\n$1 (y/n)? [y] "

		read answer
		[ "$answer" ] || answer="y"

		case $answer in
		[Nn])
			return 1 ;;
		[Yy])
			return 0
		esac
	done
}

check_for_missing_deps() {
	local curr dep jobs=0 line pkgs_done pkgs_to_check

	list_add pkgs_to_check $need_install $need_reinstall $need_upgrade

	while true; do
		[ $jobs -eq 0 -a ! "$pkgs_to_check" ] && break

		if [ $jobs -lt $NCPU -a "$pkgs_to_check" ]; then
			curr=$(list_first pkgs_to_check)
			(
				for dep in $(pmake $curr -V FETCH_DEPENDS \
					-V EXTRACT_DEPENDS -V PATCH_DEPENDS \
					-V BUILD_DEPENDS -V LIB_DEPENDS \
					-V RUN_DEPENDS |
					tr ' ' '\n' | cut -d: -f2 | sort -u)
				do
					pkg_exists $dep && continue
					tmpfile_locked_write $curr:$dep
				done
				tmpfile_locked_write DONE:$curr
			) &
			jobs=$((jobs+1))
			list_remove pkgs_to_check $curr
		fi

		for line in $(tmpfile_locked_read); do
			case ${line%:*} in
			DONE)
				list_add pkgs_done ${line#*:}
				jobs=$((jobs-1)) ;;
			*)
				list_find pkgs_done ${line#*:} && continue
				list_add need_install ${line#*:}
				list_add $(to_env_str ${line#*:})_reqby ${line%:*}
				list_add $(to_env_str ${line%:*})_mdeps ${line#*:}
				list_add pkgs_to_check ${line#*:}
			esac
		done
	done
}

check_packages() {
	local data port pkg pkg_ignore

	echo "===> Checking packages for missing libraries/dependencies..."
	for pkg in $(pkg-static lock -lq); do
		list_add pkg_ignore $(pkg-static query %n $pkg)
	done
	eval $($SUDO pkg-static check -dan | awk -v pkg_ignore="$pkg_ignore" '
	BEGIN {
		if ((n = split(pkg_ignore, array, " ")))
			for (i=1; i<=n; i++)
				ignore[array[i]] = array[i];
		while ("ldconfig -r" | getline line) {
			split(line, array);
			if (array[2] != "=>")
				continue;
			n = split(array[3], path, "/");
			libname = substr(path[n], 1, index(path[n], ".so") + 2);
			syslibs[libname] = array[3];
		}
	}
	/has a missing dependency:/ {
		if ($6 in ignore)
			next;

		miss_deps[$1] = $1;
		if ($6 in install) {
			install[$6] = install[$6] " " $1;
			next;
		}
		install[$6] = $1;
		inst = inst ? inst " " $6 : $6;
	}
	/is missing a required shared library:/ {
		if ($1 in ignore || $1 in miss_deps)
			next;
		libname = substr($8, 1, index($8, ".so") + 2);
		if (libname in syslibs == 0) {
			#print "===> " $1 " linked to " $8 ", not found on the system" > "/dev/stderr"
			next;
		}
		if ($1 in rebuild) {
			rebuild[$1] = rebuild[$1] " " $8;
			next;
		}
		rebuild[$1] = $8;
		rinst = rinst ? rinst " " $1 : $1;
	}
	END {
		print "check_need_install=\"" inst "\" check_need_reinstall=\"" rinst "\"";
		for (pkg in install) {
			envname = pkg;
			gsub(/(\/|-|\.|\+|@)/, "_", envname);
			print envname "_reqby=\"" install[pkg] "\"";
		}
		for (pkg in rebuild) {
			envname = pkg;
			gsub(/(\/|-|\.|\+|@)/, "_", envname);
			print envname "_miss=\"" rebuild[pkg] "\"";
		}
	}')
	for pkg in $check_need_reinstall; do
		port=$(pkg_to_port $pkg)
		list_find build_failed $port && continue
		data=$(eval echo \$$(to_env_str $pkg)_miss)
		setvar $(to_env_str $port)_miss "$data"
	done
	need_reinstall_add $check_need_reinstall

	for pkg in $check_need_install; do
		port=$(pfind -N $pkg | cut -f1 -d' ')
		list_find build_failed $port && continue
		data=$(eval echo \$$(to_env_str $pkg)_reqby)
		if [ "$port" ]; then
			setvar $(to_env_str $port)_reqby "$data"
			list_add need_install $port
		else
			[ $force_recompile -eq 1 ] && need_reinstall_add $data
		fi
	done
	if [ "$need_install" -o "$need_reinstall" ]; then
		tmpfile_initialize
		check_for_missing_deps
		detect_conflicts
		print_list "Packages need to be REMOVED" $(to_rm_str $need_remove)
		print_list "Ports need to be INSTALLED" $(to_inst_str $need_install)
		print_list "Packages need to be REINSTALLED" $(to_reinst_str $need_reinstall)
		if ask_yesno "Continue with changes"; then
			pkg_remove $need_remove
			rebuild_ports $need_install $need_reinstall
		fi
	fi
}

clean_after_build_error() {
	local pkgfile=""

	if list_find conflicts_self $(port_to_pkg $1) || [ "$2" = "install" ]; then
		pkgfile=$(eval echo \$$(to_env_str $1)_backup)
	fi
	if [ "$pkgfile" -a -f "$pkgfile" ]; then
		echo "===> Restoring older package version from backup..."
		$SUDO pkg-static add --quiet $pkgfile
	fi
	if [ $interrupted -eq 1 ]; then
		build_interrupted=$1
	else
		skip_reverse_deps $1 "$2 error"
	fi
	$SUDO make clean
}

command_is_valid() {
	local old_IFS=$IFS

	IFS=" "
	if [ "${*%% *}" = "pkg" ]; then
		cmd_pkg_validate ${*#pkg }
	else
		cmd_pm_validate ${*#portmaster }
	fi
	ret=$?
	IFS=$old_IFS
	return $ret
}

cmd_pkg_validate() {
	local cmd=${*%% *} exists=0 pkgname

	[ "$cmd" = "delete" -o "$cmd" = "set" ] || return 1
	shift 1
	while getopts "fgn:o:y" opt; do
		case $opt in
		f|g|y)
			;;
		n|o)
			pkgname=${OPTARG%:*} ;;
		\?)
			echo "===> Pkg unhandled option"
			return 1
		esac
	done
	if [ ! "$cmd" = "set" ]; then
		shift $((OPTIND-1))
		pkgname=$*
	fi
	for pkg in $pkgname; do
		pkg_exists $pkg && exists=1
	done
	if [ $exists -eq 0 ]; then
		echo "===> $pkgname is not installed, skipping command"
		return 1
	fi
}

cmd_pm_validate() {
	local cmdline change_origin=0 exists=0 pkg pkgname

	while getopts "Rafo:r:w" opt; do
		case $opt in
		a)
			echo "===> Skipping upgrade of all packages for now"
			return 1 ;;
		f|w|R)
			;;
		o)
			change_origin=1 ;;
		r)
			list_add pkgname $OPTARG ;;
		\?)
			echo "===> Portmaster unhandled option, skipping"
			return 1
		esac
	done
	shift $((OPTIND-1))
	if [ $change_origin -eq 1 ]; then
		pkgname=$*
	else
		case $* in
		'$('*')')
			cmdline=$(echo $* | sed 's|^\$(||' | sed 's|)$||') ;;
		"\`"*"\`")
			cmdline=$(echo $* | sed 's|`||g') ;;
		*)
			list_add pkgname $*
		esac
	fi
	if [ "$cmdline" ]; then
		[ "$($cmdline)" ] && return 0 || return 1
	fi
	for pkg in $pkgname; do
		pkg_exists $pkg && exists=1
	done
	if [ $exists -eq 0 ]; then
		echo "===> $pkgname is not installed, skipping command"
		return 1
	fi
}

desc_pids() {
	local pids=$1 ret

	for pid in $(pgrep -P $1); do
		ret=$(desc_pids $pid)
		[ "$ret" ] && pids="$pids $ret"
	done
	echo $pids
}

detect_conflicts() {
	local cnfl jobs=0 line pkg port pkgs_to_check

	list_add pkgs_to_check $need_upgrade $need_install $need_reinstall

	while true; do
		[ $jobs -eq 0 -a ! "$pkgs_to_check" ] && break
		if [ $jobs -lt $NCPU -a "$pkgs_to_check" ]; then
			curr=$(list_first pkgs_to_check)
			(
				for cnfl in $(pmake $curr \
					-V CONFLICTS -V CONFLICTS_BUILD \
					-V CONFLICTS_INSTALL)
				do
					for pkg in $(pkg-static query -g %n "$cnfl"); do
						port=$(pkg_to_port $pkg)
						tmpfile_locked_write $curr:$port
					done
				done
				tmpfile_locked_write DONE:$curr
			) &
			jobs=$((jobs+1))
			list_remove pkgs_to_check $curr
		fi

		for line in $(tmpfile_locked_read); do
			case ${line%:*} in
			DONE)
				jobs=$((jobs-1))
				list_remove pkgs_to_check ${line#*:} ;;
			*)
				if [ "${line%:*}" = "${line#*:}" ]; then
					list_add conflicts_self \
						$(port_to_pkg ${line%:*})
				else
					pkg_mark_for_removal \
						$(port_to_pkg ${line#*:}) \
						"conflicts with $(to_pkg_str ${line%:*})"
				fi
			esac
		done
	done
}

display_usage_install() {
	<< EOF >&2 cat
Usage: ${0##*/} -h
       ${0##*/} [-Ccdiy] pkg-name|category/port|match-pattern ...

    -C - prevent running 'make clean' before each build
    -c - set port(s) options before install
    -d - build with debug support
    -h - show this help
    -i - interactive mode, confirm each matched port install
    -y - answer yes to all questions

EOF
	exit 1
}

display_usage_reinstall() {
	<< EOF >&2 cat
Usage: ${0##*/} -h
       ${0##*/} [-Ccdiy] [-r pkg-name] pkg-name|category/port|match-pattern ...

    -C - prevent running 'make clean' before each build
    -c - set port(s) options before reinstall
    -d - build with debug support
    -h - show this help
    -i - interactive mode, confirm each matched package reinstall
    -r - reinstall pkg-name and all its consumers
    -y - answer yes to all questions

EOF
	exit 1
}

display_usage_upgrade() {
	<< EOF >&2 cat
Usage:  ${0##*/} -c [-fwy]
        ${0##*/} -h
	${0##*/} [-Cfuwy]

    -C - prevent running 'make clean' before each build
    -c - only check packages database for errors
    -f - force recompile of packages with stale dependencies
    -h - show this help
    -u - skip ports tree update
    -w - wait for key press after finish
    -y - answer yes to all questions

EOF
	exit 1
}

fetch_distfiles() {
	$SUDO sh -c "
		lockf -k $tmpfile sh -c \" echo PID:\$\$ >> $tmpfile\"
		for port in $*; do
			make -C \"$PORTSDIR/\${port%%@*}\" checksum $make_args
			lockf -k $tmpfile sh -c \"echo \$port:\$? >> $tmpfile\"
		done
		lockf -k $tmpfile sh -c \" echo PID:0 >> $tmpfile\"
		" >/dev/null 2>&1 &
}

gather_missing_dependencies() {
	local port

	for port in $(eval echo \$$(to_env_str $1)_mdeps); do
		gather_missing_dependencies $port
		echo $port
	done
}

get_entry_dates() {
	grep -E "^[0-9]{8}:" "$PORTSDIR/UPDATING" | sed 's/://'
}

interactive_select() {
	local dwidth items msg=$1 pkg port selected tcols tlines

	[ $interactive -eq 1 ] || return

	shift 1
	[ "$*" ] || return
	user_selection=1

	for port in $*; do
		pkg=$(pmake $port -V PKGNAME)
		items="$items $port $pkg on"
	done

	tmpfile_initialize
	tcols=$(stty size | cut -d" " -f2)
	tlines=$(stty size | cut -d" " -f1)
	[ $tcols -lt 80 ] && dwidth=$((tcols-4)) || dwidth=76

	echo "--checklist \"$msg\" \
		$((tlines-3)) $dwidth $((tlines-5)) $items" > $tmpfile
	selected=$(dialog --stdout --file $tmpfile)
	dialog --clear
	echo -n > $tmpfile

	for port in $*; do
		list_find selected $port && continue
		list_remove need_downgrade $port
		list_remove need_install $port
		list_remove need_reinstall $port
		list_remove need_upgrade $port
	done
}

list_add() {
	local arg name=$1 list=$(eval echo \$$1)

	shift 1
	for arg in $*; do
		list_find $name $arg && continue
		list="$list $arg"
	done
	setvar $name "$list"
}

list_find() {
	local elem

	for elem in $(eval echo \$$1); do
		[ "$elem" = "$2" ] && return 0
	done
	return 1
}

list_first() {
	local elem

	for elem in $(eval echo \$$1); do
		echo $elem
		return
	done
}

list_remove() {
	local elem name=$1 new_list

	for elem in $(eval echo \$$1); do
		[ "$elem" = "$2" ] || new_list="$new_list $elem"
	done
	setvar $name "$new_list"
}

moved_reason() {
	awk -F '|' -v line="$moved_last" -v port="$1" '
		NR > line && $1 == port { print $4 }' "$PORTSDIR/MOVED"
}

need_install_add() {
	if pkg_exists $1; then
		[ "$2" ] && echo "===> $1 already installed"
		return 1
	fi
	list_add need_install $1
}

need_reinstall_add() {
	local pkg port

	for pkg in $*; do
		pkg_exists $pkg || continue
		pkg_check_status $pkg || continue
		port=$(pkg_to_port $pkg)

		case $(pkg-static version -n $pkg | awk '{print $2}') in
		'>')
			list_add need_downgrade $port ;;
		'<')
			list_add need_upgrade $port ;;
		*)
			list_add need_reinstall $port
		esac
	done
}

pkg_backup() {
	local file path=$2 pkg=$1 port

	echo "===> Backuping $(pkg-static query %n-%v $pkg)..."
	file=$($SUDO pkg-static create --out-dir $path $pkg |
		awk '{ print $4 ".txz" }')
	[ "$file" ] || return 1

	port=$(pkg_to_port $pkg)
	setvar $(to_env_str $port)_backup "$path/$file"
}

pkg_check_status() {
	local neworigin pkg=$1 pkgbase pkgname port

	if [ "$(pkg-static info -kq $pkg)" = "yes" ]; then
		echo "===> $pkg is locked, skipping"
		return 1
	fi

	port=$(pkg_to_port $pkg)
	pkgbase=$(pmake $port -V PKGBASE)
	pkgname=$(pkg-static query %n $pkg)

	if [ "$pkgbase" != "$pkgname" ]; then
		neworigin=$(pfind -N $pkgname | cut -f1 -d' ')

		if [ "$neworigin" -a "${port%@*}" != "${neworigin%@*}" ]; then
			$SUDO pkg-static set --change-origin ${port%@*}:${neworigin%@*} --yes
			if [ $? -eq 0 ]; then
				echo "===> $pkgname changed its origin from ${port%@*} to ${neworigin%@*}"
				need_reinstall_add $(pkg-static query %rn $pkg)
				port=$neworigin
			fi
		else
			list_add conflicts_self $pkgname
		fi
	fi
	if [ "$run_mode" = "upgrade" ] && [ "$pkgname" = "perl5" ]; then
		pkg_version=$(pkg-static query %v $pkg)
		port_version=$(pmake $port -V PKGVERSION)

		if [ "$pkg_version" = "$port_version" ]; then
			echo "===> $pkgname needs manual intervention to upgrade, skipping"
			return 1
		fi
	fi
	setvar $(to_env_str $port)_pkgname $pkgname
	setvar $(to_env_str $port)_version $(pmake $port -V PKGVERSION)
}

pkg_exists() {
	local pkgname

	[ "$1" ] || return 1
	case $1 in
	*/*)
		pkgname=$(port_to_pkg $1) ;;
	*)
		pkgname=$1
	esac
	[ "$pkgname" ] || return 1
	pkg-static info --exists $pkgname >/dev/null 2>&1
}

pkg_mark_for_removal() {
	local pkg

	list_add need_remove $1
	setvar $(to_env_str $1)_conflicts "$2"
	[ "$3" ] && return
	for pkg in $(pkg-static query %rn $1); do
		pkg_mark_for_removal $pkg "depends on $1"
	done
}

pkg_remove() {
	local pkg pkgrepo=$(make -f "${PORTSDIR}/Mk/bsd.port.mk" -V PKGREPOSITORY)

	[ "$*" ] || return
	for pkg in $*; do
		pkg_backup $pkg "$pkgrepo"
	done
	$SUDO pkg-static delete --quiet --yes --force $*
}

pkg_to_port() {
	local origin=$(pkg-static query %o $1)
	local flavor=$(pkg-static info $1 | awk '
		/flavor/ && NF == 3 && $2 == ":" { print $3 }')
	local flavors=$(pmake $origin -V FLAVORS | sed 's,^ ,,')
	local f pkgbase pkgname

	if [ "$flavors" -a ! "$flavor" ]; then
		pkgname=$(pkg-static query %n $1)
		for f in $flavors; do
			pkgbase=$(pmake $origin@$f -V PKGBASE)
			if [ "$pkgname" = "$pkgbase" ]; then
				echo "$origin@$f"
				return
			fi
		done
	fi
	if [ "$flavor" ]; then
		echo "$origin@$flavor"
	else
		echo $origin
	fi
}

pm_fake_run() {
	local new_origin pkg pkg_list=""

	while getopts "Rfo:r:w" opt; do
		case $opt in
		f|w|R)
			;;
		o)
			new_origin=$OPTARG ;;
		r)
			case $OPTARG in
			*/*)
				[ -d "$PORTSDIR/$OPTARG" ] || continue
				pkg=$(port_to_pkg $OPTARG) ;;
			*)
				pkg=$OPTARG
			esac
			[ "$pkg" ] || continue
			list_add pkg_list $pkg $(pkg-static query %rn $pkg)
		esac
	done
	shift $((OPTIND-1))
	if [ "$new_origin" ]; then
		$SUDO pkg-static set -o $*:$new_origin --yes
		return $?
	fi
	list_add pkg_list $*
	if [ "$pkg_list" ]; then
		printf "===> Found packages to reinstall in later stage:\n%s\n" \
			"$pkg_list"
		need_reinstall_add $pkg_list
	fi
}

pmake() {
	local farg flavor=${1##*@} port=${1%%@*}

	[ -d "$PORTSDIR/$port" ] || return
	[ "$flavor" != "$port" ] && farg="FLAVOR=$flavor"
	shift 1
	make -C "$PORTSDIR/$port" $farg $*
}

ports_configure() {
	local port
	[ $ports_config -eq 1 ] || return

	for port in $*; do
		cd "$PORTSDIR/${port%%@*}" 2>/dev/null || continue
		[ "$(make -V COMPLETE_OPTIONS_LIST)" ] || continue
		$SUDO make config
	done
}

port_to_pkg() {
	local port=$1
	local pkg=$(eval echo \$$(to_env_str $port)_pkgname)

	if [ "$pkg" ]; then
		echo $pkg
		return
	fi
	pkg=$(pmake $port -V PKGBASE)
	echo $pkg
}

ports_upgrade_sort() {
	local all_ports=$* djobs=0 flavors next port ports_list=$*

	if grep -q 'DEPENDS_SHOW_FLAVOR' "$PORTSDIR/Mk/bsd.port.mk"; then
		flavors="-DDEPENDS_SHOW_FLAVOR"
	fi
	while true; do
		[ $djobs -eq 0 -a ! "$ports_list" ] && break

		if [ $djobs -lt $NCPU -a "$ports_list" ]; then
			next=$(list_first ports_list)
			(
				deps=$(pmake $next all-depends-list $flavors |
					sed "s,$PORTSDIR/,,g")
				tmpfile_locked_write START:$next $deps END:$next
			) &
			djobs=$((djobs+1))
			list_remove ports_list $next
		fi
		for line in $(tmpfile_locked_read); do
			case ${line%:*} in
			START)
				port=$(to_env_str ${line#*:}) ;;
			END)
				eval export ${port}_alldeps
				djobs=$((djobs-1)) ;;
			*)
				list_find all_ports $line &&
					list_add ${port}_alldeps $line
			esac
		done
	done

	echo $* | tr ' ' '\n' | awk -v flavors="$flavors" '
	function partition(a, s, e) {
		i = s-1; j = e+1;

		while(1) {
			do i++; while (i < n_ports+1 && ports_cmp(a[i], a[s]) < 0)
			do j--; while (j > -1 && ports_cmp(a[j], a[s]) > 0)

			if (i >= j)
				return j;

			tmp = a[i]; a[i] = a[j]; a[j] = tmp;
		}
	}
	function ports_cmp(port_a, port_b) {
		if (port_a == port_b)
			return 0;

		if (flavors == "") {
			split(port_a, port_array, "@");
			port = port_array[1];
		} else
			port = port_a;

		if (match(port_deps[port_b], port) != 0)
			return -1;

		if (flavors == "") {
			split(port_b, port_array, "@");
			port = port_array[1];
		} else
			port = port_b;

		if (match(port_deps[port_a], port) != 0)
			return 1;

		return port_ndeps[port_a] - port_ndeps[port_b];
	}
	function qsort(a, start, end) {
		if (start >= end)
			return;
		p = partition(a, start, end);
		qsort(a, start, p);
		qsort(a, p+1, end);
	}
	{
		envname = $0 "_alldeps";
		gsub(/(\/|-|\.|\+|@)/, "_", envname);
		ports[++n_ports] = $0;
		port_deps[$0] = ENVIRON[envname];
		port_ndeps[$0] = split(port_deps[$0], adeps);
	}
	END {
		qsort(ports, 1, n_ports);
		for(x=1; x<=n_ports; x++)
			print ports[x];
	}'
}

print_list() {
	local item title=$1

	[ "$2" ] || return
	shift 1
	printf "\n$title [$#]:\n"
	for item in $*; do printf "\t$item\n" | sed 's|,,| |g'; done | sort
}

rebuild_ports() {
	local errlog error full_queue make_cmd port queue ret t
	local operation=$run_mode conflict_port flavor itarget wrkdir

	tmpfile_initialize

	if [ $# -gt 1 ]; then
		[ "$operation" = "check" ] && operation="reinstall"
		echo "===> Calculating ports $operation order..."
		queue=$(ports_upgrade_sort $*)
	else
		queue=$*
	fi
	for port in $queue; do
		[ -d "$PORTSDIR/${port%%@*}" ] || continue
		mdeps=$(gather_missing_dependencies $port)
		list_add full_queue $mdeps $port
	done
	if [ "$conflicts_self" ]; then
		echo "===> Backuping and removing self-conflicting packages..."
		pkg_remove $conflicts_self
	fi
	fetch_distfiles $full_queue
	errlog=$(tmpfile_create "-errlog")
	list_add upgrade_tmpfiles $errlog
	for port in $full_queue; do
		if [ $interrupted -eq 1 ]; then
			list_add build_skipped $port
			continue
		fi
		list_find build_skipped $port && continue

		cd "$PORTSDIR/${port%%@*}"
		if [ "$(make -V FLAVORS)" ] && [ "${port##*@}" != "${port%%@*}" ]; then
			flavor="FLAVOR=${port##*@}"
		else
			flavor=""
		fi

		for t in BROKEN IGNORE; do
			error=$(make $flavor $make_args -V$t)
			if [ "$error" ]; then
				skip_reverse_deps $port "$t: $error"
				continue 2
			fi
		done

		if ! wait_for_distfiles $port; then
			skip_reverse_deps $port "failed to fetch"
			continue
		fi
		[ $build_started -eq 1 ] || build_started=1
		wrkdir=$(pmake $port -V WRKDIR)
		[ -d "$wrkdir" -a $pre_build_clean -eq 1 ] && $SUDO make clean
		if ! $SUDO make $flavor $make_args build; then
			clean_after_build_error $port build
			continue
		fi
		if ! $SUDO make $flavor $make_args stage; then
			clean_after_build_error $port stage
			continue
		fi
		if [ "$SUDO" ] && [ "$port" = "devel/gettext-runtime" -o \
			"$port" = "security/sudo" ];
		then
			itarget="deinstall install clean"
		else
			if pkg_exists $port; then
				pkg_backup $(port_to_pkg $port) "$wrkdir"
				if ! $SUDO make $flavor $make_args deinstall; then
					clean_after_build_error $port deinstall
					continue
				fi
			fi
			itarget="install clean"
		fi
		while true; do
			$SUDO make $flavor $make_args $itarget 2>$errlog
			if [ $? -eq 0 ]; then
				ret=0
				break
			fi
			eval $(cat $errlog | awk '
				/conflicts with/ { print "CONFLICT=" $5 }')
			if [ "$CONFLICT" ]; then
				conflict_port=$(pkg_to_port $CONFLICT)
				if [ "${port%%@*}" = "${conflict_port%%@*}" ]; then
					# probably shifting from non-flavored
					# to flavored version, should be safe to remove
					pkg_remove $CONFLICT
					unset CONFLICT
					continue
				fi
				if list_find need_reinstall $conflict_port &&
				   ! list_find build_ok $conflict_port; then
					# conflicting port is marked for reinstall
					# hopefully this will address conflict
					# and if not we will deal with conflict later
					pkg_remove $CONFLICT
					unset CONFLICT
					continue
				else
					unset need_remove
					pkg_mark_for_removal $(pkg-static query %n $CONFLICT) \
						"conflicts with $port"
					printf "\n===> $port install conflicts with $CONFLICT detected\n"
					print_list "Following packages need to be REMOVED" \
						$(to_rm_str $need_remove)
					if ask_yesno "Proceed"; then
						pkg_remove $need_remove
						unset CONFLICT
						continue
					fi
				fi
			fi
			ret=1
			break
		done
		if [ $ret -ne 0 ]; then
			clean_after_build_error $port install
		else
			list_add build_ok $port
		fi
	done
	rm "$errlog"
	if [ "$build_failed" -o "$build_interrupted" ]; then
		if [ $interrupted -eq 1 ]; then
			printf "\n===> Execution of $operation operation was interrupted.\n"
		else
			printf "\n===> Not all operations were successfull.\n"
		fi
		print_list "Following ports builds SUCCEEDED" $build_ok
		print_list "Following ports builds FAILED" $(to_fail_str $build_failed)
		print_list "Following build was INTERRUPTED" $build_interrupted
		print_list "Following ports builds were SKIPPED" $(to_fail_str $build_skipped)
		upgrade_exit 1 $last_upd
	fi
	unset conflicts_self need_downgrade need_install need_reinstall
	unset need_remove need_upgrade
}

reverse_deps() {
	local dep pkg port

	pkg=$(port_to_pkg $1)
	[ "$pkg" ] || return
	for dep in $(pkg-static query %rn $pkg); do
		port=$(pkg_to_port $dep)
		[ "$port" ] && echo $port
	done
}

show_operations_summary() {
	tmpfile_initialize
	check_for_missing_deps
	detect_conflicts

	print_list "Following packages need to be REMOVED" $(to_rm_str $need_remove)
	print_list "Following ports will be INSTALLED" $(to_inst_str $need_install)
	print_list "Following packages will be REINSTALLED" $(to_pkg_str $need_reinstall)
	print_list "Following packages will be UPGRADED" $(to_upgrade_str $need_upgrade)
	print_list "Following packages will be DOWNGRADED" $(to_upgrade_str $need_downgrade)
	if ask_yesno "Proceed with $run_mode"; then
		pkg_remove $need_remove
		rebuild_ports $need_downgrade $need_install $need_reinstall $need_upgrade
	else
		upgrade_exit 0 $last_read
	fi
}

signal_handler() {
	local pids

	printf "\n===> Caught interrupt signal, cleaning up\n"
	interrupted=1
	if [ "$fetcher_pid" -a "$fetcher_pid" != "0" ]; then
		pids=$(desc_pids $fetcher_pid)
		$SUDO kill -9 $pids >/dev/null 2>&1
	fi
	[ $build_started -eq 0 ] || return
	upgrade_exit 1 $last_read
}

skip_reverse_deps() {
	local consumer rev_deps

	if ! list_find build_skipped $1; then
		list_add build_failed $1
		setvar $(to_env_str $1)_fail "$2"
	fi

	if pkg_exists $1; then
		rev_deps=$(reverse_deps $1)
	else
		rev_deps=$(eval echo \$$(to_env_str $1)_reqby)
	fi
	for consumer in $rev_deps; do
		if list_find need_install $consumer ||
		   list_find need_reinstall $consumer ||
		   list_find need_upgrade $consumer;
		then
			list_add build_skipped $consumer
			setvar $(to_env_str $consumer)_fail "$1: $2"
			skip_reverse_deps $consumer "dependency skipped"
		fi
	done
}

tmpfile_create() {
	local temp=$(mktemp -t ${0##*/}$1)

	if [ ! "$temp" ]; then
		echo "===> Failed to create temporary file" >&2
		upgrade_exit 1
	fi

	echo $temp
}

tmpfile_initialize() {
	[ "$tmpfile" ] && return

	tmpfile=$(tmpfile_create)
	list_add upgrade_tmpfiles $tmpfile
}

tmpfile_locked_read() {
	lockf -k $tmpfile sh -c "
		[ -s \"$tmpfile\" ] || exit
		cat $tmpfile
		echo -n > $tmpfile"
}

tmpfile_locked_write() {
	lockf -k $tmpfile sh -c "
		for line in $*; do
			echo \$line >> $tmpfile
		done"
}

to_env_str() {
	echo "$1" | sed -E 's,(\/|-|\.|\+|@),_,g'
}

to_fail_str() {
	local failure port str

	for port in $*; do
		failure=$(eval echo \$$(to_env_str $port)_fail | sed 's| |,,|g')
		[ "$failure" ] && list_add str "$port,,($failure)" ||
			list_add str $port
	done
	[ "$str" ] && echo $str
}

to_inst_str() {
	local count msg port reqby str

	for port in $*; do
		msg=$port
		reqby=$(eval echo \$$(to_env_str $port)_reqby)

		if [ "$reqby" ]; then
			msg="$msg,,(required,,by,,"
			case $reqby in
			*\ *)
				count=$(echo "$reqby" | wc -w)
				msg="$msg${reqby%% *},,and,,$((count-1)),,more)" ;;
			*)
				msg="$msg$reqby)"
			esac
		fi

		list_add str $msg
	done
	[ "$str" ] && echo $str
}

to_reinst_str() {
	local defect miss pkg port str

	for port in $*; do
		miss=$(eval echo \$$(to_env_str $port)_miss | sed 's| |,,|g')
		pkg=$(to_pkg_str $port)
		case $miss in
		*.so*)
			defect="misses,,libraries" ;;
		*)
			defect="stale,,dependency"
		esac
		list_add str "$pkg,,($defect:,,$miss)"
	done
	[ "$str" ] && echo $str
}

to_obs_str() {
	local pkg port reason str

	for pkg in $*; do
		port=$(pkg_to_port $pkg)
		reason=$(moved_reason $port | sed 's| |,,|g')
		list_add str "$pkg,,($reason)"
	done
	[ "$str" ] && echo $str
}

to_pkg_str() {
	local pkg port str

	for port in $*; do
		pkg=$(port_to_pkg $port)
		pkg=$(pkg-static query %n-%v $pkg)
		[ "$pkg" ] || pkg=$port
		list_add str $pkg
	done
	[ "$str" ] && echo $str
}

to_port_str() {
	local arg matched pkg str

	for arg in $*; do
		case $arg in
		*/*)
			[ -d "$PORTSDIR/${arg%%@*}" ] && list_add str $arg ;;
		*)
			matched=$(pkg-static info --quiet | grep ^$arg)
			[ "$matched" ] || continue
			for pkg in $matched; do
				list_add str $(pkg_to_port $pkg)
			done
		esac
	done
	[ "$str" ] && echo $str
}

to_rm_str() {
	local pkg pkgname reason str

	for pkg in $*; do
		reason=$(eval echo \$$(to_env_str $pkg)_conflicts | sed 's| |,,|g')
		pkgname=$(pkg-static query %n-%v $pkg)
		list_add str "$pkgname,,($reason)"
	done
	[ "$str" ] && echo $str
}

to_upgrade_str() {
	local curr new_ver pkg port str

	for port in $*; do
		pkg=$(port_to_pkg $port)
		curr=$(pkg-static query "%n:,,%v" $pkg)
		new_ver=$(eval echo \$$(to_env_str $port)_version)
		list_add str "$curr,,->,,$new_ver"
	done
	[ "$str" ] && echo $str
}

update_ports_tree() {
	local last_entry=$(tail -n 1 "$PORTSDIR/MOVED")
	local moved_lines=$(wc -l "$PORTSDIR/MOVED" | awk '{ print $1 }')
	local line_n newname oldname pkg port ports

	[ $skip_update -eq 0 ] || return

	if ! $SUDO portsnap fetch update; then
		echo "===> Ports tree update failed" >&2
		upgrade_exit 1
	fi
	line_n=$(grep -n "$last_entry" "$PORTSDIR/MOVED" | cut -d: -f1)
	[ "line_n" ] && moved_last=$line_n || moved_last=$moved_lines

	for port in $(awk -F '|' -v line="$moved_last" '
		NR > line && !$2 { print $1 }' "$PORTSDIR/MOVED")
	do
		pkg=$(pkg-static query %n-%v $port)
		[ "$pkg" ] || continue
		list_add obsolete $pkg
	done
	print_list "Following packages are marked as OBSOLETE" $(to_obs_str $obsolete)
	for pkg in $obsolete; do
		ask_yesno "Remove $pkg" &&
			$SUDO pkg-static delete --quiet --yes --force $pkg
	done
	for ports in $(awk -F '|' -v line="$moved_last" '
		NR > line && $2 { print $1 ":" $2 }' "$PORTSDIR/MOVED")
	do
		oldname=$(pkg-static query %n ${ports%%:*})
		[ "$oldname" ] || continue
		newname=$(pmake ${ports##*:} -V PKGBASE)
		[ "$newname" ] || continue

		reason=$(moved_reason ${ports%%:*})
		echo "===> ${ports%%:*} moved to ${ports##*:} ($reason)"
		if [ "$oldname" != "$newname" ]; then
			if ! $SUDO pkg-static set --change-name $oldname:$newname \
				--yes 2>/dev/null;
			then
				pkg_mark_for_removal $oldname "$reason" nodeps
				need_install_add ${ports##*:}
				continue
			fi
		fi
		if ! $SUDO pkg-static set --change-origin $ports --yes 2>/dev/null; then
			pkg_mark_for_removal $oldname "$reason" nodeps
			need_install_add ${ports##*:}
		fi
	done
}

updating_commands() {
	awk -F ':' -v entry="$1" '
	function strip_command(str) {
		sub("^[ \t#]*", "", str);
		in_cmd = sub(/\\$/, "", str);
		return str;
	}
	/^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]:/ {
		if (entry == $1)
			in_entry=1;
		else if (in_entry)
			exit;
	}
	in_entry && !in_cmd {
		if (match($0, /^[ \t#]*(pkg|portmaster) /)) {
			cmd = strip_command($0);
			if (!in_cmd) {
				if (!match(cmd, "^pkg upgrade"))
					print cmd;
				cmd="";
			}
		}
		next;
	}
	in_entry && in_cmd {
		line = strip_command($0);
		if (in_cmd)
			cmd=cmd line;
		else {
			print cmd line;
			cmd="";
		}
	}' "$PORTSDIR/UPDATING"
}

updating_entry() {
	awk -F ':' -v entry="$1" '
	/^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]:/ {
		if (entry == $1)
			in_entry=1;
		else if (in_entry)
			exit;
	}
	in_entry { print $0 }' "$PORTSDIR/UPDATING"
}

# $1 - exit status
# $2 - last read/executed UPDATING entry
# $3 - wait for final keypress?
upgrade_exit() {
	local key keywait=$3 temp

	[ "$2" ] && echo $2 > "$CONFIG"
	[ "$keywait" ] || keywait=$wait_for_key

	for temp in $upgrade_tmpfiles; do
		[ -f "$temp" ] && rm -rf "$temp"
	done

	if [ $keywait -eq 1 ]; then
		printf "\n===> Press Enter key to finish.\n\n"
		read key
	fi
	exit $1
}

upgrade_ports() {
	local cmd entry entry_prev old_IFS pkg port ret
	# needs to be global, used by upgrade_exit() in few places
	last_upd=$(get_entry_dates | head -n 1)

	if [ $last_read -lt $last_upd ]; then
		tmpfile_initialize
		printf "===> New entries since last upgrade in $PORTSDIR/UPDATING file:\n\n"
		for entry in $(get_entry_dates | sort -u); do
			[ $entry -gt $last_read ] || continue

			updating_entry $entry
			updating_commands $entry > $tmpfile

			old_IFS=$IFS
			IFS=$'\n'

			for cmd in $(cat $tmpfile); do
				command_is_valid $cmd || continue

				if ask_yesno "Run '$cmd'"; then
					IFS=$old_IFS
					case $cmd in
					"pkg "*)
						$SUDO $cmd ;;
					*)
						pm_fake_run ${cmd#portmaster }
					esac
					ret=$?
					IFS=$'\n'

					if [ $ret -ne 0 ]; then
						echo "===> Upgrade command '$cmd' failed" >&2
						upgrade_exit $ret $entry_prev
					fi
				fi
			done

			IFS=$old_IFS
			entry_prev=$entry
		done
		echo -n > $tmpfile
	fi

	echo "===> Checking for ports needing upgrade..."
	for pkg in $(pkg-static version -l '<' | cut -f1 -d' '); do
		pkg_check_status $pkg || continue
		port=$(pkg_to_port $pkg)
		list_add need_upgrade $port
		list_remove need_reinstall $port
	done
	if [ ! "$need_upgrade" ]; then
		echo "===> No ports needing upgrade found"
		upgrade_exit 0 $last_upd
	fi
	show_operations_summary
}

wait_for_distfiles() {
	local done=0 f_res=0 fetched=$(eval echo \$$(to_env_str $1)_fetch)
	local msg_shown=0 res

	[ "$fetched" ] && return $fetched

	while true; do
		for res in $(tmpfile_locked_read); do
			if [ "${res%:*}" = "PID" ]; then
				fetcher_pid=${res#*:}
				continue
			fi
			setvar $(to_env_str ${res%:*})_fetch ${res#*:}
			if [ "${res%:*}" = "$1" ]; then
				done=1
				f_res=${res#*:}
			fi
		done
		[ $interrupted -eq 0 ] || return 1
		[ $done -eq 0 ] || return $f_res
		if [ $msg_shown -eq 0 ] ; then
			echo "===> Waiting for $1 distfiles to fetch..."
			msg_shown=1
		fi
		while [ ! -s $tmpfile ]; do sleep 1; done
	done
}

trap signal_handler INT

case ${0##*/} in
	pinstall)
		run_mode="install"
		while getopts "Ccdhiy" option; do
			case $option in
			C)
				pre_build_clean=0 ;;
			c)
				ports_config=1 ;;
			d)
				make_args="$make_args WITH_DEBUG=yes" ;;
			i)
				interactive=1 ;;
			y)
				answer_yes=1 ;;
			*)
				display_usage_install
			esac
		done
		shift $((OPTIND-1)) ;;
	preinstall)
		run_mode="reinstall"
		while getopts "Ccdhir:y" option; do
			case $option in
			C)
				pre_build_clean=0 ;;
			c)
				ports_config=1 ;;
			d)
				make_args="$make_args WITH_DEBUG=yes" ;;
			i)
				interactive=1 ;;
			r)
				if ! pkg_exists $OPTARG; then
					printf "$OPTARG: no such package\n\n" >&2
					display_usage_reinstall
				fi
				need_reinstall_add $OPTARG \
					$(pkg-static query %rn $OPTARG)
				;;
			y)
				answer_yes=1 ;;
			*)
				display_usage_reinstall
			esac
		done
		shift $((OPTIND-1)) ;;
	upgrade-ports)
		run_mode="upgrade"
		while getopts "Ccfhuwy" option; do
			case $option in
			C)
				pre_build_clean=0 ;;
			c)
				run_mode="check" ;;
			f)
				force_recompile=1 ;;
			u)
				skip_update=1 ;;
			w)
				wait_for_key=1 ;;
			y)
				answer_yes=1 ;;
			*)
				display_usage_upgrade
			esac
		done ;;
	*)
		echo "===> ${0##*/}: bad frontend name" >&2
		exit 1
esac

if [ ! -d "$PORTSDIR" ]; then
	echo "===> Ports directory not found: $PORTSDIR" >&2
	echo "===> Try setting PORTSDIR environment variable" >&2
	exit 1
fi

if [ $(id -u) -ne 0 ]; then
	if ! which sudo >/dev/null; then
		echo "===> sudo not found" >&2
		echo "===> This script requires root privileges or properly configured sudo." >&2
		upgrade_exit 1
	fi
	SUDO="sudo"
fi

if ! which pfind >/dev/null; then
	echo "===> Required script pfind not found" >&2
	exit 1
fi

case $run_mode in
check)
	check_packages ;;
install)
	[ "$*" ] || display_usage_install
	echo "===> Checking ports selected for install..."
	for pattern in $*; do
		case $pattern in
		*/*)
			[ -d "$PORTSDIR/$pattern" ] || continue
			need_install_add $pattern verbose ;;
		*)
			for port in $(pfind -n $pattern | grep -v INSTALLED); do
				need_install_add $port verbose
			done
		esac
	done
	interactive_select "Select ports to install:" $need_install
	if [ ! "$need_install" ]; then
		[ $user_selection -ne 1 ] &&
			echo "===> No suitable ports to install found" >&2
		upgrade_exit 1
	fi
	ports_configure $need_install
	show_operations_summary ;;
reinstall)
	[ "$need_reinstall" -o "$*" ] || display_usage_reinstall
	echo "===> Checking ports selected for reinstall..."
	for pattern in $*; do
		case $pattern in
		*/*)
			need_reinstall_add $(port_to_pkg $pattern) ;;
		*)
			if pkg_exists $pattern; then
				need_reinstall_add $pattern
				continue
			fi
			need_reinstall_add $(pkg-static info --quiet | grep $pattern)
		esac
	done
	interactive_select "Select packages for reinstall:" $need_reinstall \
		$need_upgrade $need_downgrade
	if [ ! "$need_reinstall" -a ! "$need_upgrade" -a ! "$need_downgrade" ]; then
		[ $user_selection -ne 1 ] &&
			echo "===> No suitable packages to reinstall found" >&2
		upgrade_exit 1
	fi
	ports_configure $need_downgrade $need_reinstall $need_upgrade
	show_operations_summary ;;
upgrade)
	if [ -f "$CONFIG" ]; then
		last_read=$(cat "$CONFIG")
	else
		[ -d "$XDG_CONFIG_HOME" ] || mkdir "$XDG_CONFIG_HOME"
	fi
	[ "$last_read" ] || last_read=$(get_entry_dates | head -n 1)

	update_ports_tree
	upgrade_ports
	check_packages
esac

upgrade_exit 0 $last_upd
