#!/bin/sh
#
# Copyright (c) 2003-2004 Michael Telahun Makonnen <mtm@FreeBSD.Org>.
# 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 THE AUTHOR ``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 THE AUTHOR 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: build,v 1.11 2004/10/10 12:28:35 mtm Exp mtm $
#


ARCH=${ARCH:-`/sbin/sysctl -n hw.machine_arch`}
J=
JLIST="0"
BUILDROOT=${BUILDROOT:-/usr}
SRCROOT=${SRCROOT:-${BUILDROOT}}
OBJROOT=${OBJROOT:-${BUILDROOT}}
TIMECMD=/usr/bin/time
PATCHPREFIX=".done."
srctree=src
srcbranch=HEAD
ropt=
srcdir="${SRCROOT}/src"
objdir="${OBJROOT}/obj"
kernel=${BUILD_KERNELS:-GENERIC}
logfile=buildlog
worldlog="${logfile}"
kernellog="${logfile}"
cvslog="${logfile}"
doKernel=
doWorld=
doClean=
doCleanCvs=
doCvs=
doPatch=
noclean=
patchdir="${BUILDROOT}/patches"
kernelConfPath="${BUILDROOT}/conf"
_me=`basename $0`

trap 'exit 1' SIGHUP SIGINT SIGQUIT

# build_kernel kernname log
#	Build the kernel kernname.
#	global variables: J, OBJROOT, srcdir
#
build_kernel()
{
	local _ret kernel log kcpath srcSysConf
	kernel=$1
	log="$2"
	srcSysConf="${srcdir}/sys/${ARCH}/conf/${kernel}"
	kcpath="${kernelConfPath}/${kernel}"

	[ -z "$doKernel" ] && return 1

	# Make sure the kernel configuration file exists
	if [ ! -e "${srcSysConf}" ]; then
		if [ -f "$kcpath" -o -L "$kcpath" ]; then
			echo "Setting up kernel conf file ${kernel}."
			ln -s "${kcpath}" "${srcSysConf}"
		else
			echo "ERR: Unable to find configuration file: ${kcpath}"
			return 1
		fi
	fi

	cd ${srcdir}
	echo -n "Building kernel ${kernel}:   "
	env MAKEOBJDIRPREFIX="$objdir"					\
	    ${TIMECMD} make ${J} ${noclean} KERNCONF=${kernel}		\
	    buildkernel > ${log} 2>&1
	_ret=$?
	if [ $_ret -eq 0 ]; then
		echo OK
	else
		echo ERROR
	fi
	return $_ret
}

build_world()
{
	local _ret log
	log="$1"

	[ -z "$doWorld" ] && return 1
	cd ${srcdir}
	echo -n "Building world:   "
	env MAKEOBJDIRPREFIX="$objdir" \
	    ${TIMECMD} make -DUSE_KQUEUE ${J} ${noclean} buildworld > ${log} 2>&1
	_ret=$?
	if [ $_ret -eq 0 ]; then
		echo OK
	else
		echo ERROR
	fi
	return $_ret
}

# cvs_update
#
cvs_update()
{
	local _conflicts _ret logfile
	logfile="$1"

	[ -z "$doCvs" ] && return 1
	[ -z "$logfile" ] && return 1
	if [ -z "$CVSROOT" ]; then
		echo 'ERR: CVSROOT not defined. Cannot update from cvs.'
		return 1
	fi

	cd ${SRCROOT}

	if [ -n "$doCleanCvs" ]; then
		echo "Removing cvs source tree."
		rm -rf src
		reset_patches
	fi

	echo -n "Updating branch ${srcbranch} module ${srctree}:   "
	echo cvs -R co -P ${srctree} > $logfile 2>&1
	cvs -R co -P ${ropt} ${srctree} >> $logfile 2>&1
	_ret=$?
	_conflicts="`grep '^C' $logfile`"
	if [ $_ret -eq 0 -a -z "$_conflicts" ]; then
		echo OK
	elif [ -n "$_conflicts" ]; then
		echo ERROR
		echo 'ERR: There were conflicts while updating from cvs'
	else
		echo ERROR
	fi
	return $_ret
}

# clean_obj objroot
#	Remove all files and directories under $objroot
#
clean_obj()
{
	local _objdir
	_objdir="$1"
	[ -z "$doClean" ] && return
	echo "Removing object tree."
	rm -rf "$_objdir/*"
}

# record_times jlevel log
#	Reads the /usr/bin/time output for a particular build session
#	from its log file. The argument $jlevel is used to output the
#	argument to the -j option of the make(1) command.
#
record_times()
{
	_j=$1
	_logfile="$2"
	_time="`tail -n 1 $_logfile | sed 's/\ \{1,\}/ /g'`"
	_date="`date '+%m/%d %H:%M'`"
	_ver="`uname -r`"
	_kernel="`uname -v | sed 'sx.*/xx'`"
	[ "$_j" -eq 0 ] && _jopt='---' || _jopt="-j$_j"
	printf '%-11s %-12s %-7s %-4s %-10s\n' "$_date" "$_ver" "$_kernel" "$_jopt" "$_time"
}

# create_timefile file
#	Output, to file, the headers for a list used to track build times
#	and the environment during the build.
#
create_timefile()
{
	local f
	f="$1"
	[ -z "$f" -o -f "$f" ] && return 1

	printf >${f} '%-11s %-12s %-7s %-4s %-10s\n' "" "Host" "Host" "make" ""
	printf >>${f} '%-11s %-12s %-7s %-4s %-10s\n' "Date" "OS" "Kernel" "-j" "Time"
	printf >>${f} '%-11s %-12s %-7s %-4s %-10s\n' "-----------" "------------" "-------" "----" "----------"
	return 0
}

# do_patch
#	Apply all patches in a designated directory to the src tree.
#	It keeps track of the patches that have already been applied
#	and does not reapply them. The default is that if it seems
#	a patch needs to be reverse-applied it will silently do so.
#
do_patch()
{
	local file dir acount
	dir="${BUILDROOT}/patches"
	failcount=0
	okcount=0
	acount=0

	[ -z "$doPatch" ] && return 1

	if [ ! -d "$dir" ]; then
		echo "ERR: Patch directory $dir does not exist. Skipping."
		return 0
	fi

	cd ${srcdir}
	ls ${dir} | while read file ; do
		# Don't reapply already applied patches
		[ -e "${dir}/${PATCHPREFIX}${file}" ] && continue

		if [ $acount -eq 0 ]; then
			echo "Applying patches."
			acount=1
		fi
		patch -s -t -i "${dir}/${file}"
		if [ "$?" -ne 0 ]; then
			failcount=$(($failcount + 1))
			echo "  ${file} : FAIL"
		else
			okcount=$(($okcount + 1))
			echo "  $file : OK"
		fi

		# Save info so next invocation doesn't try to reapply
		# the patches that have already been applied.
		#
		touch "${dir}/${PATCHPREFIX}${file}"
	done
	if [ $okcount -ne 0 -o $failcount -ne 0 ];then
		echo "Patches Applied:  $okcount OK,  $failcount FAILED"
	fi
}

# reset_patches
#	Usually used when a the old cvs tree is removed and a fresh
#	one is checked out. All the patches need to be reapplied.
#
reset_patches()
{
	local dir
	dir="${BUILDROOT}/patches"

	if [ ! -d "$dir" ]; then
		echo "ERR: Patch directory $dir does not exist. Skipping."
		return 0
	fi

	echo -n "Removing record of applied patches:"
	rm -rf "${dir}/${PATCHPREFIX}"*
	if [ $? -eq 0 ]; then
		echo ' OK'
	else
		echo ' ERROR'
	fi
}

main()
{
	local _k sufix logdir
	sufix="`date +"%Y.%m.%d.%H:%M"`"
	logdir="${BUILDROOT}/logs"

	worldlog="${BUILDROOT}/log.w"
	kernellog="${BUILDROOT}/log.k"
	cvslog="${BUILDROOT}/log.cvs"
	kerneltimes="${BUILDROOT}/times.k"
	worldtimes="${BUILDROOT}/times.w"

	# Create necessary files and directories
	#
	if [ ! -e "${BUILDROOT}" ]; then
		mkdir -p "${BUILDROOT}"
	elif [ ! -d "${BUILDROOT}" ]; then
		echo "${BUILDROOT} exists but is not a directory."
		exit 1
	fi
	if [ ! -e "${logdir}" ]; then
		mkdir -p "${logdir}"
	elif [ ! -d "${logdir}" ]; then
		echo "${logdir} exists but is not a directory."
		exit 1
	fi
	if [ ! -e "${objdir}" ]; then
		mkdir -p "${objdir}"
	elif [ ! -d "${objdir}" ]; then
		echo "${objdir} exists but is not a directory."
		exit 1
	fi

	create_timefile ${kerneltimes}
	create_timefile ${worldtimes}

	eval cvslog=\"${logdir}/cvs.${sufix}\"

	# Output info
	printf '%-22s: %s\n' "Build root" "${BUILDROOT}"
	printf '%-22s: %s\n' "Src dir" "$srcdir"
	printf '%-22s: %s\n' "Obj dir" "$objdir"
	printf '%-22s: %s\n' "Log directory" "$logdir"
	printf '%-22s: %s\n' "Patch directory" "$patchdir"
	printf '%-22s: %s\n' "Kernel conf directory" "$kernelConfPath"
	echo
	echo ">>>> Begin Build at `date`"
	echo

	# Update sources from cvs
	cvs_update "$cvslog"
	do_patch

	# Buildworld/buildkernel cycle depending on the 'make -j' level
	#
	for _i in ${JLIST}; do
		[ "$_i" -ne 0 ] && J="-j $_i" || J=

		clean_obj "${OBJROOT}"

		eval worldlog=\"${logdir}/world.j$_i.${sufix}\"
		build_world "$worldlog" &&
		    record_times $_i "$worldlog" >> "${worldtimes}"

		for _k in $kernel ; do
			eval kernellog=\"${logdir}/kernel-${_k}.j$_i.${sufix}\"
			build_kernel $_k "$kernellog" &&
			    record_times $_i "$kernellog" >> "${kerneltimes}"
		done

		sync; sync; sync
	done
	echo
	echo ">>>> End Build at `date`"
}

usage()
{
	echo "usage: $_me [-CcfhkOpw] [-b branch] [-j num] [-r base_dir] [-K kernel,kernel...]"
	echo "    -C : Remove all source files prior to updating (implies -c)"
	echo "    -c : do a cvs checkout"
	echo "    -f : do a fast build (pass -DNO_CLEAN to make(1))"
	echo "    -h : display this help screen"
	echo "    -k : build a kernel"
	echo "    -O : Remove object tree prior to building"
	echo "    -p : apply any patches found in the patch directory"
	echo "    -w : build world"
	echo "    -b branch   : which branch or revision to check out"
	echo "    -j num      : number to use for \'make -j\'"
	echo "    -r base_dir : the base directory in which to build sources"
	echo "    -K kernel,kernel...   : a comma separated list of the kernels to build"
	echo "ENVIRONMENT VARIABLES"
	echo "    BUILD_KERNELS : list of kernels to build"
}

args=`getopt cCfhkOpwK:b:j:r: $*`
if [ $? -ne 0 ]; then
	usage
	exit 1
fi
set -- $args
for opt ; do
	case "$opt" in
		-b)
			srcbranch=$2
			ropt="-r ${srcbranch}"
			shift;shift;;
		-C)
			doCleanCvs=yes
			doCvs=yes
			shift;;
		-c)
			doCvs=yes
			shift;;
		-f)
			noclean="-DNO_CLEAN"
			shift;;
		-h)
			usage
			exit 0;;
		-j)
			JLIST="$2"
			shift;shift;;
		-K)
			klist="$2"
			_saved_ifs="$IFS"
			IFS=","
			kernel=
			for k in $klist ; do
				kernel="$kernel $k"
			done
			IFS="$_saved_ifs"
			shift;shift;;
		-k)
			doKernel=yes
			shift;;
		-O)
			doClean=yes
			shift;;
		-p)
			doPatch=yes
			shift;;
		-r)
			BUILDROOT="$2"
			SRCROOT="$2"
			OBJROOT="$2"
			srcdir="$2/src"
			objdir="$2/obj"
			kernelConfPath="${BUILDROOT}/conf"
			patchdir="${BUILDROOT}/patches"
			shift;shift;;
		-w)
			doWorld=yes
			shift;;
	esac
done

main

exit $?
