#!/bin/sh
#
# SliTaz Build Bot. The Cooker is a tool to automate and test SliTaz package
# building. Please read the Cookbook documentation for more information
# and discuss with the AUTHORS before adding anything here. PS: no translations
# here since it's not an end user tool and it's not useful. All devs should
# at least understand basic English.
#

. /usr/lib/slitaz/libcook.sh

# Set pkg name and use same wok as cook.
pkg="$2"
wok="$WOK"

# PID file.
pidfile='/var/run/cooker.pid'

#
# Functions
#

usage() {
	cat <<EOT

Usage: cooker [command] [pkg|list|note|hours]

Options:
  usage|-u        Display this short usage.
  setup|-s        Setup the Cooker environment.
  setup-cron      Setup a cron job for the Cooker.
  arch-db         Create host arch packages DB.
  note|-n         Add a note to the cooknotes.
  notes|-ns       Display all the cooknotes.
  block|-b        Block a package so cook will skip it.
  unblock|-ub     Unblock a blocked package.
  pkg|-p          Same as 'cook pkg' but with cooker log.
  flavor|-f       Cook all packages of a flavor.
  list|-l         Cook all packages in the given list.
  cat|-c          Cook all packages of a category.
  rev|-r          Cook packages of a specific revision.
  all|-a          Find and cook all unbuilt packages.

EOT
	exit 0
}


# Some messages occur in activity but log verbose output when checking for commits
# into a log file.

log_commits() {
	sed '/^.\//'d | sed '/^.hg/'d | tee -a $LOGS/commits.log
}


# Clean up before exit when check and cook commits finish.

clean_exit() {
	rm -f $command; touch $command
	[ "$previous_command" ] && ps | grep -q "${previous_command/:/ }" &&
	echo -n "$previous_command" > $command
	rm -f $pidfile
}


# Summary for commits.

commits_summary() {
	msg="from revision $cur to $new"
	[ "$new" == "$cur" ] && msg="revision $new"
	echo "Will cook $msg"
	separator
	echo -e "\nSummary for commits"
	separator
	echo "Hg wok revision  : $cur"
	echo "Pulled revision  : $new"
	echo "Check date       : $(date '+%F %T')"
}


# Scan packages build deps and fill up cookorder list.

cook_order_scan() {
	rm -f $cookorder
	touch $cookorder
	for pkg in $(cat $cooklist); do
		unset WANTED BUILD_DEPENDS
		. $wok/$pkg/receipt
		# The :: is for web interface color.
		[ "$WANTED$BUILD_DEPENDS" ] && echo "$pkg :: $WANTED $BUILD_DEPENDS"
		for dep in $WANTED $BUILD_DEPENDS; do
			if grep -q "^$dep$" $cooklist; then
				if ! grep -q "^$dep$" $cookorder; then
					echo "$dep" >> $cookorder
				fi
			fi
		done
	done

	# Append unordered packages to cookorder.
	for pkg in $(cat $cooklist); do
		if ! grep -q "^$pkg$" $cookorder; then
			echo "$pkg" >> $cookorder
		fi
	done
}


# Scan and rescan until the cooklist is ordered then handle WANTED.

cook_order() {
	time=$(date +%s)
	scan=0

	# Keep an original cooklist so we do a diff when ordering is finished.
	cp -f $cooklist $cooklist.0
	echo 'cookorder' > $command
	echo -e '\nInitial Cooker order scan'
	separator
	cook_order_scan

	# Diff between the cooklist and new ordered list ? So copy the last
	# cookorder to cooklist and rescan it.
	while /bin/true; do
		diff $cooklist $cookorder > $cookorder.diff
		if [ -s "$cookorder.diff" ]; then
			scan=$(($scan + 1))
			echo -e "\nDiff scan: $scan"
			separator
			mv -f $cookorder $cooklist
			cook_order_scan
		else
			break
		fi
	done

	# Keep a diff between submitted cooklist and the ordered.
	diff $cooklist.0 $cooklist > $cooklist.diff
	rm -f $cookorder $cookorder.diff $cooklist.0

	# Scan finished: append pkg to WANTED or leave it in the ordered cooklist.
	# TODO: grep the line number to get pkg position and keep it higher.
	echo -e '\nHandle WANTED package'
	separator
	for pkg in $(cat $cooklist); do
		unset WANTED
		. $wok/$pkg/receipt
		for wanted in $WANTED; do
			echo "$pkg :: $wanted"
			if grep -q ^${wanted}$ $cooklist; then
				sed -i -e "/^$pkg$/"d  \
					-e "/^$wanted$/ a $pkg" $cooklist
			fi
		done
	done

	# Show ordered cooklist
	echo -e '\nCooklist order'
	separator
	cat $cooklist
	separator

	time=$(($(date +%s) - $time))
	pkgs=$(cat $cooklist | wc -l)
	echo -e '\nSummary for cookorder'
	separator
	cat <<EOT
Ordered packages : $pkgs
Scans executed   : $scan
Scan duration    : ${time}s
EOT
	separator

	rm -f $command
}


# Remove blocked (faster this way than grepping before).

strip_blocked() {
	for pkg in $(cat $blocked); do
		sed -i /^${pkg}$/d $cooklist
	done
	sed -i /^$/d $cooklist
}


# Use in default mode and with all cmd.

cook_commits() {
	if [ -s "$commits" ]; then
		for pkg in $(cat $commits); do
			ps | grep -q "cook $pkg$" && continue
			echo "cook:$pkg" > $command
			cook $pkg || broken
			sed -i /^${pkg}$/d $commits
		done
	fi
}


# Cook all packages in a cooklist.

cook_list() {
	for pkg in $(cat $cooklist); do
		ps | grep -q "cook $pkg$" && continue
		cook $pkg || broken
		sed -i /^${pkg}$/d $cooklist
	done
}


# Create a arch.$ARCH file for each package cooked for the target host
# architecture
#
# The deal: we dont want all packages handled by cooker commands and stats,
# since we dont cross compile all packages for each arch but only a set of
# packages to provide one full featured desktop, servers and goodies useful
# for the host system.
#

arch_db() {
	count=0
	echo "Cleaning packages DB : arch.$ARCH"
	rm -f $wok/*/arch.$ARCH && cd $wok
	echo "Creating $ARCH packages DB..."
	for pkg in *; do
		[ -s $wok/$pkg/receipt ] || continue
		HOST_ARCH=
		. $wok/$pkg/receipt
		if [ -n "$HOST_ARCH" ]; then
			if $(echo "$HOST_ARCH" | egrep -q "$ARCH|any"); then
				count=$(($count + 1))
				echo "Adding: $pkg"
				touch $pkg/arch.$ARCH
			fi
			unset HOST_ARCH
		else
			# HOST_ARCH not set --> in i486
			if [ "$ARCH" == 'i486' ]; then
				count=$(($count + 1))
				echo "Adding: $pkg"
				touch $pkg/arch.$ARCH
			fi
		fi
	done
	echo "Packages for $ARCH : $count"
}


#
# Commands
#

previous_command="$(cat $command)"
case "$1" in
	usage|help|-u|-h)
		usage ;;

	setup|-s)
		# Setup the Cooker environment.
		echo -e '\nSetting up the Cooker'
		mkdir -p $CACHE
		echo "Cooker setup using: $SLITAZ" | log
		separator
		for pkg in $SETUP_PKGS mercurial rsync tazlito; do
			[ ! -d "$INSTALLED/$pkg" ] && tazpkg get-install $pkg
		done
		mkdir -p $SLITAZ && cd $SLITAZ
		if [ -d "${wok}-hg" ]; then
			echo -e 'Hg wok already exists.\n'
			exit 1
		fi
		if [ -d "$wok" ]; then
			echo -e 'Build wok already exists.\n'
			exit 1
		fi

		# Directories and files
		echo "mkdir's and touch files in: $SLITAZ"
		mkdir -p $PKGS $LOGS $FEEDS $CACHE $SRC
		for f in $activity $blocked $broken $commits $cooklist $command; do
			touch $f
		done
		hg clone $WOK_URL ${wok}-hg || exit 1
		[ -d "$flavors" ] || hg clone $FLAVORS_URL flavors
		cp -a ${wok}-hg $wok
		separator; newline ;;

	arch-db)
		# Manually create arch packages DB.
		arch_db ;;

	setup-cron)
		# Create cron job for the cooker.
		[ "$2" ] || hours=2
		if [ ! -f "$crontabs" ]; then
			mkdir -p /var/spool/cron/crontabs
		fi
		if ! fgrep -q /usr/bin/cooker $crontabs; then
			cat > $crontabs << EOT
# Run SliTaz Cooker every $hours hours
59 */$hours * * *  touch $CACHE/cooker-request
*/5 * * * *  [ $CACHE/cooker-request -nt $CACHE/activity ] && /usr/bin/cooker --output=html
*/5 * * * *  [ -z "$(pidof cooker)" ] && [ -s $CACHE/recook-packages ] && /usr/bin/cooker list $CACHE/recook-packages
EOT
			touch $CACHE/cooker-request $CACHE/recook-packages
			chmod 666 $CACHE/cooker-request $CACHE/recook-packages
			killall crond 2>/dev/null && /etc/init.d/crond start
		fi ;;

	check-cron)
		if [ ! -f "$crontabs" ]; then
			echo "There is no $crontabs here. Use setup-cron option."
			exit 1
		fi
		fgrep /usr/bin/cooker $crontabs ;;

	note|-n)
		# Blocked a pkg and want others to know why? Post a note!
		[ -n "$2" ] && echo "$(date '+%F %R') : $2" >> $cooknotes ;;

	notes|-ns)
		# View cooknotes.
		echo -e '\nCooknotes'
		separator
		cat $cooknotes
		separator; newline ;;

	block|-b)
		# Block a package.
		[ "$pkg" ] && cook $pkg --block ;;

	unblock|-ub)
		# Unblock a package.
		[ "$pkg" ] && cook $pkg --unblock ;;

	reverse|-r)
		# Cook all reverse dependencies for a package. This command lets us
		# control the Cooker manually for commits that will cook a lot of packages.
		#
		# Use hg commit? Ex: hg commit -m "Message bla bla | cooker:reverse"
		#
		if [ ! -d "$wok/$pkg" ]; then
			echo -e "\nNo package $2 found.\n"
			exit 0
		fi
		rm -f $cooklist; touch $cooklist
		echo -e "\nReverse cooklist for: $pkg"
		separator

		cd $wok
		for rev in *; do
			[ -s $wok/$rev/receipt ] || continue
			unset WANTED DEPENDS BUILD_DEPENDS; . $wok/$rev/receipt
			if echo "$WANTED $DEPENDS $BUILD_DEPENDS" | fgrep -q $pkg; then
				echo "$rev" | tee -a $cooklist
			fi
		done
		separator
		echo -e "Reverse dependencies found: $(cat $cooklist | wc -l)\n"
		strip_blocked
		cook_order | tee $LOGS/cookorder.log
		cook_list ;;

	pkg|-p)
		# Same as 'cook pkg' but with log for web interface.
		ps | grep -q "cook $pkg$" && echo 'Already running' && continue
		cook $pkg || broken
		clean_exit ;;

	cat|-c)
		# Cook all packages of a category.
		cat="$2"
		rm -f $cooklist; touch $cooklist

		cd $wok
		for pkg in *; do
			[ -s $pkg/receipt ] || continue
			unset CATEGORY; . $pkg/receipt
			[ "$CATEGORY" == "$cat" ] && echo $pkg >> $cooklist
		done
		strip_blocked
		cook_order | tee $LOGS/cookorder.log
		cook_list ;;

	flavor|-f)
		# Cook all packages of a flavor.
		name="$2"
		if [ ! -d "$flavors/$name" ]; then
			echo -e "\nSpecified flavor does not exist: $name\n"
			exit 1
		fi
		if [ -d "$flavors/.hg" ]; then
			cd $flavors; hg pull -u
		fi
		list="$flavors/$name/packages.list"
		cp -a $list $cooklist
		strip_blocked
		cook_order | tee $LOGS/cookorder.log
		cook_list ;;

	list|-l)
		# Cook a list of packages given in argument.
		list="$2"
		if [ ! -f "$list" ]; then
			echo -e "\nSpecified list does not exist: $list\n"
			exit 1
		fi
		cat $list >> $cooklist
		echo -n > $list
		strip_blocked
		cook_order | tee $LOGS/cookorder.log
		cook_list ;;

	rev|-r)
		# Cook or recook a specific Hg revision.
		rev="$2"
		[ "$rev" ] || exit 0
		rm -f $cooklist; touch $cooklist

		cd $wok
		for pkg in $(hg log --rev=$rev --template "{files}"); do
			echo "$pkg" | cut -d/ -f1 >> $cooklist
		done
		strip_blocked
		cook_order | tee $LOGS/cookorder.log
		cook_list ;;

	all|-a)
		# Try to build all unbuilt packages except blocked's.
		echo 'cooker:all' > $command
		rm -f $cooklist; touch $cooklist
		newline
		echo 'Cooker cooklist'
		separator

		# Find all unbuilt packages. Get EXTRAVERSION from packed receipt
		# if it exists since extra version is added when packing the package.
		echo 'Searching for all unbuilt packages' | log

		cd $wok
		for pkg in *; do
			[ -s $pkg/receipt ] || continue
			unset EXTRAVERSION
			. $pkg/receipt
			[ -f "$pkg/taz/$PACKAGE-$VERSION/receipt" ] && \
				. $pkg/taz/$PACKAGE-$VERSION/receipt
			if [ ! -f "$PKGS/$PACKAGE-$VERSION$EXTRAVERSION.tazpkg" ]; then
				echo $pkg; echo $pkg >> $cooklist
			fi
		done
		strip_blocked
		cook_order | tee $LOGS/cookorder.log
		echo "Packages to cook: $(cat $cooklist | wc -l)" | log
		cook_list ;;

	*)
		# Default is to cook all commits if not yet running.
		[ -n "$1" ] && usage
		cooklist=$commits
		if [ -f "$pidfile" ]; then
			pid=$(cat $pidfile)
			if [ -s /proc/$pid/status ]; then
				echo -e "\nStill cooking latest commits with pid:"
				echo -e " $pid\n"
				exit 0
			fi
			rm -f "$pidfile"
		fi

		# Start and get a PID file.
		rm -f $LOGS/commits.log
		newline
		echo 'Checking for commits' | log_commits
		separator | tee -a $LOGS/commits.log

		echo $$ > $pidfile
		trap 'echo -e "\nCooker stopped: PID $$\n" && \
			rm -f $pidfile $command && exit 1' INT TERM

		echo "Cooker PID   : $$" | log_commits
		echo "Cooker date  : $(date '+%F %T')" | log_commits

		# Get revisions. Here we have 2 echoes since we want a msg on screen,
		# in commits log and activity DB without a space before.
		cd $wok || exit 1
		cur=$(hg head --template '{rev}\n')
		echo "Updating wok : ${wok}-hg (rev $cur)" | log_commits
		echo "Updating wok: ${wok}-hg" | log
		echo 'hg:pull' > $command
		cd $wok-hg; hg pull -u | log_commits
		new=$(hg head --template '{rev}\n')
		# Store last rev to be used by CGI so it doesn't need to call hg head
		# on each load.
		echo "$new" > $wokrev

		# Sync build wok with rsync so we don't take care about removing old
		# files as before.
		if [ "$new" -gt "$cur" ]; then
			echo "Changes found from: $cur to $new" | log
			echo 'Syncing build wok with Hg wok...' | log_commits
			rsync -r -t -c -l -u -v -D -E $wok-hg/ $wok/ | \
				sed '/^$/'d | log_commits
		else
			echo "No revision changes: $cur vs $new" | log
			separator | log_commits
			clean_exit; newline
			exit 0
		fi

		# Get and display modifications.
		cd $wok-hg
		commits_summary | log_commits
		cur=$(($cur + 1))
		rm -f $commits.tmp; touch $commits.tmp
		for rev in $(seq $cur $new); do
			for file in $(hg log --rev=$rev --template "{files}"); do
				pkg=$(echo $file | cut -d/ -f1)
				desc=$(hg log --rev=$rev --template "{desc}" $file)
				echo "Committed package : $pkg - $desc" | log_commits
				echo $pkg >> $commits.tmp
			done
		done

		# We may have deleted packages and files in stuff/. Remove it and
		# clean DB as well as log file.
		cd $wok
		for pkg in *; do
			if [ ! -d "${wok}-hg/$pkg" ]; then
				echo "Removing package: $pkg" | log_commits
				if [ -s $wok/$pkg/receipt ]; then
					. $wok/$pkg/receipt
					rm -f $PKGS/$PACKAGE-$VERSION*
				fi
				rm -rf $wok/$pkg $LOGS/$pkg.log
				sed -i "/^${pkg}$/"d $blocked $broken $commits.tmp
			fi
		done

		# Keep previous commit and discard duplicate lines
		cat $commits $commits.tmp | sed /"^$"/d > $commits.new
		uniq $commits.new > $commits; rm $commits.*

		# Handle cross compilation. Create arch packages DB and remove pkgs
		# not cooked for this arch from the commits list.
		arch_db
		for pkg in $(cat $commits); do
			if [ ! -f "$wok/$pkg/arch.$ARCH" ]; then
				echo "Cooker arch : skip $pkg (not included in: $ARCH)" | \
					log_commits
				sed -i "/^${pkg}$/"d $commits
			else
				echo "Cooker arch : $ARCH" | log_commits
			fi
		done

		# Stats
		pkgs=$(cat $commits | wc -l)
		echo "Packages to cook: $pkgs" | log
		echo "Packages to cook : $pkgs" | log_commits
		separator | log_commits
		newline
		strip_blocked
		cook_order | tee $LOGS/cookorder.log
		cook_commits
		clean_exit ;;
esac

exit 0
