#!/bin/bash
## Manage linux driver patches for netmap.
## usage (from the dir containing the Makefile):
## 
##    scripts/np <action> [args...]
##
## where <action> is any of the functions below.
##

[ -f scripts/conf ] && source scripts/conf

## The following enviroment variables must be set:
##
##  GITDIR: the absolute path of the netmap linux
## 	git repository, containing all the required netmap-*
## 	branches.
[ -n "$GITDIR" -a -d "$GITDIR/.git" ] || {
	echo "GITDIR not set or not valid" >&2
	exit 1
}

NETMAP_BRANCH=${NETMAP_BRANCH:-master}

function error {
	echo "$@" >&2
	exit 1
}

function get-params {
	local params=$1; shift
	err_msg="$PROGNAME $COMMAND $(echo $params| perl -pe 's/\S+/<$&>/g')"
	local param
	for param in $params; do
		[[ -z "$@" ]] && error "$err_msg"
		pname=$(echo -n $param | perl -pe 's/\W/_/g')
		eval $pname="$1"
		shift
	done
	[[ -n "$@" ]] && error "$err_msg"
}

##
##  LINUX_SOURCES: the absolute path of a
## 	directory used to store all required linux-* source trees
## 	(The script will extract linux-x.y.z from GITDIR if it needs
## 	it and $LINUX_SOURCES does not already contain it).
##
##  LINUX_CONFIGS: the absolute path of a
## 	directory containing the configuration files for 
## 	the linux kernel. The file for version x must be named
## 	config-x. config-all can be used as a default.
##
## The configuration variables can be put in scripts/conf.
##

##
## Available actions:
##

##
##  driver-path <driver> <version>
##	retrieves the path of <driver> in the linux sources
##	for version <version>. The path is output to stdout.
##	It uses a local cache to minimize the expensive
##	file system search.
function driver-path()
{
	get-params "driver version" "$@"
	
	cat cache/$version/$driver/path 2>/dev/null && return
	local kern=$(get-kernel $version)
	mkdir -p cache/$version/$driver
	(
		cd $kern
		find drivers/net -name $driver
	) | tee cache/$version/$driver/path
}


##
##  get-patch [-c] <driver> <version>
##	extract the netmap patch for the given <driver> and the
##	given kernel <version>. The patch is stored in tmp-patches
##	and the name of the patch is output to stdout.
##	If a patch with the same name already exists in tmp-patches
##	it is overwritten, unless the -c option is used,
##	in which case the existing patch is kept (the patch name is still output).
function get-patch()
{
	local use_cache
	[ "$1" = -c ] && { use_cache=1; shift; }

	get-params "driver version" "$@"

	# convert kernel version to fixed notation
	local v1=$(scripts/vers $version -c)
	# compute next kernel version (in fixed notation)
	local v2=$(scripts/vers $version -i -c)
	local patchname=diff--$driver--$v1--$v2
	local out=tmp-patches/$patchname
	[ -n "$use_cache" -a -s $out ] && { echo $out; return; }
	local drvpath=$(driver-path $driver $version)
	[ -n "$drvpath" ] || return
	local drvdir=$(dirname $drvpath)
	(
		cd $GITDIR
		git diff --relative=$drvdir v$version..netmap-$version -- $drvpath
	) > $out
	# an empty patch means no netmap support for this driver
	[ -s $out ] || { rm $out; return 1; }
	echo $out
	return 0;
}

##
##  get-range <driver> <version1> <version2>
##	extracts the netmap patches for the given <driver> for
##	all the kernel versions from <version1> (included) to
##	<version2> (excluded). All patches are stored in tmp-patches
##	and their names are output to stdout.
function get-range()
{
	get-params "driver version1 version2"	 "$@"

	local v=$version1
	# while version is less than $version2
	while scripts/vers -b $v $version2 -L; do
		get-patch $driver $v
		# compute next version
		v=$(scripts/vers $v -i)
	done
}


##
##  get-src <driver> <version> <dest>
##	copies the original sources of the given <driver>,
##	from the given kernel <version> to the given <dest>
##	directory.
##	It uses a local cache to minimize the expensive
##	checkouts in GITDIR.
function get-src()
{
	get-params "driver version dest" "$@"

	local kern=$(get-kernel $version)
	local src=$(driver-path $driver $version)
	cp -r $kern/$src $dest
}


##
##  extend <patch> <version>
##	checks wether the range of applicability of the 
##	given <patch> can be extented to include <version>.
##	It returns 0 on success and 1 on failure.
function extend()
{
	get-params "patch version" "$@"

	local _patch=$(realpath $patch)
	# extract the driver name from the patch name
	local driver=$(scripts/vers $_patch -s -p -p)
	local tmpdir1=$(mktemp -d)
	local tmpdir2=$(mktemp -d)
	trap "rm -rf $tmpdir1 $tmpdir2" 0
	# we get the driver sources for the given <version> and
	# we apply two patches separately:
	# i) the given <patch>;
	# ii) the proper patch from GITDIR.
	# We declare <patch> to be extendable if
	# - it is still applicable AND
	# - we obtain the same files from i) and ii) (ignoring whitespace)
	get-src $driver $version $tmpdir1
	get-src $driver $version $tmpdir2
	(
		cd $tmpdir1
		patch --no-backup-if-mismatch -p1 < $_patch >/dev/null 2>&1
	) || return 1
	local patch2=$(get-patch -c $driver $version)
	patch2=$(realpath $patch2)
	(
		cd $tmpdir2
		patch -p1 < $patch2 >/dev/null 2>&1
	) # this will certainly apply
	diff -qbBr $tmpdir1 $tmpdir2 >/dev/null || return 1
	return 0
} 

##
##  minimize <driver>
##	tries to minimize the number of patch files for the given
##	<driver>. It uses the patches currently found in tmp-patches
##	and stores the resulting patches in final-patches.
##	If final-patches already contained patches for <driver>,
##	they are deleted first.
function minimize()
{
	get-params "driver" "$@"

	mkdir -p final-patches
	local drv=$(basename $driver)
	local patches=$(ls tmp-patches/diff--$drv--* 2>/dev/null)
	[ -n "$patches" ] || return 1
	# put the patch names in $1, $2, ...
	set $patches
	rm -f final-patches/diff--$drv--*
	# the original patches (in tmp-patches) are ordered by version number.
	# We consider one patch in turn (the 'pivot') and try
	# to extend its range to cover the range of the next
	# patch. If this succedes, the merged patch is the new
	# pivot, otherwise the current pivot is output and the
	# next patch becomes the new pivot. The process
	# is repeated until there are no more patches to consider.
	local pivot=$1
	[ -n "$pivot" -a -e "$pivot" ] || return 1
	# extract the left end and right end of the pivot's range
	local ple=$(scripts/vers $pivot -s -p -C)
	local pre=$(scripts/vers $pivot -s -C)
	while [ -n "$pivot" ]; do
		shift
		if [ -n "$1" ]; then 
			# extract the left end and right end of the next patch
			local nle=$(scripts/vers $1 -s -p -C)
			local nre=$(scripts/vers $1 -s -C)
			# we admit no gaps in the range
			if [ $pre = $nle ] && extend $pivot $nle; then
				pre=$nre
				continue
			fi
		fi
		# either out of patches or failed merge.
		# Compute the file name of the current pivot and store
		# the patch in its final location
		out=$(scripts/vers diff $drv $ple -c $pre -c -S4)
		cp $pivot final-patches/$out
		# the new pivot becames the next patch (if any)
		pivot=$1
		pre=$nre
		ple=$nle
	done
	return 0
}

##
##  infty <driver> <version>
##	if final-patches contains a patch for <driver> with a range
##	ending in <version>, extend it to infinity.
##	Do nothing otherwise.
function infty()
{
	get-params "driver version" "$@"

	local drv=$(basename $driver)
	# convert kernel version to fixed notation
	local v=$(scripts/vers $version -c)
	local last=$(ls final-patches/diff--$drv--*--$v 2>/dev/null|tail -n1)
	[ -n "$last" ] || return 1
	mv -n $last $(scripts/vers $last -s -p 99999 -S4) 2>/dev/null
}

function get-kernel()
{
	get-params "version" "$@"

	local dst="$(realpath $LINUX_SOURCES)/linux-$version"

	[ -d $dst ] && { echo $dst; return; }

	mkdir -p $dst

	(
		cd $GITDIR
		git archive v$v | tar xf - -C $dst 
	)
	echo $dst
}


##
##  build-prep <version>
##	prepare the linux tree for <version> to be ready
##	for external modules compilation.
##	The tree is put in $LINUX_SOURCES/linux-<version> and the
##	configuration is obtained from $LINUX_CONFIGS/config-<version>
##	(or $LINUX_CONFIGS/config-all by default).
##	Errors are logged to $LINUX_CONFIGS/linux-<version>.log.
##	If $LINUX_SOURCES/linux-<version> already exists,
##	nothing is done.
##	In all cases, the absolute path of linux-<version> is
##	output.
function build-prep()
{
	get-params "version" "$@"

	local dst=$(get-kernel $version)

	exec 3>&1 4>&2 >$dst.log 2>&1
	cp $LINUX_CONFIGS/config-$v $dst/.config  2>/dev/null ||
	cp $LINUX_CONFIGS/config-all $dst/.config 
	(
		cd $dst
		yes '' | make oldconfig
		make modules_prepare
	)
	exec 1>&3 2>&4
	echo $dst
}

##
##  check-patch <patch>
##	check that the given <patch> applies and compiles without
##	error for all its declared range of applicability.
##	Errors are logged to log/<patch>.
function check-patch()
{
	get-params "patch" "$@"

	# extract the left version
	local v1=$(scripts/vers $patch -s -p -C)
	# extract the right version
	local v2=$(scripts/vers $patch -s -C)
	# extract the driver name
	local driver=$(scripts/vers $patch -s -p -p)
	local p=$(realpath $patch)
	mkdir -p log
	local log="$(realpath log)/$(basename $patch)"
	local nmcommit=$(cd ..; git show-ref -s heads/$NETMAP_BRANCH)

	echo -n $patch...

	while scripts/vers -b $v1 $v2 -L; do
		# cache lookup
		local cache=cache/$v1/$driver
		local cpatch=$cache/patch
		local cnmcommit=$cache/nmcommit
		local cstatus=$cache/status
		local clog=$cache/log
		if [ -f $cpatch ]				&&
		   cmp -s $cpatch $patch			&&
		   [ "$nmcommit" = "$(cat $cnmcommit)" ]; then
			cp $clog $log
			ok=$(cat $cstatus)
		else
			# update cache
			cp $patch $cpatch
			echo $nmcommit > $cnmcommit

			local ksrc=$(build-prep $v1)
			local tmpdir=$(mktemp -d)
			trap "rm -rf $tmpdir" 0
			(cd ..; git archive $NETMAP_BRANCH | tar xf - -C $tmpdir )
			pushd $tmpdir/LINUX >/dev/null
			mkdir single-patch
			rm patches
			ln -s single-patch patches
			cp $p single-patch
			ok=false
			make KSRC=$ksrc >$log 2>&1 && ok=true
			popd >/dev/null
			cp $log $clog
		fi
		[ $ok = true ] || { echo FAILED; echo false > $cstatus; return 1; }
		echo true > $cstatus
		rm -rf $tmpdir
		# compute next version
		v1=$(scripts/vers $v1 -i)
	done
	echo OK
}

##
##  build-check <driver>
##	do a check-patch for all the patches of <driver> that are
##	currently in tmp-patches. Patches that fail the check
##	are moved to failed-patches.
function build-check()
{
	get-params "driver" "$@"

	mkdir -p failed-patches
	local drv=$(basename $driver)
	local patches=$(ls tmp-patches/diff--$drv--* 2>/dev/null)
	local p
	for p in $patches; do
		check-patch $p || mv $p failed-patches
	done
}

##
##  forall <cmd> [args...]
##	exec <cmd> <driver> [args...] for all known drivers.
function forall()
{
	local cmd=$1
	shift
	# we obtain the value of DRIVER_SRC from the makefile
	# (the +% target is defined in our Makefile and prints
	# the contents of variable %)
	local driver_srcs=$(make +DRIVER_SRCS)

	local driver
	for driver in $driver_srcs; do
		$cmd $(basename $driver) "$@"
	done
}

mkdir -p tmp-patches

PROGNAME=$0

[ -n "$1" ] || {
	scripts/help $PROGNAME;
	exit 1
}

COMMAND=$1; shift
case $COMMAND in
*-all)
	forall ${COMMAND%-all} "$@"
	;;
-[hH]|--help|-help|help)
	scripts/help $PROGNAME
	;;
*)
	$COMMAND "$@"
	;;
esac
