#!/bin/sh -ef
#
# Copyright (C) 2003-2007  Dmitry V. Levin <ldv@altlinux.org>
# Copyright (C) 2006  Alexey Gladkov <legion@altlinux.org>
# Copyright (C) 2007  Kirill A. Shutemov <kas@altlinux.org>
# 
# This file defines functions used by hasher scripts.
#
# This file is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
#

unset CDPATH ||:
PROG="${0##*/}"

Info()
{
	printf %s\\n "${0##*/}: $*" >&2
}

Fatal()
{
	Info "$@"
	exit 1
}

# Create Var_of_$1 variable and set it to $2 if exist, otherwise to $1
Helpify() # var_name=var_value [cmdline_key_name]
{
	eval "Var_of_${1%%=*}='
                                    (\$${2:-${1%%=*}})'"
}

quiet=
verbose=
Verbose()
{
	[ -n "$verbose" ] || return 0
	printf %s\\n "${0##*/}: $*" >&2
}

print_version()
{
	local prog
	prog="$1" && shift
	cat <<EOF
$prog version 1.2.7
Written by Dmitry V. Levin <ldv@altlinux.org>

Copyright (C) 2003-2007  Dmitry V. Levin <ldv@altlinux.org>
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
EOF
	exit
}

show_usage()
{
	[ -z "$*" ] || Info "$*"
	echo "Try \`$PROG --help' for more information." >&2
	exit 1
}

# safe umask.
umask 022

# save current work directory.
saved_cwd="$(/bin/pwd)"

### Begin configuration variables

# path to custom apt.conf file
apt_config=
Helpify apt_config

# path to apt directory prefix (e.g. /usr)
apt_prefix=
Helpify apt_prefix

# Whether build environment should be deleted after
# each successful build or before each new build
lazy_cleanup=
Helpify lazy_cleanup
Helpify eager_cleanup lazy_cleanup

# RPM --excludedocs
exclude_docs=
Helpify excludedocs exclude_docs

# hasher-priv directory.
def_hasher_priv_dir=/usr/libexec/hasher-priv
hasher_priv_dir=
Helpify hasher_priv_dir def_hasher_priv_dir

# colon-separated list of languages to install
def_install_langs=all
install_langs=
Helpify install_langs def_install_langs

# comma-separated list of known mount points
known_mountpoints=
Helpify mountpoints known_mountpoints

# override default cache directory $workdir/cache
cache_dir=
Helpify cache_dir

# whether to use initroot cache
no_cache=
Helpify no_cache

# whether to use content indices
no_contents_indices=
Helpify no_contents_indices

# default sisyphus_check config
no_sisyphus_check=
Helpify no_sisyphus_check
no_sisyphus_check_in=
Helpify no_sisyphus_check_in
no_sisyphus_check_out=
Helpify no_sisyphus_check_out

# number of CPUs to use
nprocs=
Helpify nprocs

# subconfig identifier
number=
Helpify number

# override default packager <hasher@localhost>
packager=
Helpify packager

# whether repackage source along with binaries
repackage_source='--repackage-source'
Helpify repackage_source
Helpify no_repackage_source repackage_source

# build stage package file list
pkg_build_list='basesystem rpm-build>=0:4.0.4-alt21 kernel-headers-common>=0:1.1.4-alt1 sisyphus_check>=0:0.7.3 time'
build_list=
Helpify pkg_build_list

# initial stage package file list
pkg_init_list='setup filesystem rpm fakeroot>=0:0.7.3'
init_list=
Helpify pkg_init_list

# program to run to query for requirements instead of autogenerted script
prog_query_req=
Helpify query_req_prog prog_query_req

# program to run for rebuild instead of autogenerated script
prog_rebuild=
Helpify rebuild_prog prog_rebuild

# whether to repackage the source before query for requirements
query_repackage=
Helpify query_repackage

# noinstall package pattern list
noinstall_pattern_list='dev[-_][0-9]*'
Helpify pkg_noinstall_pattern_list noinstall_pattern_list

# repo directory.
def_repo=repo
repo=
Helpify repo def_repo

# repo-bin directory.
repo_bin=
Helpify repo_bin

# repo-src directory.
repo_src=
Helpify repo_src

# extra arguments for rpmbuild
rpmargs=
Helpify build_args rpmargs

# path to rpmi
def_rpmi=rpmi
rpmi=

# whether to save fakeroot state
save_fakeroot=
Helpify save_fakeroot

# target architecture.
def_target="$(rpm --showrc |sed -ne 's/^install arch[[:space:]]*:[[:space:]]*\([^[:space:]]\+\).*/\1/p')"
[ -n "$def_target" ] || def_target="$(uname -m)"
target=
Helpify target def_target

# Whether to use results of previous builds stored in
# $workdir/$repo during setup of new build environment
no_stuff=
Helpify with_stuff no_stuff
Helpify without_stuff no_stuff

# qemu architecture
qemu_arch=
Helpify qemu_arch

# various reasonable work limits.
wlimit_time_short=60
Helpify wlimit_time_short
wlimit_time_long=600
Helpify wlimit_time_long
wlimit_bytes_out=65536
Helpify wlimit_bytes_out

# working directory.
workdir="$HOME/hasher"
Helpify workdir

### End configuration variables

# source user config if any
hasher_config="$HOME/.hasher/config"
if [ -s "$hasher_config" ]; then
	. "$hasher_config"
fi

# APT workdir, $workdir/aptbox
aptbox=

# chroot workdir, $workdir/chroot
chroot=

# program to execute while entering chroot, $chroot/.host/entry
entry=

# variables used by hasher-priv
mountpoints=
wlimit_time_elapsed=
wlimit_time_idle=
wlimit_bytes_written=
use_pty=

opt_check_read()
{
	local value
	value="$(readlink -ev -- "$2")" &&
		[ -r "$value" ] ||
		Fatal "$1: $2: file not available."
	printf %s "$value"
}

opt_check_dir()
{
	local value
	value="$(readlink -ev -- "$2")" &&
		[ -d "$value" -a -x "$value" ] ||
		Fatal "$1: $2: directory not available."
	printf %s "$value"
}

opt_check_number()
{
	[ -n "${2##0*}" -a -n "${2##*[!0-9]*}" ] &&
		[ "$2" -gt 0 ] 2>/dev/null ||
		Fatal "$1: $2: invalid number."
	printf %s "$2"
}

parse_common_options()
{
	local prog
	prog="$1" && shift

	case "$1" in
		-h|--help) show_help
			;;
		-q|--quiet) quiet=-q
			;;
		-v|--verbose) verbose=-v
			;;
		-V|--version) print_version "$prog"
			;;
		*) Fatal "Unrecognized option: $1"
			;;
	esac
}

set_workdir()
{
	[ -n "$workdir" -a -z "${1:-}" ] ||
		workdir="${1:-}"
	cd "$workdir" || return 1
	workdir="$(/bin/pwd)" || return 1

	[ -n "$(printf %s "$workdir" |tr -d /.)" ] ||
		Fatal "$workdir: illegal working directory."
	if printf %s "$workdir" |LC_ALL=C grep -qs '[`"$\]'; then
		Fatal "$workdir: illegal symbols in pathname."
	fi
	[ -w . ] || Fatal "$workdir: unwritable working directory."

	Verbose "changed working directory to \`$workdir'"
	aptbox="$workdir/aptbox"
	chroot="$workdir/chroot"
	cache_dir="${cache_dir:-$workdir/cache}"
	entry="$chroot/.host/entry"
}

# assumes: initialized $chroot
install_static_helper()
{
	local name="$1"
	local src="$name.static"
	local dst="${2:-$name}"
	local path="$(type -p "$src")"
	[ -n "$path" -a -z "${path##/*}" ] ||
		Fatal "Static helper $src not found."
	install -p -m755 $verbose "$path" "$chroot/.host/$dst"
}

# assumes: defined aptbox
get_apt_config()
{
	local get_eval get_name get_value
	get_name="$1"
	shift || return 1
	get_value="$1"
	shift || return 1

	get_eval="$("$aptbox/apt-config" shell "$get_value" "$get_name")" || return 1
	eval "$get_eval"
}

# assumes: working get_apt_config()
read_apt_config()
{
	local name orig_name res_value dir_name value dir_value
	orig_name="$1"
	shift || return 1
	res_value="$1"
	shift || return 1

	name="$orig_name"
	get_apt_config "$name" value
	[ -n "$value" ] || Fatal "apt-config: undefined: $name"

	while [ -n "${value##/*}" ]; do
		dir_name="${name%::*}"
		[ "$dir_name" != "$name" ] || break
		get_apt_config "$dir_name" dir_value
		[ -n "$dir_value" ] || Fatal "apt-config: undefined: $dir_name"
		name="$dir_name"
		value="$dir_value$value"
	done

	eval "$res_value=\"$(quote_arg "$value")\""
}
	
hash=
rpm_verbose=

# set hash and rpm_verbose variables.
check_tty()
{
	[ -n "$verbose" ] && tty -s <&1 &&
		hash=-h ||
		hash=

	rpm_verbose="$verbose"
	if [ -z "$rpm_verbose" ]; then
		if [ -z "$quiet" ]; then
			rpm_verbose=-v
		fi
	fi
}

check_helpers()
{
	[ -d "${hasher_priv_dir:-$def_hasher_priv_dir}" ] ||
		Fatal "${hasher_priv_dir:-$def_hasher_priv_dir}: cannot access hasher-priv helper directory."

	getugid1="${hasher_priv_dir:-$def_hasher_priv_dir}/getugid1.sh"
	[ -x "$getugid1" ] ||
		Fatal "$getugid1: cannot access getugid1 helper."

	getugid2="${hasher_priv_dir:-$def_hasher_priv_dir}/getugid2.sh"
	[ -x "$getugid2" ] ||
		Fatal "$getugid2: cannot access getugid2 helper."

	chrootuid1="${hasher_priv_dir:-$def_hasher_priv_dir}/chrootuid1.sh"
	[ -x "$chrootuid1" ] ||
		Fatal "$chrootuid1: cannot access chrootuid1 helper."

	chrootuid2="${hasher_priv_dir:-$def_hasher_priv_dir}/chrootuid2.sh"
	[ -x "$chrootuid2" ] ||
		Fatal "$chrootuid2: cannot access chrootuid2 helper."

	makedev="${hasher_priv_dir:-$def_hasher_priv_dir}/makedev.sh"
	[ -x "$makedev" ] ||
		Fatal "$makedev: cannot access makedev helper."
}

# assumed: cwd == workdir
purge_chroot_in()
{
	find chroot/.in/ -mindepth 1 -depth -delete
}

# assumed: cwd == workdir
purge_chroot_out()
{
	find chroot/.out/ -mindepth 1 -depth -delete
}

# assumed: cwd == workdir
copy_chroot_incoming()
{
	purge_chroot_in
	install -p -m644 $verbose -- "$@" chroot/.in/ ||
		Fatal 'failed to copy files.'
}

# assumed: cwd == workdir
make_repo()
{
	if [ -n "$repo_src" -o -n "$repo_bin" ]; then
		mkdir -p $verbose -- ${repo_src:+"$repo_src"} ${repo_bin:+"$repo_bin"}
	fi
	mkdir -p $verbose -- ${repo:-$def_repo}/{{SRPMS,${target:-$def_target}/RPMS}.hasher,${target:-$def_target}/base}
}

# assumed: cwd == workdir, defined aptbox
update_repo()
{
	[ -z "$no_stuff" ] || return 0
	"$aptbox/genbasedir" --topdir=${repo:-$def_repo} --no-oldhashfile --bz2only --mapi ${target:-$def_target} hasher &&
		Verbose 'updated hasher repository indices.' ||
		Fatal 'failed to update hasher repository indices.'
}

# assumed: defined variables: hash
update_RPM_database()
{
	rpmi -i $verbose $hash --dbpath "$aptbox/var/lib/rpm" $exclude_docs --ignorearch --ignoresize --noorder --noscripts --notriggers --justdb "$@"
		Verbose 'RPM database updated.' ||
		Fatal 'RPM database update failed.'
}

# execute as pseudoroot.
chrootuid1()
{
	[ $# -gt 0 ] || set -- /.host/entry
	local rc=0
	"$chrootuid1" ${number:+-$number} "$chroot" "$@" || rc=$?
	wlimit_time_elapsed= wlimit_time_idle= wlimit_bytes_written=
	return $rc
}

# execute as builder.
chrootuid2()
{
	[ $# -gt 0 ] || set -- /.host/entry
	local rc=0
	"$chrootuid2" ${number:+-$number} "$chroot" "$@" || rc=$?
	wlimit_time_elapsed= wlimit_time_idle= wlimit_bytes_written=
	return $rc
}

create_entry_header()
{
	cat >"$entry" <<__EOF__
#!/bin/sh -e
TMPDIR="\$HOME/tmp"
export TMPDIR
cd /.in
__EOF__
	chmod 755 "$entry"
}

create_entry_fakeroot_header()
{
	cat >"$entry" <<__EOF__
#!/bin/sh -e
if [ -z "\$FAKEROOTKEY" -a "\$USER" = root -a -x /usr/bin/fakeroot ]; then
	if [ -f '/.fakedata' ]; then
		exec /usr/bin/fakeroot -i /.fakedata -s /.fakedata "\$0" "\$@"
	else
		exec /usr/bin/fakeroot "\$0" "\$@"
	fi
fi
cd /.in
__EOF__
	chmod 755 "$entry"
}

lock_file=
hasher_exit_handler()
{
	local rc=$?
	trap - EXIT
	[ -z "$lock_file" ] ||
		rm -f $verbose -- "$lock_file"
	exit $rc
}

create_lock_file()
{
	local dest="$1"; shift
	local item="$1"; shift
	local silent="${1:-}"; shift ||:
	local temp
	temp="$(mktemp -- "$dest.XXXXXXXX")" ||
		Fatal "Unable to lock $item."
	if ! echo $$ >"$temp"; then
		rm -f $verbose -- "$temp"
		Fatal "Unable to lock $item."
	fi

	if ln -- "$temp" "$dest" 2>/dev/null; then
		rm -f -- "$temp"
		Verbose "Locked $item."
		return 0
	fi

	rm -f -- "$temp"
	local pid
	if pid="$(head -c32 -- "$dest")" &&
	   kill -0 -- "$pid" 2>/dev/null; then
		Info "$item is locked, pid=$pid."
		ps hp "$pid" >&2
		return 2
	fi

	[ -n "$silent" ] ||
		Info "$item is locked, stale pid=$pid."
	return 1
}

lock_workdir()
{
	[ -z "$lock_file" ] || return 0

	local lockdir="$workdir/lockdir"
	mkdir -p $verbose "$lockdir" ||
		Fatal 'Unable to lock working directory.'
	lock_file="$lockdir/lockfile"

	if create_lock_file "$lock_file" "working directory \`$workdir'"; then
		trap hasher_exit_handler HUP PIPE INT QUIT TERM EXIT
		return 0
	elif [ $? -eq 1 ]; then
		# stale lock detected
		local unlock_file="$lockdir/unlockfile"
		create_lock_file "$unlock_file" "stale lock \`$lock_file'" ||
			Fatal 'Unable to remove stale lock.'

		if create_lock_file "$lock_file" "working directory \`$workdir'" silent; then
			# stale lock disappeared
			trap hasher_exit_handler HUP PIPE INT QUIT TERM EXIT
			rm -f -- "$unlock_file"
			return 0
		elif [ $? -eq 2 ]; then
			# actual lock detected
			rm -f -- "$unlock_file"
			Fatal 'Unable to lock working directory.'
		elif [ $? -eq 1 ] &&
		     rm -f $verbose -- "$lock_file"; then
			# stale lock removed
			if create_lock_file "$lock_file" "working directory \`$workdir'"; then
				trap hasher_exit_handler HUP PIPE INT QUIT TERM EXIT
				rm -f -- "$unlock_file"
				return 0
			fi
			rm -f -- "$unlock_file"
			Fatal 'Unable to lock working directory.'
		fi
		# error removing stale lock
		rm -f -- "$unlock_file"
		Fatal 'Unable to remove stale lock.'
	fi
	# actual lock detected
	Fatal 'Unable to lock working directory.'
}

# assumed: defined aptbox
APT_Get_PrintLocalFile=
print_uris()
{
	local out
	if ! out="$("$aptbox/apt-get" -q -y --print-uris install -- "$@" 2>&1)"; then
		printf %s\\n "$out" >&2
		Fatal 'failed to calculate package file list.'
	fi

	local all_pattern="'\\([a-z]\\+\\):\\([^']\\+\\)' .*"
	local local_pattern="'\\(file\\|copy\\):\\([^']\\+\\)' .*"

	local all_uris
	if ! all_uris="$(printf %s "$out" |sed -ne "s/^$all_pattern/\\2/pg")"; then
		printf %s\\n "$out" >&2
		Fatal 'failed to filter package file list.'
	fi

	local local_uris cached_uris=
	if ! local_uris="$(printf %s "$out" |sed -ne "s/^$local_pattern/\\2/pg")"; then
		printf %s\\n "$out" >&2
		Fatal 'failed to filter package file list.'
	fi

	if [ "$all_uris" != "$local_uris" ]; then
		if [ -z "$APT_Get_PrintLocalFile" ]; then
			if ! out="$("$aptbox/apt-get" -o APT::Get::PrintLocalFile=yes -q -y --print-uris install -- "$@" 2>&1)"; then
				printf %s\\n "$out" >&2
				Fatal 'calculated package file list is not local and apt-get does not support APT::Get::PrintLocalFile option.'
			fi

			if ! cached_uris="$(printf %s "$out" |sed -ne "s/^$all_pattern/\\2/pg")" ||
			   [ -n "$cached_uris" ]; then
				printf %s\\n "$out" >&2
				Fatal 'calculated package file list is not local and apt-get does not support APT::Get::PrintLocalFile option.'
			fi

			APT_Get_PrintLocalFile=1
		fi

		if [ -n "$verbose" ]; then
			"$aptbox/apt-get" -y -d install -- "$@" >&2 ||
				Fatal 'failed to download packages from calculated package file list.'
		else
			if ! out="$("$aptbox/apt-get" -y -d install -- "$@" 2>&1)"; then
				printf %s\\n "$out" >&2
				Fatal 'failed to download packages from calculated package file list.'
			fi
		fi

		if ! out="$("$aptbox/apt-get" -o APT::Get::PrintLocalFile=yes -q -y --print-uris install -- "$@" 2>&1)"; then
			printf %s\\n "$out" >&2
			Fatal 'failed to calculate local package file list.'
		fi

		if ! cached_uris="$(printf %s "$out" |LC_ALL=C grep ^/)" || [ -z "$cached_uris" ]; then
			printf %s\\n "$out" >&2
			Fatal 'failed to calculate local package file list.'
		fi
	fi

	if [ -n "$cached_uris" ]; then
		[ -n "$local_uris" ] && local_uris="$local_uris
$cached_uris" || local_uris="$cached_uris"
	fi

	printf %s "$local_uris" &&
		Verbose 'calculated package file list.'
}

parse_xauth_entry()
{
	XAUTH_DISPLAY="$1" && shift ||:
	XAUTH_PROTO="$1" && shift ||:
	XAUTH_KEY="$1" && shift ||:

	if [ -z "$XAUTH_DISPLAY" -o -n "${XAUTH_DISPLAY##*:*}" \
	  -o -z "$XAUTH_PROTO" -o -z "$XAUTH_KEY" ]; then
		Info "Invalid auth entry for DISPLAY \`$DISPLAY', disabling X11 forwarding."
		return 1
	fi

	# Workaround for broken xauth entries.
	XAUTH_DISPLAY="$DISPLAY"
}

prepare_x11_forwarding()
{
	[ -n "$x11_forwarding" ] || return 0

	if [ -z "$DISPLAY" ]; then
		Info 'DISPLAY not set, disabling X11 forwarding.'
		return 1
	fi

	if [ -n "${DISPLAY##*:*}" ]; then
		Info 'Invalid DISPLAY set, disabling X11 forwarding.'
		return 1
	fi

	local xauth=xauth xentry
	if [ "$x11_forwarding" = trusted ]; then
		local display
		[ -n "${DISPLAY##localhost:*}" ] &&
			display="$DISPLAY" ||
			display="unix:${DISPLAY#localhost:}"
		xentry="$(xauth list "$display")" ||
			return 1
	elif [ "$x11_forwarding" = untrusted ]; then
		local xfile="$workdir/lockdir/xauth"
		touch "$xfile" &&
		$xauth -f "$xfile" generate "$DISPLAY" . untrusted timeout "$x11_timeout" ||
			return 1
		xentry="$(xauth -f "$xfile" list "$DISPLAY")" ||
			return 1
	else
		Fatal 'Invalid $x11_forwarding value.'
	fi

	parse_xauth_entry $xentry || return 1

	export XAUTH_DISPLAY XAUTH_PROTO XAUTH_KEY
}

# quote argument for /.host/entry.
quote_arg()
{
	local out="$*"
	if [ -z "${out##*[\"\$\`\\]*}" ]; then
		out="$(printf %s "$out" |sed -e 's/["$`\]/\\&/g')" || return 1
	fi
	printf %s "$out"
}

