#!/bin/sh -efu
#
# Copyright (C) 2005  Ivan Zakharyaschev <imz@altlinux.org>
# Copyright (C) 2005-2006  Konstantin A. Lepikhov <lakostis@altlinux.org>
# Copyright (C) 2003-2004  Anton Farygin <rider@altlinux.org>
# Copyright (C) 2003-2008  Sergey Vlasov <vsu@altlinux.org>
#
# Build the kernel-modules-* packages from the git repository.
#
# 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.
#

TOP="$(readlink -ev .)"

. kernel-build-sh-functions

# quiet_if_ok <command> <args>...
#
# Hides stdout and stderr output of the command if it returns 0, otherwise
# prints all stored output to stderr.
#
quiet_if_ok()
{
	local output rc=0

	output="$( "$@" 2>&1 )" || rc=$?
	[ $rc = 0 ] || printf '%s\n' "$output" 1>&2
	return $rc
}

branch_exists()
{
	git rev-parse --verify "refs/heads/$1" >/dev/null 2>&1
}

# get_package_info <commit>
#
# Use "gear --describe" to get package name/version/release and place the
# result in global variables: $pkg_name, $pkg_version, $pkg_release.
#
get_package_info()
{
	local commit="$1" && shift

	local output
	output="$(gear --describe -t "$commit")" ||
		fatal "'gear --describe -t $commit' failed"
	[ -n "$output" ] ||
		fatal "'gear --describe -t $commit' gave empty output"
	[ -z "${output##* * *}" ] && [ -n "${output##* * * *}" ] ||
		fatal "'gear --describe -t $commit' gave invalid output"
	[ -n "${output##*%*}" ] ||
		fatal "'gear --describe -t $commit' did not expand macros"

	pkg_name="${output%% *}"
	output="${output#* }"
	pkg_version="${output%% *}"
	pkg_release="${output##* }"
}

# make_git_workdir <new_workdir>
#
# Create a new git working directory which shares most of .git/ internal files
# with an existing git repository.  In particular, .git/refs/ is shared,
# therefore commits made in $new_workdir propagate to $git_dir.
#
make_git_workdir()
{
	local new_workdir="$1" && shift

	local git_dir x
	git_dir="$(git rev-parse --git-dir)" ||
		fatal "Failed to find a valid git directory"
	git_dir="$(readlink -ev "$git_dir")" ||
		fatal "Invalid git directory \"$git_dir\""
	mkdir -p "$new_workdir/.git/logs"
	for x in config refs logs/refs objects info hooks packed-refs remotes rr-cache; do
		ln -s "$git_dir/$x" "$new_workdir/.git/$x"
	done
	cp "$git_dir/HEAD" "$new_workdir/.git/HEAD"
}

# update_module_branch <branch> <template>
#
# Create a new commit in <branch> based on the previous commit in that branch
# and the template in <template>.  The new commit may be a merge commit if
# <template> has been updated after the previous update of <branch>, or a
# simple commit if <template> was not updated.
#
# If <branch> did not exist yet, it will be created based on <template>.
#
update_module_branch()
{
	local branch="$1" && shift
	local template="$1" && shift

	local old_desc new_desc old_head= new_head

	rm -rf "$workdir/module_tree"
	make_git_workdir "$workdir/module_tree"
	cd "$workdir/module_tree"
	unset GIT_DIR
	if [ -n "$opt_pull" ]; then
		module=$(echo $branch | cut -d / -f1 )
		git fetch git://git.altlinux.org/gears/k/$module +$opt_distribution:$branch ||:
	fi
	if branch_exists "$branch"; then
		git checkout -f -q "$branch^{commit}}"
		old_head="$(git rev-parse --verify HEAD)"
	else
		git checkout -f -q "$template^{commit}"
	fi
	old_desc="$(gear --describe)"
	quiet_if_ok git merge --no-summary --no-commit -s ours "$template"
	git read-tree --reset -u "$template"
	( set +f; subst_module_spec *.spec ) || return
	if [ -n "$(git diff --raw HEAD)" ]; then
		GIT_EDITOR=: gear-commit -a -q
		new_desc="$(gear --describe)"
		[ "$old_desc" != "$new_desc" ] || [ -n "$opt_force" ] ||
			fatal "Package \"$old_desc\" already committed"
	else
		new_desc="$old_desc"
	fi
	if [ -n "$opt_tag" ]; then
		gear-create-tag ${opt_force:+-f} -v \
			-n "@name@-@version@-@release@" >/dev/null 2>&1
		gear --describe | tr ' ' '-' >>"$rpm_outdir/taglist"
	fi
	if [ -n "$opt_commit" ]; then
		new_head="$(git rev-parse --verify HEAD)"
		[ -n "$old_head" ] ||
			git branch "$branch" "$template"
		git update-ref -m "commit package: $new_desc" \
			"refs/heads/$branch" "$new_head" $old_head
	fi
}

build_module_local()
{
	local branch="$1" && shift

	eval "gear --rpmbuild -t $(
		shell_quote_args "$branch"
	) -- $RPMBUILD $(
		shell_quote_args \
			--dbpath "$rpm_rootdir/var/lib/rpm" \
			--define "_includedir $rpm_rootdir/usr/include" \
			--define "_usrsrc $rpm_rootdir/usr/src" \
			-ba
	)" ||
		return $?
}

build_module_hasher()
{
	local branch="$1" && shift

	eval "setarch "$rpm_target" gear --hasher -t $(
		shell_quote_args "$branch"
	) -- hsh $hsh_options $(
		shell_quote_args --target "$rpm_target" "$hsh_workdir"
	)" ||
		return $?
}

build_module()
{
	local module="$1" && shift

	local branch="kernel-modules-$module-$KERNEL_FLAVOUR/$opt_distribution"
	local template="template/$module/$opt_distribution"

	if [ -z "$opt_force" ]; then
		local pkg_name pkg_version pkg_release package real
		local need_update=
		get_package_info "$template"
		pkg_name="${pkg_name/@kflavour@/$KERNEL_FLAVOUR}"
		pkg_release="$pkg_release.$KERNEL_CODE.$KERNEL_BUILDRELEASE"
		package="$pkg_name-$pkg_version-$pkg_release.$rpm_target.rpm"
		local rpmdir
		if [ -n "$opt_hasher" ]; then
			local hsh_repo=$(hasher_config_param 'def_repo')
			[ -n "$hsh_repo" ] || hsh_repo="$hsh_workdir/repo"
			rpmdir="$hsh_repo/$rpm_target/RPMS.hasher"
		else
			rpmdir="$rpm_rpmdir"
		fi
		real="$(find "$rpmdir" -type f -name "$package")"
		if [ -z "$real" ]; then
			need_update=t
		else
			# FIXME: compare build time with commit time?
			# Use '--force' to rebuild existing packages for now.
			:
		fi

		if [ -z "$need_update" ]; then
			return 42
		fi
	fi

	local builder
	if [ -n "$opt_no_build" ]; then
		builder=:
	elif [ -n "$opt_hasher" ]; then
		builder=build_module_hasher
	else
		builder=build_module_local
	fi

	update_module_branch "$branch" "$template"
	$builder HEAD
}

print_version()
{
	cat <<EOF
$PROG version $PROG_VERSION

Copyright (C) 2005  Ivan Zakharyaschev <imz@altlinux.org>
Copyright (C) 2005-2006  Konstantin A. Lepikhov <lakostis@altlinux.org>
Copyright (C) 2003-2004  Anton Farygin <rider@altlinux.org>
Copyright (C) 2003-2008  Sergey Vlasov <vsu@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_help()
{
	cat <<EOF
$PROG - build kernel-modules-* packages.

Usage: $PROG [options] <flavour> [<module>...]
   or: $PROG [options] -k <flavour> [-k <flavour>...] [<module>...]

Options:
  -d, --distribution=NAME     distribution branch name (alt-linux-X.Y);
  -k, --kernel=FLAVOUR[=V-R]  kernel flavour and version-release;
  -f, --force                 rebuild packages even if already built;
  --commit                    commit to the package branch;
  --tag                       create tag for release (implies --commit);
  -p --pull                   pull from git.altlinux.org/gears;
  --no-build                  do not build packages (requires --commit/--tag);
  --hasher                    use hasher to build packages(default);
  --rpmbuild		      use rpm to build package
  --hsh-options=OPTIONS       pass additional options to hsh;
  --hsh-workdir=DIR           specify another working directory for hsh;
  --no-install                do not install packages into fake root;
  --rpmbuild-options=OPTIONS  pass additional options to rpmbuild;
  --target=ARCH               specify target architecture;
  -V, --version               print program version and exit;
  -h, --help                  show this text and exit.

EOF
	exit
}

TEMP=`getopt -n "$PROG" -o d:,k:,f,p,V,h -l distribution:,kernel:,force,commit,tag,no-build,hasher,hsh-options:,hsh-workdir:,no-install,rpmbuild,rpmbuild-options:,target:,pull,version,help -- "$@"` ||
	show_usage
eval set -- "$TEMP"

opt_distribution=sisyphus
opt_kernels=
opt_force=
opt_commit=
opt_pull=
opt_tag=
opt_no_build=
opt_hasher=t
opt_install=t
while :; do
	case "$1" in
		--) shift; break
			;;
		-d|--distribution) shift; opt_distribution="$1"
			;;
		-k|--kernel) shift; opt_kernels="$opt_kernels $1"
			;;
		-f|--force) opt_force=t
			;;
		-p|--pull) opt_pull=t
			;;
		--commit) opt_commit=t
			;;
		--tag) opt_tag=t
			;;
		--no-build) opt_no_build=t
			;;
		--hasher) opt_hasher=t
			;;
		--rpmbuild) opt_hasher=""
			;;
		--hsh-options) shift; hsh_options="$1"
			;;
		--hsh-workdir) shift; hsh_workdir="$1"
			;;
		--no-install) opt_install=
			;;
		--rpmbuild-options) shift; rpmbuild_options="$1"
			;;
		--target) shift; rpm_target="$1"
			;;
		-V|--version) print_version
			;;
		-h|--help) show_help
			;;
		*) fatal "Unrecognized option: $1"
			;;
	esac
	shift
done

[ -z "$opt_tag" ] || opt_commit=t
[ -z "$opt_no_build" ] || [ -n "$opt_commit" ] ||
	fatal "--no-build must be used with --commit or --tag"
[ -z "$opt_no_build" ] || [ -z "$opt_hasher" ] ||
	fatal "--no-build cannot be used together with --hasher"

finish_setup

if [ -z "$opt_kernels" ]; then
	[ $# -ge 1 ] || show_usage "Not enough arguments"
	opt_kernels="$1" && shift
fi

# Determine kernel versions to use
kernels=
for flavour in $opt_kernels; do
	if [ -z "${flavour##*=*}" ]; then
		[ -n "${flavour##*=*=*}" ] ||
			fatal "Invalid kernel flavour \"$flavour\""
		[ -z "${flavour##*=*-*}" ] ||
			fatal "Invalid kernel version-release in \"$flavour\""
		kernels="$kernels $flavour"
		continue
	fi

	kernel_pkg="kernel-headers-modules-$flavour"
	if [ -n "$opt_hasher" ]; then
		make_aptbox
		kernel_evr="$(latest_package_evr "$kernel_pkg")" ||
			fatal "Cannot find $kernel_pkg package"
	else
		headers="$(find "$rpm_rpmdir" -type f \
			-name "$kernel_pkg-*.rpm")"
		[ -n "$headers" ] ||
			fatal "No $kernel_pkg-*.rpm packages found"
		[ -n "${headers##*$LF*}" ] ||
			fatal "Multiple $kernel_pkg-*.rpm packages found"
		kernel_evr="$(get_package_evr "$headers")"
	fi
	kernels="$kernels $flavour=$kernel_evr"
done

# Collect all modules to build
all_modules=
all_modules_count=0
for kernel in $kernels; do
	KERNEL_FLAVOUR="${kernel%=*}"
	parse_kernel_evr "${kernel##*=}"
	echo "* Kernel: $KERNEL_VERSION-$KERNEL_FLAVOUR-$KERNEL_RELEASE"
	if [ $# -eq 0 ]; then
		branch_spec=
		if [ "${opt_distribution}" != "${opt_distribution##alt-linux-}" ]; then
			branch_spec=".M${opt_distribution##alt-linux-}"
			branch_spec=".${branch_spec//./}"
		fi
		modules="$(get_modules_to_build "kernel-image-$KERNEL_FLAVOUR${branch_spec}")"
	else
		modules="$*"
	fi
	for module in $modules; do
		all_modules="$all_modules $kernel=$module"
		all_modules_count=$(( $all_modules_count + 1 ))
	done
done

# Prepare for build
make_rpm_dirs
[ -n "$opt_hasher$opt_no_build" ] || [ -z "$opt_install" ] || install_packages

# Now build everything
module_num=0
num_ok=0 num_failed=0 num_skipped=0
for module in $all_modules; do
	module_num=$(( $module_num + 1 ))
	KERNEL_FLAVOUR="${module%%=*}"
	module="${module#*=}"
	parse_kernel_evr "${module%%=*}"
	module="${module#*=}"
	package="kernel-modules-$module-$KERNEL_FLAVOUR"
	log_file="$rpm_logdir/$package.log"
	printf "* Package %*d of %d: %s: " \
		${#all_modules_count} $module_num $all_modules_count "$package"
	( GIT_DIR="$TOP/modules/.git" build_module "$module" ) \
		>"$log_file.tmp" 2>&1
	rc=$?
	case $rc in
		42)	rm -f "$log_file.tmp"			;;
		*)	mv -f "$log_file.tmp" "$log_file"	;;
	esac
	case $rc in
		0)
			echo "Ok"
			num_ok=$(( $num_ok + 1 ))
			;;
		42)
			echo "skipped"
			num_skipped=$(( $num_skipped + 1 ))
			;;
		*)
			echo "FAILED"
			num_failed=$(( $num_failed + 1 ))
			;;
	esac
done

echo "Finished: $num_ok ok, $num_failed failed, $num_skipped skipped"
[ $num_failed = 0 ] && exit 0 || exit 1
