#!/bin/bash

## Copyright (C) 2018 Robert Krawitz
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2, or (at your option)
## any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program.  If not, see <https://www.gnu.org/licenses/>.
##
## Build release tarball

# Note that the shebang line is explicit here, not indirected through
# autoconf.  This allows the script to be run in a non-initialized workspace.
# We also require this script to be run in the root of a workspace so that
# it can find the script directory to get the version without having to be
# autotool-processed.

function xtput() {
    if [[ -z ${STP_TEST_RECURSIVE:-} && -n ${TERM:-} && ${TERM:-} != dumb ]] ; then
	tput "$@"
    fi
}

set -u

# shellcheck disable=SC2155
{
declare -r tbold="$(xtput bold)"
declare -r tred="$(xtput setaf 1)$tbold"
declare -r tyellow="$(xtput setaf 3)$tbold"
declare -r tgreen="$(xtput setaf 2)$tbold"
declare -r tpurple="$(xtput setaf 5)$tbold"
declare -r tblue=$(xtput setaf 4)
declare -r tcyan=$(xtput setaf 6)
declare -r treset=$(xtput sgr0)
}
declare -i exitstatus=1
declare -i stepstatus=-1
declare currentmodule=
declare -a failedmodules=()
declare top_log
function fatal() {
    echo "${tred}FATAL: $*${treset}"
    exit 1
}

# shellcheck disable=SC2155
declare GIT=$(type -p git)
[[ -n $GIT ]] || fatal "Can't find git"

"$GIT" rev-parse --show-toplevel >/dev/null 2>&1 || fatal "Current directory is not a git workspace"
declare rootdir
rootdir="$("$GIT" rev-parse --show-toplevel 2>/dev/null)"
[[ -n $rootdir ]] || fatal "Can't find workspace root"
cd "$rootdir" || fatal "Can't cd to workspace root ($rootdir)"
[[ -s ChangeLog.pre-5.2.11 ]] || fatal "$rootdir does not appear to be a Gutenprint tree"

# shellcheck disable=SC2155
declare MAKE=$(type -p make)
[[ -n $MAKE ]] || fatal "Can't find make"

# shellcheck disable=SC2155
declare -r ROOT=$(pwd)
declare tmpfile=/dev/null
export STP_PARALLEL=${STP_PARALLEL:-$("$ROOT/scripts/count-cpus")}
declare counter=1
declare git_dirty=
declare build_type=release
[[ -n ${STP_BUILD_SNAPSHOT:-} ]] && build_type=snapshot

# This can't simply be a constant because scripts/gversion might
# not exist (or may be incorrect) prior to autogen being run.
function pkg_version() {
    "$ROOT/scripts/gversion" pkg
}

function pkg_tag() {
    # shellcheck disable=SC2155
    local version=$(pkg_version)
    echo "gutenprint-${version//./_}"
}

# Clean up any trailing whitespace.

function preflight() {
    # shellcheck disable=SC2155
    local trailing_ws="$("$GIT" grep -Il '[	 ]$')"
    if [[ -n $trailing_ws ]] ; then
	console_log "*** ERROR: The following files have trailing whitespace:"
	console_log "$trailing_ws"
	return 2
    fi
    return 0
}

# Git pre-checks (not version-specific)

function check_git() {
    "$GIT" fetch
    local gstatus=0

    # Check for uncommitted files.
    if [[ -n $("$GIT" status -uno --porcelain) ]] ; then
	console_log "*** ERROR: Uncommitted changes in repository:"
	"$GIT" status -uno --porcelain | console_log
	gstatus=2
    fi

    # Ensure that the workspace is up to date (git status -uno
    # --porcelain -b |grep -v ahead is empty -- it's OK to be ahead,
    # but not behind) and that we don't need to rebase (no merges.
    # Also check that we haven't diverged.

    # shellcheck disable=SC2155
    local ahead=$("$GIT" rev-list '@{u}..@')
    # shellcheck disable=SC2155
    local behind=$("$GIT" rev-list '@..@{u}')
    if [[ -n $ahead && -n $behind ]] ; then
	# Oops!  Both ahead *and* behind remote.  Really bad news!
	console_log "*** ERROR: HEAD and remote have diverged!"
	console_log "***        Please merge and rebase all changes!"
	return 2
    elif [[ -n $behind ]] ; then
	# We're behind.  Not good.
	console_log "*** ERROR: Behind remote by $(wc -w <<< "$behind") commits."
	return 2
    elif [[ -n $ahead ]] ; then
	# We're ahead.  That's OK as long as there are no merge commits.
	local merges=0
	for h in $ahead ; do
	    (( $("$GIT" rev-parse "$h^@" |wc -w) > 1 )) && merges=$((merges + 1))
	done
	console_log "*** Warning: Ahead of remote."
	if (( merges > 0 )) ; then
	    (( merges != 1 )) && pl=s
	    console_log "*** ERROR: $merges merge${pl:-} between HEAD and remote"
	    return 2
	fi
    fi
    return $gstatus
}

# Run autogen.sh to ensure that we're using default build settings
# Everything else depends on this.

function run_target() {
    make "$1" && return 0
    local lstatus=$?
    console_log "*** ERROR: ${3:+$3 }make $1 failed"
    (( lstatus >= 127 )) && return $lstatus
    return "${2:-2}"
}

function run_maintainer_clean() {
    if [[ -f Makefile ]] ; then
	run_target maintainer-clean
    else
	return 0
    fi
}

function run_autogen() {
    # shellcheck disable=SC2086
    ./autogen.sh ${STP_CONFIG_ARGS:-} && return 0
    console_log "*** FATAL: autogen failed!"
    return 1
}

function colorize() {
    sed \
	-e "s/\*\*\* \(FATAL\|ERROR\):\(.*\)/${tred}*** \1:\2${treset}/" \
	-e "s/\*\*\* Warning:\(.*\)/${tyellow}*** Warning:\1${treset}/"
}

function run_clean() {
    run_target clean
}

function run_build() {
    run_target "${STP_PARALLEL:+-j$STP_PARALLEL}" 1
}

# Same as above, without make clean if we know we're in a clean
# environment (e. g. CI)
function run_build_fresh() {
    # shellcheck disable=SC2086
    ./autogen.sh ${STP_CONFIG_ARGS:-} && make "${STP_PARALLEL:+-j$STP_PARALLEL}" && return 0
    console_log "*** FATAL preliminary build failed!"
    return 1
}

# Git check tag.  This can't be run until after the build, because we
# don't have the version available until autogen.

function check_git_tag() {
    # Make sure that the tag that we're going to want to apply isn't
    # already present.
    [[ $build_type != release ]] && return 0
    if [[ -n $("$GIT" show-ref "refs/tags/$(pkg_tag)") ]] ; then
	console_log "*** ERROR: Tag named $(pkg_tag) is already present"
	return 2
    fi
}

function _cleanup_test_repo() {
    if [[ -d $TESTREPO ]] ; then
	rm -rf -- "$TESTREPO"
    fi
}

# Check that we can build a clone of this workspace
function _check_git_builds() {
    # shellcheck disable=SC2155
    export TESTREPO=$(mktemp -d "/tmp/stpbuild.XXXXXXXX")
    trap _cleanup_test_repo EXIT SIGHUP SIGINT SIGQUIT SIGTERM
    # shellcheck disable=SC2155
    local rev=$("$GIT" rev-parse @)
    cwd=$(pwd -P)
    [[ -n $cwd ]] || {
	console_log "*** ERROR: Can't find directory!"
	return 2
    }
    cd "$TESTREPO" || {
	console_log "*** ERROR: Can't cd to test repo directory $cwd!"
	(( $? >= 127 )) && return 127
	return 2
    }
    "$GIT" clone "$cwd" . || {
    	console_log "*** ERROR: Unable to clone repo"
	(( $? >= 127 )) && return 127
	return 2
    }
    "$GIT" checkout "$rev" || {
    	console_log "*** ERROR: Unable to check out rev $rev"
	(( $? >= 127 )) && return 127
	return 2
    }
    STP_TEST_RECURSIVE=$((${STP_TEST_RECURSIVE?-0}+1)) STP_LOG_NO_SUBDIR=1 STP_LOG_DIR=$STP_TEST_LOG_PREFIX scripts/build-release preflight run_autogen run_build run_distcheck_minimal || {
	console_log "*** ERROR: Repo build failed!"
	(( $? >= 127 )) && return 127
	return 2
    }
}

function check_git_builds() {
    (_check_git_builds)
}

# Run make valgrind-minimal.
#
#    This does a *very* limited set of valgrind checks, running
#    testpattern and rastertogutenprint on 9 (currently) selected
#    printers.  It takes about 30 seconds on my laptop.  Smoketest and
#    all.

function run_valgrind_minimal() {
    run_target check-valgrind-minimal
}

function run_valgrind_fast() {
    run_target check-valgrind-fast
}

function run_check_minimal() {
    run_target check-minimal
}

function run_check_fast() {
    run_target check-fast
}

# Run make distcheck-fast.
#
#    This actually builds the tarball, unpacks the tarball, builds it
#    out of tree, runs a short set of tests against it, does a local
#    make install, followed by make uninstall, and makes sure no
#    debris is left around.  This runs configure with all default
#    arguments, so it is testing dynamically linked executables.
#
#    The particular tests it runs are:
#
#    - Conformance tests all non-translated non-simplified PPD files
#      and distinct global ones.
#
#    - Runs test-rastertogutenprint on distinct printers, with fast
#      options (minimum paper size, lowest resolution, very fast
#      dithering).
#
#    - Runs run-testpattern-2:
#
#      + Distinct printers, fast options
#
#      + Selected printers, with cross product of input mode (and bit
#        depth), color correction, ink type, and use gloss.
#
#    It also has the property of maybe updating the .po files.  These
#    will later need to be committed and included in the tag.  So we
#    have to do our check for uncommitted bits prior to this.
#
#    It has not escaped me that this could be part of a CI testing
#    process.  I don't know if Sourceforge has the necessary gittage
#    (as GitHub does) to allow a merge bot to run something like this
#    and only merge to the main repository if this suite passes.
#
#    The reason for the distcheck-fast is so that if something stupid
#    goes wrong it gets caught quickly.  It takes about 270 seconmds
#    on my laptop.  It would be Kind Of Annoying to spend hours
#    testing only to find out that something's not handling destdir
#    correctly or make clean isn't removing something.
#
#    Note that this can't be combined with valgrind, since this builds
#    dynamic executables which can't conveniently be valground since
#    they're actually shell scripts.
#
#    There's now an even faster check, distcheck-minimal, that only
#    tests a handful of printers.  It takes about 50 seconds to run.
#    But that's really most useful for testing the distcheck
#    apparatus.
#
# So far we're at just over 5 minutes on a Skylake Xeon E3-1505Mv5,
# which isn't too bad for a prerelease smoke test.  The rest of this
# takes a lot longer.

function run_distcheck_fast() {
    run_target distcheck-fast
}

# Run make check-valgrind
#
# This is slow.  It tests only unique printers, and a lot of extra
# combinations with a few printers, all using fast options.  It
# uses both CUPS and run-testpattern-2 testing.  However, it's
# essentially embarrassingly parallel.
#
# I'd like not to go too long without running it, as it's easy for
# things to make their way in.  For CI purposes, if we ever go
# there, like to find a happy medium.

function run_valgrind() {
    run_target check-valgrind
}

# Run make check-full
#
#    This one I'm not sure of; do we need this or is this well enough
#    covered by the combination of distcheck-fast and check-valgrind?
#    It does take a while, but I haven't benchmarked it lately.
#
#    - Conformance test all PPD files
#
#    - Run test-rastertogutenprint on all printers, with default options
#
#    - Runs run-testpattern-2:
#
#      + Distinct printers, default options
#
#      + All printers, fast options
#
#      + Distinct printers, fast options, with cross product of input
#        mode (and bit depth), color correction, ink type, and use
#        gloss.
#
#    IIRC this takes 60-90 minutes on my laptop, but again, it
#    parallelizes very well.

function run_full() {
    run_target check-full
}

# Run make checksums-release to generate a new regression file.
#
# The problem here is what do we require for the release build.  Do
# we require a clean regression run (other than added
# printers/modes)?  There are legitimate reasons for changing, and
# having to rerun the procedure because the release engineer forgot a
# command line option is a bit harsh.  Something better might be to
# simply record changes unless there's an outright failure here, and
# let those be reviewed.
#
# For CI purposes, the default might be to require no changes, with
# human intervention if there are.
#
# This takes about 30 minutes on my laptop.  This is extremely
# scalable.  Give us a really big machine instance to run it on, this
# will run really fast.

function run_checksums() {
    make checksums
    local lstatus=$?
    if (( lstatus == 0 )) ; then
	# shellcheck disable=SC2155
	local csum_file="src/testpattern/Checksums/sums.$(pkg_version).zpaq"
	if [[ ! -f $csum_file ]] ; then
	    console_log "*** ERROR: Can't find new checksums file $csum_file"
	    (( lstatus > 127 )) && return $lstatus
	    return 2
	fi
	cp -p "$csum_file" "$ARTIFACTDIR"
	return 0
    fi
    console_log "*** ERROR: make checksums failed"
    (( lstatus > 127 )) && return $lstatus
    return 2
}

# Prep the release

function git_prep_release() {
    # .po files might have changed; nothing else should have!
    # Add any of those changed files.
    if [[ $build_type == release ]] ; then
	"$GIT" add -u || return 1
	# Add the checksums file.
	# TBD whether to do this for snapshots.  The file's not very big,
	# but it's completely incompressible!
	"$GIT" add -f "src/testpattern/Checksums/sums.$(pkg_version).zpaq" || return 1
	# Commit this change
	"$GIT" commit -m"Gutenprint $(pkg_version) release" || return 1
    else
	# Don't update the .po files for every snapshot.
	echo "Cleaning up .po files"
	"$GIT" checkout -- po
    fi
    # Shouldn't have anything left after this.
    if "$GIT" status -uno --porcelain |grep -q -E -v 'po/.*\.po' ; then
	console_log "*** ERROR: Unexpected untracked files:"
	"$GIT" status -uno --porcelain |grep -E -v 'po/.*\.po' | console_log
	return 1
    fi
    # Apply the tag.  Ideally we should sign the tag too.
    # But don't tag snapshot builds.
    [[ $build_type != release ]] && return 0
    "$GIT" tag -a "$(pkg_tag)" -m "Gutenprint $(pkg_version) release" || return 1
}

# make distcheck-minimal
#
# We have to rebuild the tarball in any event here, so that we pick up
# the tag (to get a correct change log) and updated .po files.
# A minimal distcheck only takes about a minute; we might as well
# do a final sanity check.

function run_distcheck_minimal() {
    run_target distcheck-minimal 1 Final
}

function run_check_minimal() {
    run_target check-minimal
}

#  Save away build
function save_build_artifacts() {
    # shellcheck disable=SC2155
    local tarball="$(pkg_version).tar.xz"
    if [[ -s $tarball ]] ; then
	cp -p "$tarball" "$ARTIFACTDIR"
    else
	echo "Cannot find $tarball"
	return 1
    fi
}

# Final release prep

function finis() {
    local extra_verbiage=
    [[ $build_type == release ]] && {
	STP_DATA_PATH=src/xml test/gen-printer-list > "printer-list.$(pkg_version)" || return 1
	extra_verbiage=$(cat <<EOF

  * Update the web site

  * Merge the updated printer list in ${tcyan}printer-list.$(pkg_version)${tpurple}
    into p_Supported_Printers.php and upload that to the web site
EOF
)
}
    console_log <<EOF
${tpurple}
================================================================
Remainder to be done manually:

  * git push

  * Upload the tarball (${tcyan}gutenprint-$(pkg_version).tar.xz${tpurple})
$extra_verbiage
================================================================
$treset
EOF
    return 0
}

################################################################
#
# Runtime
#
################################################################

# Note that if we change the format of this timestamp we have to
# change console_log if the width changes.
#
# Unfortunately the shell built-in printf can't specify UTC.
function stamp() {
    printf '%(%Y-%m-%dT%H:%M:%S%z)T' -1
}

function date_sec() {
    printf '%(%s)T' -1
}

function report_progress() {
    local outst="RUNNING[$1]: "
    shift
    # shellcheck disable=SC2046
    echo $([[ -z ${BUILD_VERBOSE:-} ]] && echo '' -n) ">>> $(stamp) $outst $*"
}

# This allows us to log to multiple outputs, including stdout and
# (where available) file descriptors.  Ideally we'd be able to build a
# pipeline and eval it, but it's not clear that that's possible.
function log1() {
    if [[ $# -eq 0 || ($# == 1 && $1 == -) ]] ; then
	exec cat
    else
	local dest="$1"
	shift
	# stdout needs to come last, because we just want to send data
	# to stdout rather than teeing off or explicitly going to a file.
	if [[ $dest == - && $# -gt 0 ]] ; then
	    # Protect against someone inadvertently specifying '-' twice!
	    # shellcheck disable=SC2046
	    log1 "$@" $([[ $1 == - ]] || echo -)
	elif [[ $dest == -* ]] ; then
	    dest=${dest:1}
	    destdir=${dest%/*}
	    [[ -n $destdir && ! -d $destdir ]] && mkdir -p "$destdir"
	    if (( $# == 0 )) ; then
		exec cat > "$dest"
	    elif [[ $* == - ]] ; then
		exec tee "$dest"
	    else
		exec tee "$dest" | log1 "$@"
	    fi
	else
	    destdir=${dest%/*}
	    [[ -n $destdir && ! -d $destdir ]] && mkdir -p "$destdir"
	    if (( $# == 0 )) ; then
		exec cat >> "$dest"
	    elif [[ $* == - ]] ; then
		exec tee -a "$dest"
	    else
		exec tee -a "$dest" | log1 "$@"
	    fi
	fi
    fi
}

function log() {
    (log1 "$@" ${BUILD_VERBOSE:+-})
}

function log_top1() {
    log "$top_log" -
}

function red() {
    sed -e "s/^/${tred}/" -e "s/$/${treset}/"
}

function green() {
    sed -e "s/^/${tgreen}/" -e "s/$/${treset}/"
}

function log_top() {
    if [[ -n "$*" ]] ; then
	log_top1 <<< "$*"
    else
	log_top1
    fi
}

# Log the output to the console as well as the master log file and the
# per-operation log file.
#
# fd#4 (/dev/fd/4 -- let's hope we're building the package on
#      a system that supports /dev/fd, but linux does)
#      in the operation gets tied to stderr
#
# Note that fd#3 is used by lower levels
#
# Then we timestamp the data and send it to the top-level log (which
# is not normally timestamped).
#
# Finally, we remove the existing timestamp (which relies upon the timestamp
# format, ugh) and send it to stdout where it gets picked up and timestamped
# again.
function console_log1() {
    if [[ -n ${STP_TEST_RECURSIVE:-} ]] ; then
	tee >(exec cat 1>&4) >(timestamp | log_top | cut -c26-)
    else
	timestamp | log_top - | cut -c26-
    fi
}

function console_log_immediate1() {
    case "${STP_TEST_RECURSIVE:-0}" in
	2)
	    tee >(exec cat 1>&6) >(exec cat 1>&5) >(exec cat 1>&4) >(timestamp | log_top | cut -c26-)
	    ;;
	1)
	    tee >(exec cat 1>&5) >(exec cat 1>&4) >(timestamp | log_top | cut -c26-)
	    ;;
	*)
	    tee >(exec cat 1>&2) >(timestamp | log_top | cut -c26-)
	    ;;
    esac
}

function console_log() {
    if [[ -n "$*" ]] ; then
	console_log1 <<< "$*"
    else
	console_log1
    fi
}

function console_log_immediate() {
    if [[ -n "$*" ]] ; then
	console_log_immediate1 <<< "$*"
    else
	console_log_immediate1
    fi
}

function time_delta() {
    local -i i=$((${2:-0} - ${1:-0}))
    printf "%d:%02d:%02d" $((i / 3600)) $(((i % 3600) / 60)) $((i % 60))
}

function report_step_status() {
    (( stepstatus == -1 )) && return
    [[ -z ${BUILD_VERBOSE:-} ]] && {
	local ststatus=OK
	local stcolor="${tgreen}"
	if (( stepstatus)) ; then
	    ststatus=FAILED
	    stcolor="${tred}"
	    if (( stepstatus == 126 )) ; then
		ststatus="NOT FOUND"
		stcolor="${tpurple}"
	    elif (( stepstatus >= 127 )) ; then
		ststatus=INTERRUPTED
	    fi
	fi
	printf "$stcolor%$((longest_op - ${#op} ))s $ststatus$treset\n"
    }
    [[ -n $currentmodule && $stepstatus -ne 0 ]] && failedmodules+=("$currentmodule")
    currentmodule=
    if [[ -f $tmpfile ]] ; then
	[[ -s $tmpfile ]] && colorize < "$tmpfile"
	rm -f -- "$tmpfile"
    fi
    stepstatus=-1
}

# shellcheck disable=SC2155
function finish() {
    local status=$exitstatus
    local etime=$(date_sec)
    local estamp=$(stamp)
    report_step_status
    if [[ $status != 0 || -n ${failedmodules[*]:-} ]] ; then
	log_top "The following modules failed:"
	log_top "$(printf "    %s\n" "${failedmodules[@]:-}")"
	log_top "*** Gutenprint $build_type build FAILED at $estamp ($(time_delta "$stime" "$etime"))" |red
    else
	log_top "*** Gutenprint $build_type build completed at $estamp ($(time_delta "$stime" "$etime"))" |green
    fi
    log "$top_log" <<< "================================================================"
    if [[ -n ${TRAVIS_MODE:-} ]] ; then
	# We really don't want the termination message from the deadman
	exec 3>&2 2>/dev/null
	kill %travis_deadman
	wait %travis_deadman
	exec 2>&3 3>&-
    fi
    trap - EXIT
    exit "$status"
}

# Travis times out if there's no output for 10 minutes, but some things
# go silent for quite a while
function travis_deadman() {
    while : ; do sleep 60; echo -e "\n${tblue}Mark $(uptime)${treset}" | log -; done
}

function timestamp() {
    while read -r ; do echo "$(stamp) $REPLY"; done
}

# Run one operation.
# shellcheck disable=SC2155
function runit() {
    local cmdname=$1
    local cmd="$*"
    local fcounter=$(printf "%02d" "$counter")
    local local_logdir="$logdir/$fcounter.${cmd// /_/}"
    mkdir -p "$local_logdir"
    local logfile="$local_logdir/Master"
    [[ -n ${DONTRUN_OP:-} ]] && logfile=/dev/null
    local sstime=$(date_sec)
    local ssstamp=$(stamp)
    local status=0
    local msg=completed
    log "-$logfile" "$top_log" <<< "----------------------------------------------------------------"
    if [[ -z ${DONTRUN_OP:-} ]] ; then
	echo "$cmdname started at $ssstamp" | log "$logfile" "$top_log"
	echo "Command: $cmd" | log "$logfile" "$top_log"
	echo "Log file: ${logfile#${logdir}/}" | log "$top_log"
    else
	echo "$cmdname SKIPPED" | log "$top_log"
    fi
    report_progress "$fcounter" "$cmdname"

    if [[ -z ${DONTRUN_OP:-} ]] ; then
	#
	stepstatus=127
	currentmodule="$cmdname"
	# Run the command, capturing console output as well as logged output.
	if [[ -z $(type -t "$cmdname") ]] ; then
	    msg="NOT FOUND"
	    # 126 is also used for "permission denied", which amounts
	    # to the same thing -- it's something the user should not
	    # have tried to run.
	    stepstatus=126
	else
	    if [[ -n ${STP_TEST_RECURSIVE:-} ]] ; then
		STP_TEST_RECURSIVE=2 STP_TEST_LOG_PREFIX="$local_logdir/" $cmd </dev/null 4>"$tmpfile" 6>&5 5>&2 3>&1 2>&1 | timestamp | log "$logfile"
	    else
		STP_TEST_RECURSIVE=1 STP_TEST_LOG_PREFIX="$local_logdir/" $cmd </dev/null 4>"$tmpfile" 5>&2 3>&1 2>&1 | timestamp | log "$logfile"
	    fi
	    stepstatus=${PIPESTATUS[0]}
	    (( stepstatus > 0 )) && msg=FAILED
	fi
	local -a emptyfiles=()
	for f in "$local_logdir"/* ; do
	    [[ -f $f && ! -s $f ]] && emptyfiles+=("$f")
	done
	[[ -n "${emptyfiles[*]:-}" ]] && rm -f -- "${emptyfiles[@]}"
    else
	msg='(SKIPPED)'
    fi
    local setime=$(date_sec)
    local sestamp=$(stamp)
    if [[ -z ${DONTRUN_OP:-} ]] ; then
	echo "$cmd $msg at $sestamp ($(time_delta "$sstime" "$setime"))" | log "$logfile" "$top_log"
	echo "----------------------------------------------------------------" | log "$logfile" "$top_log"
    fi
    counter=$((counter+1))
}

declare -a OPERATIONS=(preflight
		       check_git
		       check_git_tag
		       run_maintainer_clean
		       run_autogen
		       run_clean
		       run_build
		       check_git_builds
		       run_valgrind_minimal
		       run_distcheck_fast
		       run_valgrind
		       run_full
		       run_checksums
		       run_distcheck_minimal
		       git_prep_release
		       save_build_artifacts
		       finis)

function get_longest_op() {
    local longest_op=0
    local op
    for op in "${OPERATIONS[@]}" ; do
	(( ${#op} > longest_op )) && longest_op=${#op}
    done
    echo $((longest_op + 2))
}

# shellcheck disable=SC2206
[[ -n ${STP_BUILD_OPERATIONS:-} ]] && OPERATIONS=($STP_BUILD_OPERATIONS)
[[ -n "$*" ]] && OPERATIONS=("$@")

trap finish EXIT SIGHUP SIGINT SIGQUIT SIGTERM

# shellcheck disable=SC2155
{
declare sstamp=$(stamp)
declare stime=$(date_sec)
}
declare toplogdir=${STP_LOG_DIR:-"$ROOT/BuildLogs"}
declare logdir="$toplogdir/Log.${sstamp// /_}"
[[ -n ${STP_LOG_NO_SUBDIR:-} ]] && logdir=$toplogdir
top_log="$logdir/00.Master"
mkdir -p "$logdir"
if [[ -z ${STP_LOG_NO_SUBDIR:-} ]] ; then
     if [[ -L $toplogdir/Current ]] ; then
	 rm -f -- "$toplogdir/Previous"
	 mv "$toplogdir/Current" "$toplogdir/Previous"
     fi
    ln -s "${logdir##*/}" "$toplogdir/Current"
fi
export ARTIFACTDIR="$logdir/Artifacts"
mkdir -p "$ARTIFACTDIR"

[[ -n $("$GIT" status --porcelain -uno) ]] && git_dirty=' (dirty)'
log "-$top_log" <<< "================================================================"

log_top <<EOF
${tbold}*** Gutenprint $build_type build started at $sstamp on $(uname -n)${treset}

Directory:   $ROOT
Logs:        ${logdir#${ROOT}/}
Kernel:      $(uname -o) $(uname -rv)
Revision:    $("$GIT" rev-parse @)$git_dirty
Parallel:    $STP_PARALLEL
EOF

[[ -n ${STP_TEST_ROTOR_CIRCUMFERENCE:-} && -n ${STP_TEST_ROTOR:-} ]] &&
    log_top "Rotor:       $STP_TEST_ROTOR / $STP_TEST_ROTOR_CIRCUMFERENCE"

# Or we could just do "env" here, but having them sorted makes it
# easier to read.  Variables containing '%%' are functions
log_top <<EOF

Environment:
$(while read -r var ; do
    eval "echo '    '$var=\${$var@Q}"
done <<< "$(compgen -e)")

CPU information:
$(lscpu -e)
$(lscpu)

Memory information:
$(free)

Running operations:
$(printf "    %s\n" "${OPERATIONS[@]}")
EOF

if [[ -n ${TRAVIS_MODE:-} ]] ; then
    export BUILD_VERBOSE=1
    travis_deadman&
fi

declare -A SKIP_OPS
[[ -n ${STP_BUILD_SKIP:-} ]] &&
    for o in ${STP_BUILD_SKIP//,/ } ; do SKIP_OPS[$o]=1; done
declare -i runstatus=0
# shellcheck disable=SC2155
declare -i longest_op=$(get_longest_op)
declare -i lstatus=0
for op in "${OPERATIONS[@]}" ; do
    [[ -z ${BUILD_VERBOSE:-} ]] && tmpfile=$(mktemp "/tmp/stpconsole.XXXXXXXX")
    DONTRUN_OP="${DONTRUN:-}${SKIP_OPS[$op]:-}" runit "$op"
    lstatus=$stepstatus
    report_step_status
    if (( lstatus > 0 )) ; then
	runstatus=1
	(( lstatus != 2 )) && break
    fi
done
exitstatus="$runstatus"
