#!/bin/bash
# Simple shell script for driving a remote cowbuilder via ssh
#
# Copyright(C) 2007, 2008, 2009, 2011, 2012, 2014, Ron <ron@debian.org>
# This script is distributed according to the terms of the GNU GPL.

set -e

#BUILDD_HOST=
#BUILDD_USER=
BUILDD_ARCH="$(dpkg-architecture -qDEB_BUILD_ARCH 2>/dev/null)"

# The 'default' dist is whatever cowbuilder is locally configured for
BUILDD_DIST="default"

INCOMING_DIR="cowbuilder-incoming"
PBUILDER_BASE="/var/cache/pbuilder"

#SIGN_KEYID=
#UPLOAD_QUEUE="ftp-master"
BUILDD_ROOTCMD="sudo"

REMOTE_SCRIPT="cowssh_it"
DEBOOTSTRAP="cdebootstrap"

for f in /etc/cowpoke.conf ~/.cowpoke .cowpoke "$COWPOKE_CONF"; do [ -r "$f" ] && . "$f"; done


get_archdist_vars() {
    _ARCHDIST_OPTIONS="RESULT_DIR BASE_PATH BASE_DIST CREATE_OPTS UPDATE_OPTS BUILD_OPTS SIGN_KEYID UPLOAD_QUEUE"
    _RESULT_DIR="result"
    _BASE_PATH="base.cow"

    for arch in $BUILDD_ARCH; do
	for dist in $BUILDD_DIST; do
	    for var in $_ARCHDIST_OPTIONS; do
		eval "val=( \"\${${arch}_${dist}_${var}[@]}\" )"

		if [ "$1" = "display" ]; then
		    case $var in
			RESULT_DIR | BASE_PATH )
			    [ ${#val[@]} -gt 0 ] || eval "val=\"$PBUILDER_BASE/$arch/$dist/\$_$var\""
			    echo "   ${arch}_${dist}_${var} = $val"
			    ;;

			*_OPTS )
			    # Don't display these if they are overridden on the command line.
			    eval "override=( \"\${OVERRIDE_${var}[@]}\" )"
			    [ ${#override[@]} -gt 0 ] || [ ${#val[@]} -eq 0 ] ||
				echo "   ${arch}_${dist}_${var} =$(printf " '%s'" "${val[@]}")"
			    ;;

			* )
			    [ ${#val[@]} -eq 0 ] || echo "   ${arch}_${dist}_${var} = $val"
			    ;;
		    esac
		else
		    case $var in
			RESULT_DIR | BASE_PATH )
			    # These are always a single value, and must always be set,
			    # either by the user or to their default value.
			    [ ${#val[@]} -gt 0 ] || eval "val=\"$PBUILDER_BASE/$arch/$dist/\$_$var\""
			    echo "${arch}_${dist}_${var}='$val'"
			    ;;

			*_OPTS )
			    # These may have zero, one, or many values which we must not word-split.
			    # They can safely remain unset if there are no values.
			    #
			    # We don't need to worry about the command line overrides here,
			    # they will be taken care of in the remote script.
			    [ ${#val[@]} -eq 0 ] ||
				echo "${arch}_${dist}_${var}=($(printf " %q" "${val[@]}") )"
			    ;;

			SIGN_KEYID | UPLOAD_QUEUE )
			    # We don't need these in the remote script
			    ;;

			* )
			    # These may have zero or one value.
			    # They can safely remain unset if there are no values.
			    [ ${#val[@]} -eq 0 ] || echo "${arch}_${dist}_${var}='$val'"
			    ;;
		    esac
		fi
	    done
	done
    done
}

display_override_vars() {
    _OVERRIDE_OPTIONS="CREATE_OPTS UPDATE_OPTS BUILD_OPTS SIGN_KEYID UPLOAD_QUEUE"

    for var in $_OVERRIDE_OPTIONS; do
	eval "override=( \"\${OVERRIDE_${var}[@]}\" )"
	[ ${#override[@]} -eq 0 ] || echo "   override: $var =$(printf " '%s'" "${override[@]}")"
    done
}


PROGNAME=${0##*/}
version() {
    echo \
"This is $PROGNAME, from the Debian devscripts package, version ###VERSION###
This code is Copyright 2007-2014, Ron <ron@debian.org>.
This program comes with ABSOLUTELY NO WARRANTY.
You are free to redistribute this code under the terms of the
GNU General Public License."
    exit 0
}

usage() {
    cat 1>&2 <<EOF

cowpoke [options] package.dsc

  Uploads a Debian source package to a cowbuilder host and builds it,
  optionally also signing and uploading the result to an incoming queue.
  The following options are supported:

   --arch="arch"         Specify the Debian architecture(s) to build for.
   --dist="dist"         Specify the Debian distribution(s) to build for.
   --buildd="host"       Specify the remote host to build on.
   --buildd-user="name"  Specify the remote user to build as.
   --create              Create the remote cowbuilder root if necessary.
   --return[="path"]     Copy results of the build to 'path'.  If path is
                         not specified, return them to the current directory.
   --no-return           Do not copy results of the build to RETURN_DIR
                         (overriding a path set for it in the config files).
   --sign="keyid"        Specify the key to sign packages with.
   --upload="queue"      Specify the dput queue to upload signed packages to.

  The current default configuration is:

   BUILDD_HOST   = $BUILDD_HOST
   BUILDD_USER   = $BUILDD_USER
   BUILDD_ARCH   = $BUILDD_ARCH
   BUILDD_DIST   = $BUILDD_DIST
   RETURN_DIR    = $RETURN_DIR
   SIGN_KEYID    = $SIGN_KEYID
   UPLOAD_QUEUE  = $UPLOAD_QUEUE

  The expected remote paths are:

   INCOMING_DIR  = $INCOMING_DIR
   PBUILDER_BASE = ${PBUILDER_BASE:-/}

$(get_archdist_vars display)
$(display_override_vars)

  The cowbuilder image must have already been created on the build host
  and the expected remote paths must already exist if the --create option
  is not passed.  You must have ssh access to the build host as BUILDD_USER
  if that is set, else as the user executing cowpoke or a user specified
  in your ssh config for '$BUILDD_HOST'.
  That user must be able to execute cowbuilder as root using '$BUILDD_ROOTCMD'.

EOF

    exit $1
}


for arg; do
    case "$arg" in
	--arch=*)
	    BUILDD_ARCH="${arg#*=}"
	    ;;

	--dist=*)
	    BUILDD_DIST="${arg#*=}"
	    ;;

	--buildd=*)
	    BUILDD_HOST="${arg#*=}"
	    ;;

	--buildd-user=*)
	    BUILDD_USER="${arg#*=}"
	    ;;

	--create)
	    CREATE_COW="yes"
	    ;;

	--return=*)
	    RETURN_DIR="${arg#*=}"
	    ;;

	--return)
	    RETURN_DIR=.
	    ;;

	--no-return)
	    RETURN_DIR=
	    ;;

	--dpkg-opts=*)
	    # This one is a bit tricky, given the combination of the calling convention here,
	    # the calling convention for cowbuilder, and the behaviour of things that might
	    # pass this option to us.  Some things, like when we are called from the gitpkg
	    # hook using options from git-config, will preserve any quoting that was used in
	    # the .gitconfig file, which is natural for anyone to want to use in a construct
	    # like: options = --dpkg-opts='-uc -us -j6'.  People are going to cringe if we
	    # tell them they must not use quotes there no matter how much it may 'make sense'
	    # if you know too much about the internals.  And it will only get worse when we
	    # then tell them they must quote it like that if they type it directly in their
	    # shell ...
	    #
	    # So we do the only thing that seems sensible, and try to Deal With It here.
	    # If the outermost characters are paired quotes, we manually strip them off.
	    # We don't want to let the shell do quote removal, since that might change a
	    # part of this which we don't want modified.
	    # We collect however many sets of those we are passed in an array, which we'll
	    # then combine back into a single argument at the final point of use.
	    #
	    # Which _should_ DTRT for anyone who isn't trying to blow this up deliberately
	    # and maybe will still do it for them too in spite of their efforts. But unless
	    # someone finds a sensible case this fails on, I'm not going to cry over people
	    # who want to stuff up their own system with input they created themselves.
	    val=${arg#*=}
	    [[ $val == \'*\' || $val == \"*\" ]] && val=${val:1:-1}
	    DEBBUILDOPTS+=( "$val" )
	    ;;

	--create-opts=*)
	    OVERRIDE_CREATE_OPTS+=( "${arg#*=}" )
	    ;;

	--update-opts=*)
	    OVERRIDE_UPDATE_OPTS+=( "${arg#*=}" )
	    ;;

	--build-opts=*)
	    OVERRIDE_BUILD_OPTS+=( "${arg#*=}" )
	    ;;

	--sign=*)
	    OVERRIDE_SIGN_KEYID=${arg#*=}
	    ;;

	--upload=*)
	    OVERRIDE_UPLOAD_QUEUE=${arg#*=}
	    ;;

	*.dsc)
	    DSC="$arg"
	    ;;

	--help)
	    usage 0
	    ;;

	--version)
	    version
	    ;;

	*)
	    echo "ERROR: unrecognised option '$arg'"
	    usage 1
	    ;;
    esac
done

if [ -z "$REMOTE_SCRIPT" ]; then
    echo "No remote script name set.  Aborted."
    exit 1
fi
if [ -z "$DSC" ]; then
    echo "ERROR: No package .dsc specified"
    usage 1
fi
if ! [ -r "$DSC" ]; then
    echo "ERROR: '$DSC' not found."
    exit 1
fi
if [ -z "$BUILDD_ARCH" ]; then
    echo "No BUILDD_ARCH set.  Aborted."
    exit 1
fi
if [ -z "$BUILDD_HOST" ]; then
    echo "No BUILDD_HOST set.  Aborted."
    exit 1
fi
if [ -z "$BUILDD_ROOTCMD" ]; then
    echo "No BUILDD_ROOTCMD set.  Aborted."
    exit 1
fi
if [ -e "$REMOTE_SCRIPT" ]; then
    echo "$REMOTE_SCRIPT file already exists and will be overwritten."
    echo -n "Do you wish to continue (Y/n)? "
    read -e yesno
    case "$yesno" in
	N* | n*)
	    echo "Ok, bailing out."
	    echo "You should set the REMOTE_SCRIPT variable to some other value"
	    echo "if this name conflicts with something you already expect to use"
	    exit 1
	    ;;
	*) ;;
    esac
fi

[ -z "$BUILDD_USER" ] || BUILDD_USER="$BUILDD_USER@"

PACKAGE="$(basename $DSC .dsc)"
DATE="$(date +%Y%m%d 2>/dev/null)"


cat > "$REMOTE_SCRIPT" <<-EOF
	#!/bin/bash
	# cowpoke generated remote worker script.
	# Normally this should have been deleted already, you can safely remove it now.

	compare_changes() {
	    p1="\${1%_*.changes}"
	    p2="\${2%_*.changes}"
	    p1="\${p1##*_}"
	    p2="\${p2##*_}"

	    dpkg --compare-versions "\$p1" gt "\$p2"
	}

	$(get_archdist_vars)

	for arch in $BUILDD_ARCH; do
	  for dist in $BUILDD_DIST; do

	    echo " ------- Begin build for \$arch \$dist -------"

	    CHANGES="\$arch.changes"
	    LOGFILE="$INCOMING_DIR/build.${PACKAGE}_\$arch.\$dist.log"
	    UPDATELOG="$INCOMING_DIR/cowbuilder-\${arch}-\${dist}-update-log-$DATE"
	    eval "RESULT_DIR=\"\\\$\${arch}_\${dist}_RESULT_DIR\""
	    eval "BASE_PATH=\"\\\$\${arch}_\${dist}_BASE_PATH\""
	    eval "BASE_DIST=\"\\\$\${arch}_\${dist}_BASE_DIST\""
	    eval "CREATE_OPTS=( \"\\\${\${arch}_\${dist}_CREATE_OPTS[@]}\" )"
	    eval "UPDATE_OPTS=( \"\\\${\${arch}_\${dist}_UPDATE_OPTS[@]}\" )"
	    eval "BUILD_OPTS=( \"\\\${\${arch}_\${dist}_BUILD_OPTS[@]}\" )"

	    [ -n "\$BASE_DIST" ]                  || BASE_DIST=\$dist
	    [ ${#OVERRIDE_CREATE_OPTS[@]} -eq 0 ] || CREATE_OPTS=("${OVERRIDE_CREATE_OPTS[@]}")
	    [ ${#OVERRIDE_UPDATE_OPTS[@]} -eq 0 ] || UPDATE_OPTS=("${OVERRIDE_UPDATE_OPTS[@]}")
	    [ ${#OVERRIDE_BUILD_OPTS[@]}  -eq 0 ] || BUILD_OPTS=("${OVERRIDE_BUILD_OPTS[@]}")
	    [ ${#DEBBUILDOPTS[*]} -eq 0 ]         || DEBBUILDOPTS=("--debbuildopts" "${DEBBUILDOPTS[*]}")


	    # Sort the list of old changes files for this package to try and
	    # determine the most recent one preceding this version.  We will
	    # debdiff to this revision in the final sanity checks if one exists.
	    # This is adapted from the insertion sort trickery in git-debimport.

	    OLD_CHANGES="\$(find "\$RESULT_DIR/" -maxdepth 1 -type f \\
	                         -name "${PACKAGE%%_*}_*_\$CHANGES" 2>/dev/null \\
	                    | sort 2>/dev/null)"
	    P=( \$OLD_CHANGES )
	    count=\${#P[*]}

	    for(( i=1; i < count; ++i )) do
	        j=i
	        #echo "was \$i: \${P[i]}"
	        while ((\$j)) && compare_changes "\${P[j-1]}" "\${P[i]}"; do ((--j)); done
	        ((i==j)) || P=( \${P[@]:0:j} \${P[i]} \${P[j]} \${P[@]:j+1:i-(j+1)} \${P[@]:i+1} )
	    done
	    #for(( i=1; i < count; ++i )) do echo "now \$i: \${P[i]}"; done

	    OLD_CHANGES=
	    for(( i=count-1; i >= 0; --i )) do
	        if [ "\${P[i]}" != "\$RESULT_DIR/${PACKAGE}_\$CHANGES" ]; then
	            OLD_CHANGES="\${P[i]}"
	            break
	        fi
	    done


	    set -eo pipefail

	    if ! [ -e "\$BASE_PATH" ]; then
	        if [ "$CREATE_COW" = "yes" ]; then
	            mkdir -p "\$RESULT_DIR"
	            mkdir -p "\$(dirname \$BASE_PATH)"
	            mkdir -p "$PBUILDER_BASE/aptcache"
	            $BUILDD_ROOTCMD cowbuilder --create --distribution \$BASE_DIST  \\
	                                       --basepath "\$BASE_PATH"             \\
	                                       --aptcache "$PBUILDER_BASE/aptcache" \\
	                                       --debootstrap "$DEBOOTSTRAP"         \\
	                                       --debootstrapopts --arch="\$arch"    \\
	                                       "\${CREATE_OPTS[@]}"                 \\
	            2>&1 | tee "\$UPDATELOG"
	        else
	            echo "SKIPPING \$dist/\$arch build, '\$BASE_PATH' does not exist" | tee "\$LOGFILE"
	            echo "         use the cowpoke --create option to bootstrap a new build root" | tee -a "\$LOGFILE"
	            continue
	        fi
	    elif ! [ -e "\$UPDATELOG" ]; then
	        $BUILDD_ROOTCMD cowbuilder --update --distribution \$BASE_DIST  \\
	                                   --basepath "\$BASE_PATH"             \\
	                                   --aptcache "$PBUILDER_BASE/aptcache" \\
	                                   --autocleanaptcache                  \\
	                                   "\${UPDATE_OPTS[@]}"                 \\
	        2>&1 | tee "\$UPDATELOG"
	    fi
	    $BUILDD_ROOTCMD cowbuilder --build --basepath "\$BASE_PATH"      \\
	                               --aptcache "$PBUILDER_BASE/aptcache"  \\
	                               --buildplace "$PBUILDER_BASE/build"   \\
	                               --buildresult "\$RESULT_DIR"          \\
	                               "\${DEBBUILDOPTS[@]}"                 \\
	                               "\${BUILD_OPTS[@]}"                   \\
	                               "$INCOMING_DIR/$(basename $DSC)" 2>&1 \\
	    | tee "\$LOGFILE"

	    set +eo pipefail


	    echo >> "\$LOGFILE"
	    echo "lintian \$RESULT_DIR/${PACKAGE}_\$CHANGES" >> "\$LOGFILE"
	    lintian "\$RESULT_DIR/${PACKAGE}_\$CHANGES" 2>&1 | tee -a "\$LOGFILE"

	    if [ -n "\$OLD_CHANGES" ]; then
	        echo >> "\$LOGFILE"
	        echo "debdiff \$OLD_CHANGES ${PACKAGE}_\$CHANGES" >> "\$LOGFILE"
	        debdiff "\$OLD_CHANGES" "\$RESULT_DIR/${PACKAGE}_\$CHANGES" 2>&1 \\
	        | tee -a "\$LOGFILE"
	    else
	        echo >> "\$LOGFILE"
	        echo "No previous packages for \$dist/\$arch to compare" >> "\$LOGFILE"
	    fi

	  done
	done

EOF
chmod 755 "$REMOTE_SCRIPT"


if ! dcmd rsync -vP $DSC "$REMOTE_SCRIPT" "$BUILDD_USER$BUILDD_HOST:$INCOMING_DIR";
then
    dcmd scp $DSC "$REMOTE_SCRIPT" "$BUILDD_USER$BUILDD_HOST:$INCOMING_DIR"
fi

ssh -t "$BUILDD_USER$BUILDD_HOST" "\"$INCOMING_DIR/$REMOTE_SCRIPT\" && rm -f \"$INCOMING_DIR/$REMOTE_SCRIPT\""

echo
echo "Build completed."

for arch in $BUILDD_ARCH; do
    CHANGES="$arch.changes"
    for dist in $BUILDD_DIST; do

	sign_keyid=$OVERRIDE_SIGN_KEYID
	[ -n "$sign_keyid" ] || eval "sign_keyid=\"\$${arch}_${dist}_SIGN_KEYID\""
	[ -n "$sign_keyid" ] || sign_keyid="$SIGN_KEYID"
	[ -n "$sign_keyid" ] || continue

	eval "RESULT_DIR=\"\$${arch}_${dist}_RESULT_DIR\""
	[ -n "$RESULT_DIR" ] || RESULT_DIR="$PBUILDER_BASE/$arch/$dist/result"

	_desc="$dist/$arch"
	[ "$dist" != "default" ] || _desc="$arch"

	while true; do
	    echo -n "Sign $_desc $PACKAGE with key '$sign_keyid' (yes/no)? "
	    read -e yesno
	    case "$yesno" in
		YES | yes)
		    debsign "-k$sign_keyid" -r "$BUILDD_USER$BUILDD_HOST" "$RESULT_DIR/${PACKAGE}_$CHANGES"

		    upload_queue=$OVERRIDE_UPLOAD_QUEUE
		    [ -n "$upload_queue" ] || eval "upload_queue=\"\$${arch}_${dist}_UPLOAD_QUEUE\""
		    [ -n "$upload_queue" ] || upload_queue="$UPLOAD_QUEUE"

		    if [ -n "$upload_queue" ]; then
			while true; do
			    echo -n "Upload $_desc $PACKAGE to '$upload_queue' (yes/no)? "
			    read -e upload
			    case "$upload" in
				YES | yes)
				    ssh "$BUILDD_USER$BUILDD_HOST" \
					"cd \"$RESULT_DIR/\" && dput \"$upload_queue\" \"${PACKAGE}_$CHANGES\""
				    break 2
				    ;;

				NO | no)
				    echo "Package upload skipped."
				    break 2
				    ;;
				*)
				    echo "Please answer 'yes' or 'no'"
				    ;;
			    esac
			done
		    fi
		    break
		    ;;

		NO | no)
		    echo "Package signing skipped."
		    break
		    ;;
		*)
		    echo "Please answer 'yes' or 'no'"
		    ;;
	    esac
	done
    done
done

if [ -n "$RETURN_DIR" ]; then
    for arch in $BUILDD_ARCH; do
      CHANGES="$arch.changes"
      for dist in $BUILDD_DIST; do

	eval "RESULT_DIR=\"\$${arch}_${dist}_RESULT_DIR\""
	[ -n "$RESULT_DIR" ] || RESULT_DIR="$PBUILDER_BASE/$arch/$dist/result"


	cache_dir="./cowpoke-return-cache"
	mkdir -p $cache_dir

	scp "$BUILDD_USER$BUILDD_HOST:$RESULT_DIR/${PACKAGE}_$CHANGES" $cache_dir

	for f in $(cd $cache_dir && dcmd ${PACKAGE}_$CHANGES); do
	    RESULTS="$RESULTS $RESULT_DIR/$f"
	done

	rm -f $cache_dir/${PACKAGE}_$CHANGES
	rmdir $cache_dir


	if ! rsync -vP "$BUILDD_USER$BUILDD_HOST:$RESULTS" "$RETURN_DIR" ;
	then
	    scp "$BUILDD_USER$BUILDD_HOST:$RESULTS" "$RETURN_DIR"
	fi

      done
    done
fi

rm -f "$REMOTE_SCRIPT"

# vi:sts=4:sw=4:noet:foldmethod=marker
