#!/bin/bash
# make kpatch'es for the RPM-based packages using spec file.
# Each package contains the config file: info in their project directory,
# like /var/libcareplus/qemu/info
#

echo '+ set -x'
set -x
if [ -z "$SOURCING" ]; then
	set -e
fi

die() {
	echo "$@"
	if test -f "/etc/kcbuildsigstop"; then
		kill -SIGSTOP $$
	fi
	exit 1
}

usage() {
	echo "Makes kpatch'es for the RPM-based packages using spec file."
	echo "Usage: libcare-pkgbuild [--prebuild] [--test] [--help] DIR"
	echo "    -p|--prebuild     prebuild project for further use"
	echo "    -t|--test         run unit and stress tests"
	echo "    -h|--help         print this message"
	echo "    DIR               directory with project's info file and other resources"
}

prepare() {
	# Parse cmdline args
	ACTION=build
	PDIR=
	while [ "$1" != "" ]; do
		case $1 in
		-p|--prebuild)
			ACTION=prebuild
			;;
		-t|--test)
			ACTION=test
			;;
		-h|--help)
			usage
			exit 0
			;;
		*)
			if [ -n "$PDIR" ] || [[ $1 == -* ]]; then
				echo "Unknown option $1"
				usage
				exit 1
			fi
			PDIR=$(cd "$(dirname "$1")" && pwd)/$(basename "$1")
			;;
		esac
		shift
	done

	KPATCH_PATH="$(dirname "$( which libcare-cc )" )"
	export KPATCH_PATH
	export OLDPATH=$PATH
	export KPATCH_PASSTHROUGH_ASM=1
	CPUS=`cat /proc/cpuinfo | grep ^processor | wc -l`
	export PARALLEL=-j$[CPUS*2]
	export RPM_BUILD_NCPUS=$[CPUS*2]

	# Obtain information about the project
	source $PDIR/info

	ROOT_ORIGINAL=$PDIR/root.original
	ROOT_PATCHED=$PDIR/root.patched
}

clean_dirs() {
	echo "  cleaning up"
	rm -rf $KP_PROJECT_BUILD_ROOT $ROOT_ORIGINAL $ROOT_PATCHED
}

kp_prepare_env_hook() {
	# use this to add repos
	:
}

kp_pack_prebuilt() {
	echo "  packing prebuilt $KP_PROJECT into $KP_PROJECT_PREBUILT"
	pushd $KP_PROJECT_BUILD_ROOT
		tar -zcf $PDIR/$KP_PROJECT_PREBUILT		\
			$KP_PROJECT_BUILD_ROOT			\
			$ROOT_ORIGINAL
	popd
}

kp_unpack_prebuilt() {
	echo "  unpacking prebuilt $KP_PROJECT into $KP_PROJECT_PREBUILT"
	tar -xf $PDIR/$KP_PROJECT_PREBUILT -C /
}

kp_prepare_source_raw() {
	mkdir -p $KP_PROJECT_BUILD_ROOT
	pushd $KP_PROJECT_BUILD_ROOT
		tar -xf $PDIR/$KP_PROJECT_SOURCE
		mkdir -p $KP_PROJECT_BUILD_DIR
	popd
}

kp_download_source_rpm() {
	if test -n "$KP_PROJECT_SOURCE_URL"; then
		curl $KP_PROJECT_SOURCE_URL -o $KP_PROJECT_ORIG_RPMS/$KP_PROJECT_SOURCE
	else
		yumdownloader --source --destdir $KP_PROJECT_ORIG_RPMS ${KP_PROJECT_SOURCE%.src.rpm}
	fi
}

kp_prepare_source_rpm() {
	rm -rf $PDIR/deps
	eval yum-builddep -d 1 -y $KP_RPM_REPOS				\
			--downloadonly --downloaddir=$PDIR/deps		\
			$KP_PROJECT_ORIG_RPMS/$KP_PROJECT_SOURCE
	mkdir -p $KP_PROJECT_BUILD_ROOT
	rpm -qa > $KP_PROJECT_BUILD_ROOT/all-packages.txt
	ls $PDIR/deps > $KP_PROJECT_BUILD_ROOT/dependencies.txt
	eval yum-builddep -d 1 -y $KP_RPM_REPOS			\
			$KP_PROJECT_ORIG_RPMS/$KP_PROJECT_SOURCE

	sed -i 's/.rpm$//g' $KP_PROJECT_BUILD_ROOT/dependencies.txt

	rpm -ivh $KP_PROJECT_ORIG_RPMS/$KP_PROJECT_SOURCE \
		--define "_topdir $KP_PROJECT_BUILD_ROOT"
}

kp_prepare_source() {
	if ! test -f $KP_PROJECT_ORIG_RPMS/$KP_PROJECT_SOURCE; then
		echo "  downloading source for $KP_PROJECT"
		kp_download_source_$KP_PROJECT_FORMAT
	fi
	echo "  preparing source for $KP_PROJECT"
	kp_prepare_source_$KP_PROJECT_FORMAT
}

patch_list_apply() {
	SRC_DIR=$(cd "$(dirname "$1")" && pwd)/$(basename "$1")
	PATCH_DIR=$(cd "$(dirname "$KP_PROJECT_PLIST_FILE")" && pwd)
	PLIST=$PATCH_DIR/$(basename "$KP_PROJECT_PLIST_FILE")
	PATCH_ID=$(echo $(basename "$KP_PROJECT_PLIST_FILE") | awk -F. '{print $1}')
	if test -z "$PATCH_ID"; then
		echo "Failed to get patch-id. Please check plist filename: $KP_PROJECT_PLIST_FILE"
		exit 1
	fi
	TEMP_PLIST=/tmp/build.kpatch/tmpplist

	if [ ! -f $PLIST ]; then
		echo "File $PLIST not found"
		exit 1;
	fi

	echo "patching $PWD with patches from $PLIST"

	pushd $SRC_DIR # go to the directory with sources to be patched

	#in case we don't have a newline in plist
	cat $PLIST > $TEMP_PLIST
	echo -e "\n" >> $TEMP_PLIST

	# iterate through patches in PLIST
	while read NAME
	do
		COMMENT=`echo $NAME | cut -c1`
		if [ "$COMMENT" == "#" ]; then
			continue;
		fi

		if [ -z "${NAME}" ]; then
			continue;
		fi

		echo "Applying patch $NAME"
		patch -p1 -u --fuzz=0 --batch < $PATCH_DIR/$NAME
		if [ $? != 0 ]; then
			echo "Failed applying patch $NAME"; popd; rm $TEMP_PLIST; exit 1
		else
			echo "Successfully applied patch $NAME"
		fi
	done < $TEMP_PLIST
	rm $TEMP_PLIST

	popd
}

kp_patch_source() {
	echo "  patching project"
	#patch_list_apply requires this dir
	mkdir -p /tmp/build.kpatch
	patch_list_apply $KP_PROJECT_DIR
}

kp_prebuild_rpm() {
	export KPATCH_STAGE=original

	eval yum-builddep -d 1 $KP_RPM_REPOS \
		-y $KP_PROJECT_BUILD_ROOT/SPECS/$KP_PROJECT_SPEC

	eval rpmbuild --nocheck --noclean 				\
		-bc 							\
		$KP_RPMBUILD_FLAGS					\
		'--define "_topdir $KP_PROJECT_BUILD_ROOT"'		\
		$KP_PROJECT_BUILD_ROOT/SPECS/$KP_PROJECT_SPEC 2>&1 |	\
		tee $KP_PROJECT_BUILD_ROOT/prebuild.log
}

_kp_install_orig_rpm() {
	rpm	--force -i $1 \
		--root=$ROOT_ORIGINAL \
		--nodeps --noscripts
}

kp_install_orig_rpm() {
	for orig_rpm in $(ls $KP_PROJECT_ORIG_RPMS | grep -v $KP_PROJECT_SOURCE); do
		_kp_install_orig_rpm $KP_PROJECT_ORIG_RPMS/$orig_rpm
	done
}


kp_prebuild() {
	echo "  prebuilding $KP_PROJECT"
	kp_prebuild_$KP_PROJECT_FORMAT
	kp_install_orig_$KP_PROJECT_FORMAT
}

de_offset_syms() {
	local binary=$1

	readelf -WSs $binary > $binary.symlist 2>/dev/null
	de-offset-syms.awk $binary.symlist > $binary.symlist.tmp
	sort $binary.symlist.tmp > $binary.symlist
	rm -f $binary.symlist.tmp
}

kp_sanity_check() {
	pushd $ROOT_PATCHED
		local targets="$(find . -perm /0111 -type f)"
	popd

	local failed=""
	for target in $targets; do
		local original=`ls $ROOT_ORIGINAL/usr/lib/debug/${target}*.debug`
		local patched="$ROOT_PATCHED/$target"
		local alloweddiff="$PDIR/$(basename "$target").symlist.diff"

		de_offset_syms $original
		if test ! -s $original.symlist; then
			original="$ROOT_ORIGINAL/$target"
			de_offset_syms $original
		fi

		de_offset_syms $patched

		if ! test -f "$alloweddiff"; then
			if ! diff -qu $original.symlist $patched.symlist >/dev/null; then
				failed="$failed $original.symlist vs $patched.symlist"
			fi
		else
			local symlistdiff=$(mktemp --tmpdir)
			diff -u $original.symlist $patched.symlist > $symlistdiff || :
			sed -i '1,2d' $symlistdiff
			if ! cmp $symlistdiff $alloweddiff; then
				failed="$failed $original.symlist vs $patched.symlist"
			else
				warning="$warning $original.symlist vs $patched.symlist"
			fi
		fi
	done

	if test -n "$failed"; then
		if test "$KP_SANITY_CHECK_STRICTLY" == "yes"; then
			die "Failed sanity check for $failed"
		else
			echo "[Warning] Failed sanity check for $failed"
		fi
	fi
}

kp_build_rpm() {
	eval rpmbuild --nocheck --noclean			\
		--short-circuit					\
		-bc						\
		$KP_RPMBUILD_FLAGS				\
		'--define "_topdir $KP_PROJECT_BUILD_ROOT"'	\
		$KP_PROJECT_BUILD_ROOT/SPECS/$KP_PROJECT_SPEC 2>&1 |
		tee $KP_PROJECT_BUILD_ROOT/build.log
}

kp_install_files() {
	local src="$1"
	local dest="$2"
	local direction="$3"
	local files="$4"

	eval set -- $files
	while test -n "$1"; do
		local buildpath="$1"
		local installpath="$2"
		shift 2

		if test "$installpath" = "IGNORE"; then
			continue
		fi

		if test $direction = "from_prebuild"; then
			install -D $src/$buildpath $dest/$installpath
		else
			install -D $src/$installpath $dest/$buildpath
		fi
	done
}

kp_check_missing_files() {
	local builddir="$1"

	local failed=
	pushd $KP_PROJECT_BUILD_DIR
		set -- $(find . -perm /0111 -type f)

		for local_patched; do
			local_patched="${local_patched#./}"
			if ! eu-readelf -S $local_patched 2>/dev/null | grep -q '.kpatch'; then
				continue
			fi

			# $local_patched can't be last in the list since it is
			# src path.
			if test "${KP_INSTALL_FILES#*/$local_patched }" = \
							"$KP_INSTALL_FILES"; then
				failed="/$local_patched $failed"
			fi
		done

	popd

	if test -n "$failed"; then
		die "Files $failed patched but are not listed, aborting"
	fi
}

kp_install_generic() {
	local root_patched="$ROOT_PATCHED"

	kp_install_files $KP_PROJECT_BUILD_DIR \
		$root_patched \
		"from_prebuild" \
		"$KP_INSTALL_FILES"
	kp_check_missing_files $KP_PROJECT_BUILD_DIR
}

kp_install_rpm() {
	kp_install_generic
}


kp_build() {
	echo "  building $KP_PROJECT"

	export KPATCH_STAGE=patched
	# This option tells ld to keep relocations
	export KPCC_APPEND_ARGS="-Wl,-q"

	kp_build_$KP_PROJECT_FORMAT
	kp_install_$KP_PROJECT_FORMAT
}

kp_gen_kpatch() {
	echo "  generating kpatches"

	pushd $ROOT_PATCHED
		targets=$(find . -perm /0111 -type f)
	popd

	rm -rf $PDIR/${KP_PROJECT_PATCH%.*}
	mkdir $PDIR/${KP_PROJECT_PATCH%.*}

	local no_patches=1

	for t in $targets; do
		local debug=`ls $ROOT_ORIGINAL/usr/lib/debug/${t}*.debug`
		local patched="$ROOT_PATCHED/$t"
		local buildid=$(eu-readelf -n $debug | sed -n '/Build ID:/ { s/.* //; p }')
		if test -n "$KP_PROJECT_BUILD_ID"; then
			buildid=$KP_PROJECT_BUILD_ID
		fi
		if test -z "$buildid"; then
			continue
		fi

		if ! eu-readelf -S $patched | grep -q '.kpatch'; then
			continue
		fi

		chmod u+w $debug $patched

		eu-unstrip "$ROOT_ORIGINAL/$t" "$debug"

		$KPATCH_PATH/kpatch_strip --strip $patched $patched.kpstripped
		cp $patched.kpstripped $patched.relfixup
		$KPATCH_PATH/kpatch_strip --rel-fixup $debug $patched.relfixup
		cp $patched.relfixup $patched.stripped
		/usr/bin/strip --strip-unneeded $patched.stripped
		cp $patched.stripped $patched.undolink
		$KPATCH_PATH/kpatch_strip --undo-link $debug $patched.undolink
		$KPATCH_PATH/kpatch_make -b "$buildid" -i "$PATCH_ID" $patched.undolink -o $patched.kpatch
		cp $patched.kpatch $PDIR/${KP_PROJECT_PATCH%.*}/$buildid.kpatch
		no_patches=0
	done

	if test $no_patches -gt 0; then
		die "No binary patches found. Are your source patches correct?"
	fi
}

kp_pack_patch() {
	echo "  packing patch for $KP_PROJECT into $KP_PROJECT_PATCH"
	tar -zcf $PDIR/$KP_PROJECT_PATCH $PDIR/${KP_PROJECT_PATCH%.*}
}

kp_unpack_patch() {
	local tmpdir=$(mktemp -d --tmpdir)

	echo "  unpacking patches for $KP_PROJECT into $KP_PROJECT_BUILD_ROOT/storage"

	tar -xf /kcdata/$KP_PROJECT_PATCH -C $tmpdir

	find $tmpdir -name \*.kpatch > $tmpdir/patchlist

	while read patchfile; do
		local patchname=${patchfile##*/}
		local buildid=${patchname%.kpatch}

		local KP_STORAGE=$KP_PROJECT_BUILD_ROOT/storage/$buildid

		mkdir -p $KP_STORAGE/1
		cp $patchfile $KP_STORAGE/1/kpatch.bin
		ln -rs $KP_STORAGE/1 $KP_STORAGE/latest
	done < $tmpdir/patchlist

	rm -fr $tmpdir
}
overwrite_utils() {
	TMPBIN=$(mktemp -d --tmpdir)

	mkdir $TMPBIN/bin
	ln -fs $KPATCH_PATH/libcare-cc $TMPBIN/gcc
	ln -fs $KPATCH_PATH/libcare-cc $TMPBIN/cc
	ln -fs $KPATCH_PATH/libcare-cc $TMPBIN/g++
	ln -fs $KPATCH_PATH/libcare-cc $TMPBIN/c++
	if ! test -x /usr/bin/g++; then
		rm -f $TMPBIN/g++
	fi
	if ! test -x /usr/bin/c++; then
		rm -f $TMPBIN/c++
	fi
	if ! test -x /usr/bin/gcc; then
		rm -f $TMPBIN/gcc
	fi

	export PATH=$TMPBIN:$PATH

	if test "$(command -v cc)" != "$TMPBIN/cc"; then
		echo "Can't install our wrappers, missing?"
		exit 1
	fi
}

kp_patch_test() {
	echo "Empty kp_patch_test called, override it!"
	exit 1
}

main() {
	echo "Starting at "`date +%T`"..."

	prepare "$@"

	clean_dirs

	overwrite_utils

	kp_prepare_env_hook

	if test "$ACTION" == "prebuild"; then
		kp_prepare_source
		kp_prebuild_hook
		kp_prebuild
		kp_pack_prebuilt
	elif test "$ACTION" == "build"; then
		kp_unpack_prebuilt
		kp_patch_source
		kp_build_hook
		kp_build
		kp_sanity_check
		kp_gen_kpatch
		kp_pack_patch
	elif test "$ACTION" == "test"; then
		kp_unpack_prebuilt
		kp_prepare_test_binaries
		kp_unpack_patch
		#kp_patch_source
		kp_patch_test
	fi

	#clean_dirs

	echo "Finished at "`date +%T`"..."
}

if [ -z "$SOURCING" ]; then
	main "$@"
fi
