#!/bin/sh

# configurable variables
pb=/var/portbuild

# XXX unused
get_latest_snap() {
    snap=$1

    zfs list -rHt snapshot ${snap} | tail -1 | awk '{print $1}'
}

now() {
    date +%Y%m%d%H%M%S
}

do_list() {
    arch=$1
    branch=$2

    buildpar=/var/portbuild/${arch}/${branch}/builds

    if [ -d ${buildpar} ]; then
	snaps=$(cd ${buildpar}; ls -1d 2* 2> /dev/null)
	echo "The following builds are active:"
	echo ${snaps}

	if [ -L ${buildpar}/latest -a -d ${buildpar}/latest/ ]; then
	    link=$(readlink ${buildpar}/latest)
	    link=${link%/}
	    link=${link##*/}

	    echo "Latest build is: ${link}"
	fi
    else
	echo "No such build environment ${arch}/${branch}"
	exit 1
    fi
}

do_create() {
    arch=$1
    branch=$2
    buildid=$3
    builddir=$4
    shift 4

    zfs create -o mountpoint=${builddir} a/portbuild/${arch}/${branch}/${buildid} \
	|| (echo "Couldn't create build"; exit 1)    

    echo "New build ID is ${buildid}"

}

do_clone() {
    arch=$1
    branch=$2
    buildid=$3
    builddir=$4
    shift 4

    if [ "$#" -gt 0 ]; then
	newid=$1
	shift
    else
	newid=$(now)
    fi

    tmp=$(realpath ${builddir})
    tmp=${tmp%/}
    newbuilddir="${tmp%/*}/${newid}"

    oldfs=a/portbuild/${arch}/${buildid}
    newfs=a/portbuild/${arch}/${newid}

    zfs snapshot ${oldfs}@${newid}
    zfs clone ${oldfs}@${newid} ${newfs}
    zfs set mountpoint=${newbuilddir} ${newfs}
    zfs promote ${newfs}

    if zfs list -H -t filesystem ${oldfs}/ports 2> /dev/null; then
	portsnap=${oldfs}/ports@${newid}
	zfs snapshot ${portsnap}
	zfs clone ${portsnap} ${newfs}/ports
	zfs promote ${newfs}/ports
    fi

    if zfs list -H -t filesystem ${oldfs}/src 2> /dev/null; then
	srcsnap=${oldfs}/src@${newid}
	zfs snapshot ${srcsnap}
	zfs clone ${srcsnap} ${newfs}/src
	zfs promote ${newfs}/src
    fi

    if [ -d ${newbuilddir} ]; then
	if [ ! -f ${pbab}/builds/previous/.keep ]; then
	    /var/portbuild/scripts/build destroy ${arch} ${branch} previous
	fi
        rm -f ${pbab}/builds/previous
	mv ${pbab}/builds/latest ${pbab}/builds/previous

	ln -sf ${newbuilddir} ${pbab}/builds/latest
    fi

    echo "New build ID is ${newid}"
}

do_portsupdate() {
    arch=$1
    branch=$2
    buildid=$3
    builddir=$4
    shift 4
    if [ $# -gt 0 ]; then
	arg=$1
	shift
    fi

    portsfs=a/portbuild/${arch}/${buildid}/ports

    destroy_fs a/portbuild/${arch} ${buildid} /ports || exit 1

    if [ "${arg}" = "-umount" ]; then
	return
    fi

    echo "================================================"
    echo "Reimaging ZFS ports tree on ${builddir}/ports"
    echo "================================================"
    
    now=$(now)
    zfs snapshot a/snap/ports@${now}
    zfs clone a/snap/ports@${now} ${portsfs}
    zfs set mountpoint=${builddir}/ports ${portsfs}
    cp ${builddir}/ports/cvsdone ${builddir}	
}

do_srcupdate() {
    arch=$1
    branch=$2
    buildid=$3
    builddir=$4
    shift 4
    if [ $# -gt 0 ]; then
	arg=$1
	shift
    fi

    srcfs=a/portbuild/${arch}/${buildid}/src

    destroy_fs a/portbuild/${arch} ${buildid} /src || exit 1

    if [ "${arg}" = "-umount" ]; then
	return
    fi

    echo "================================================"
    echo "Reimaging ZFS src tree on ${builddir}/src"
    echo "================================================"
    

    case ${branch} in
	9|9-exp)
	    srcbranch=HEAD
	    ;;
	*-exp)
	    srcbranch=${branch%-exp}
	    ;;
	*)
	    srcbranch=${branch}
    esac
    now=$(now)
    
    zfs snapshot a/snap/src-${srcbranch}@${now}
    zfs clone a/snap/src-${srcbranch}@${now} ${srcfs}
    zfs set mountpoint=${builddir}/src ${srcfs}

}

cleanup_client() {
    arch=$1
    branch=$2
    buildid=$3
    mach=$4
    arg=$5

    # XXX use same exclusion protocol as claim-chroot

    echo "Started cleaning up ${arch}/${branch} build ID ${buildid} on ${mach}"

    test -f ${pb}/${arch}/portbuild.${mach} && . ${pb}/${arch}/portbuild.${mach}

    # Kill off builds and clean up chroot
    ${pb}/scripts/dosetupnode ${arch} ${branch} ${buildid} ${mach} -nocopy -queue -full

    echo "Finished cleaning up ${arch}/${branch} build ID ${buildid} on ${mach}"

}

do_cleanup() {
    arch=$1
    branch=$2
    buildid=$3
    builddir=$4
    arg=$5
    shift 5

    for i in `cat ${pb}/${arch}/mlist`; do
	cleanup_client ${arch} ${branch} ${buildid} ${i} ${arg} &
    done
    wait
}

do_upload() {
    arch=$1
    branch=$2
    buildid=$3
    builddir=$4
    shift 4

    echo "Not implemented yet"
    exit 1

}

test_fs() {
    local fs=$1

    zfs list -Ht filesystem ${fs} > /dev/null 2>&1

}


get_latest_child() {
    local fs=$1

    # Return the child of this filesystem with lexicographically
    # highest name
    # 
    # XXX if a filesystem is cloned into a different prefix
    # (e.g. different arch) then we may not get the most recent one
    # but that should not happen.
    zfs get -H -o name,value origin | grep ${fs} | sort | \
    (while read zfs origin; do
	if [ "${origin%@*}" = "${fs}" ]; then
	    child=${zfs}
	fi
    done; echo ${child})
}

get_parent() {
    local fs=$1

    # Check whether this filesystem has a parent
    zfs get -H -o value origin ${fs} | \
	(read snap;
	case "${snap}" in
	    -|a/snap/*)
		;;
	    *)
		parent=${snap}
		;;
	esac; echo ${parent})
}

destroy_fs() {
    fs=$1
    buildid=$2
    subfs=$3

    fullfs=${fs}/${buildid}${subfs}
    if test_fs "${fullfs}"; then

	# We can destroy a leaf filesystem (having no dependent
	# clones) with no further effort.  However if we are
	# destroying the root of the clone tree then we have to
	# promote a child to be the new root.
	#
	# XXX In principle we might have to iterate until we end up as
	# a leaf but I don't know if this can happen.
	echo "Filesystem ${fullfs}"
	child=$(get_latest_child ${fullfs})
	parent=$(get_parent ${fullfs})
	echo "Filesystem has parent ${parent}"
	if [ -z "${child}" ]; then
	    echo "Filesystem is a leaf"
	else
	    echo "Filesystem has latest child ${child}"
	    # Check whether filesystem is root
	    if [ -z "${parent}" ]; then
		echo "Filesystem is root; promoting ${child}"
		zfs promote ${child}
		parent=$(get_parent ${fullfs})
		echo "New parent is ${parent}"
	    else
		echo "Filesystem has parent ${parent} and cannot be destroyed"
		return 1
	    fi
	fi

	# We might have snapshots on the target filesystem, e.g. if it
	# is both the head and tail of its clone tree.  They should be
	# unreferenced.
	# We have to grep because zfs list -H returns an error instead of
	# a null list if no snapshots exist
	if ! (zfs list -r -H -o name -t snapshot ${fullfs} | grep "^${fullfs}@" | xargs -n 1 zfs destroy); then
	    return 1
	fi

	# The target filesystem should now be unreferenced
	if ! zfs destroy -f "${fullfs}"; then
	    return 1
	fi

	# Destroy the origin snapshot, which should be unreferenced
	if [ ! -z "${parent}" ]; then
	    if ! zfs destroy -f ${parent}; then
		return 1
	    fi
	fi
    fi
}	

do_destroy() {
    arch=$1
    branch=$2
    buildid=$3
    builddir=$4
    shift 4

    buildid=$(resolve ${pb} ${arch} ${branch} ${buildid})
    if [ -z "${buildid}" ]; then
	echo "Invalid build ID ${buildid}"
	exit 1
    fi

    latestid=$(resolve ${pb} ${arch} ${branch} latest)
    if [ "${buildid}" = "${latestid}" ]; then
	echo "Cannot destroy latest build"
	exit 1
    fi

    destroy_fs a/portbuild/${arch} ${buildid} /ports || exit 1
    destroy_fs a/portbuild/${arch} ${buildid} /src || exit 1
    destroy_fs a/portbuild/${arch} ${buildid} || exit 1

    rmdir ${builddir}

}

# Run a command as root if running as user
# Authentication and command validation is taken care of by buildproxy
proxy_root() {
    cmd=$1
    arch=$2
    branch=$3
    buildid=$4
    builddir=$5
    shift 5
    args=$@

    id=$(id -u)
    if [ ${id} != "0" ]; then
	/var/portbuild/scripts/buildproxy-client "build ${cmd} ${arch} ${branch} ${buildid} ${args}"
	error=$?
	if [ ${error} -eq 254 ]; then
	    echo "Proxy error"
	fi
    else
	eval "do_${cmd} ${arch} ${branch} ${buildid} ${builddir} ${args}"
	error=$?
    fi

    exit ${error}
}

# Run a command as the ports-${arch} user if root
proxy_user() {
    cmd=$1
    arch=$2
    branch=$3
    buildid=$4
    builddir=$5
    shift 5
    args=$@

    id=$(id -u)
    if [ ${id} != "0" ]; then
	eval "do_${cmd} ${arch} ${branch} ${buildid} \"${builddir}\" ${args}"
	error=$?
    else
	su ports-${arch} -c "/var/portbuild/scripts/build ${cmd} ${arch} ${branch} ${buildid} \"${builddir}\" ${args}"
	error=$?
    fi

    exit ${error}
}

usage () {
  echo "usage: build <command> <arch> <branch> [<buildid>] [<options> ...]"
  exit 1
}

##################

if [ $# -lt 3 ]; then
  usage
fi

cmd=$1
arch=$2
branch=$3
shift 3

. ${pb}/${arch}/portbuild.conf
. ${pb}/scripts/buildenv

pbab=${pb}/${arch}/${branch}

validate_env ${arch} ${branch} || exit 1

# Not every command requires a buildid as arg
if [ $# -ge 1 ]; then
    buildid=$1
    shift 1

    # Most commands require a buildid that is valid on the server.  The
    # exception is "cleanup" which is cleaning up a client build that may
    # already be destroyed on the server.
    case "$cmd" in
	cleanup)
	    # Resolve symlinks but don't bail if the build doesn't exist.
	    newbuildid=$(resolve ${pb} ${arch} ${branch} ${buildid})
	    if [ ! -z "${newbuildid}" -a "${newbuildid}" != "${buildid}" ]; then
		echo "Resolved ${buildid} to ${newbuildid}"
		buildid=${newbuildid}

		builddir=$(realpath ${pbab}/builds/${buildid}/)
		# We can't rely on buildenv for this code path
	    fi
	    ;;
	*)
	    newbuildid=$(resolve ${pb} ${arch} ${branch} ${buildid})
	    if [ -z "${newbuildid}" ]; then
		echo "Build ID ${buildid} does not exist"
		exit 1
	    fi
	    if [ ${newbuildid} != ${buildid} ]; then
		echo "Resolved ${buildid} to ${newbuildid}"
		buildid=${newbuildid}
	    fi

	    builddir=$(realpath ${pbab}/builds/${buildid}/)

	    buildenv ${pb} ${arch} ${branch} ${builddir}
	    ;;
    esac
fi

# Unprivileged commands
case "$cmd" in
    list)
	do_list ${arch} ${branch} $@
	;;
    create)
	if [ -z "${buildid}" ]; then
	    buildid=$(now)
	    usage
	fi
	proxy_root create ${arch} ${branch} ${buildid} ${builddir} $@
	;;
    clone)
	if [ -z "${buildid}" ]; then
	    usage
	fi
	proxy_root clone ${arch} ${branch} ${buildid} ${builddir} $@
	;;
    portsupdate)
	if [ -z "${buildid}" ]; then
	    usage
	fi
	proxy_root portsupdate ${arch} ${branch} ${buildid} ${builddir} $@
	;;
    srcupdate)
	if [ -z "${buildid}" ]; then
	    usage
	fi
	proxy_root srcupdate ${arch} ${branch} ${buildid} ${builddir} $@
	;;
    cleanup)
	if [ -z "${buildid}" ]; then
	    usage
	fi
	# builddir may be null if cleaning up a destroyed build
	proxy_user cleanup ${arch} ${branch} ${buildid} "${builddir}" $@
	;;
    upload)
	if [ -z "${buildid}" ]; then
	    usage
	fi
	proxy_user upload ${arch} ${branch} ${buildid} ${builddir} $@
	;;
    destroy)
	if [ -z "${buildid}" ]; then
	    usage
	fi
	proxy_root destroy ${arch} ${branch} ${buildid} ${builddir} $@
	;;
    *)
	echo "Invalid command: $cmd"
	exit 1
	;;
esac
