#!/bin/bash
# SCRIPT_PURPOSE: Low-rent version of sbuild. Resolve dependencies. Build a debian package, and maybe more
# DUE_VERSION_COMPATIBILITY_TRACKING=1.0.0

# Copyright 2021 NVIDIA Corporation.  All rights reserved.
# Copyright 2019,2020 Cumulus Networks, Inc.  All rights reserved.
#
#  SPDX-License-Identifier:     MIT

# Have last command in a pipe to fail return error code.
# This prevents use of tee from hiding fails.
set -o pipefail

# Set top level directory to be where we are now
if [ "$gTOP_DIR" = "" ];then
    gTOP_DIR=$(pwd)
fi

# if command line args for later reference
INVOKED_WITH="$*"

# Hold any build errors. They can be masked by git reset
RETURN_CODE="0"


# if this is set as the first argument, enable debug trace
if [ "$1" = "--script-debug" ];then
    set -x
    echo "$0 Enabling --script-debug "
fi

# Somewhat formatted status messages
function fxnPP()
{
    echo "== $*"
}

function fxnWARN()
{
    echo ""
    echo "## Warning:  $*"
    echo ""
}
# A universal error checking function. Invoke as:
# fxnEC <command line> || exit 1
# Example:  fxnEC cp ./foo /home/bar || exit 1
function fxnEC ()
{

    # actually run the command
    "$@"

    # save the status so it doesn't get overwritten
    status=$?
    # Print calling chain (BASH_SOURCE) and lines called from (BASH_LINENO) for better debug
    if [ $status -ne 0 ];then
        #echo "ERROR [ $status ] in ${BASH_SOURCE[1]##*/}, line #${BASH_LINENO[0]}, calls [ ${BASH_LINENO[*]} ] command: \"$*\"" 1>&2
        echo "ERROR [ $status ] in $(caller 0), calls [ ${BASH_LINENO[*]} ] command: \"$*\"" 1>&2
    fi

    return $status
}

# Standardized error messaging
# Line numbers are more of a suggestion than a rule
function fxnERR
{
    # Print script name, and line original macro was on.
    printf "ERROR at $(caller 0)  :  %s\\n" "$1"
    echo ""
}

# Print messages with an offset for improved visibility.
# MSG_SPACER can be used as needed for
MSG_SPACER=" ---- "
function fxnMSG ()
{
    echo "$MSG_SPACER"
    if [ "$1" = "" ];then
        return 0
    fi
    echo "${MSG_SPACER}$1 "
    echo "$MSG_SPACER"

}

function fxnHelp()
{
    echo""
    echo "Usage  : $(basename "$0"): [Deb build options] [--default|--cbuild|--build-dsc|--build-command] <name>"
    echo "  This script is a default run target for due --build."
    echo "  It performs build environment configuration before running one of the "
    echo "  build target commands. "
    echo ""
    echo "  Build target commands."
    echo "      --default                Build with default settings: dpkg-buildpackage -b -uc -j<max>"
    echo "   -c|--cbuild <args>          Supply <args> to dpkg-buildpackage. If no args, build with defaults."
    echo "                               This must be the last argument on the line as everything after is passed."
    echo "      --build-command <args>   Do environment prep and run <args>. Must be last argument on the line."
    echo "      --help-build-targets     More detailed description of the above options."
    echo "      --build-dsc <.dsc file>  Build from debian source control file .dsc and associated tar.gz."
    echo ""
    echo "  Build options:"
    echo "   -j|--jobs <#>               Number of parallel builds to use. Defaults to the number of CPU cores."
    echo "   --use-directory <dir>       cd to <dir> before trying to build."
    echo "   --prebuild-script <scr>     Run script at container path <scr> before starting build. Pass 'NONE' to ignore."
    echo "   --script-debug              Enable -x if passed as first argument."
    echo ""
    echo "  Debian package build options (use before --cbuild):"
    echo "   --skip-tests                Define DEB_BUILD_OPTIONS=nocheck before build."
    echo "   --deb-build-option <opt>    Add 'opt' to DEB_BUILD_OPTIONS. Use once per option."
    echo "   --build-attempts <times>    Try to build this many times. Default is [ $BUILD_ATTEMPTS ]"
    echo "   --dev-version <vers>        Insert string into changelog package version for pre-release builds."
    echo "                                Note: you need to supply the leading ~ or +"
    echo "   --just-dev-version          Exit after --dev-version applies"
    echo "   --set-git-hash              If building type Git, do not autodiscover, use passed hash."
    echo "   --source-date-epoch <d>     Force the creation time of files, via date +%s. Otherwise defaults to now."
    echo "   --source-format-one         Clear git source history from package and set source to build as 1.0"
    echo "   --install-debs-dir <dir>    Install all debs from <absolute path in container dir> before build."
    echo "   --add-sources-list <file>   Container relative path to include sources.list in build dir."
    echo "   --use-local-repo <repo>     Create a local package repository named <repo> to store and serve packages."
    echo "                                If <repo> starts with a '/' it will be treated as a container-relative"
    echo "                                path for the location of the repository. Otherwise it defaults to the"
    echo "                                directory above the build area."
    echo ""
    echo "  More information:"
    echo "   --quiet                    Suppress output."
    echo "   --verbose                  More info."
    echo "   --help                     This message"
    echo "   --help-examples            Print possible configurations."
    echo "   --version                  Version of this script."
    echo ""
}

#
# A more detailed breakdown on what exactly gets run with which option.
#
function fxnHelpBuildTargets()
{
    echo ""
    echo "duebuild use examples for specifying how to build Debian packages."
    echo ""
    echo "In all these examples, duebuild will:"
    echo " - Upgrade the container's packages."
    echo " - Install the package's build dependencies."
    echo " - And then apply the build command."
    echo ""
    echo " Examples:"
    echo "  DUE command:   due --build"
    echo "  duebuild runs: duebuild --default"
    echo "  Build command: dpkg-buildpackage -uc -us -j<max>"
    echo "  Note: to get the same outcome from the command line, run:"
    echo "    due --run --command sudo apt-get update \; sudo apt-get upgrade \; sudo mk-build-deps --install --remove ./debian/control --tool 'apt-get -y' \; dpkg-buildpackage -uc -us -j<max>"

    echo ""
    echo "  DUE command:   due --build --cbuild"
    echo "  duebuild runs: duebuild --cbuild"
    echo "  Build command: dpkg-buildpackage-uc -us -j<max>"
    echo ""
    echo "  DUE command:   due --build --cbuild -b"
    echo "  duebuild runs: duebuild --cbuild -b"
    echo "  Build command: dpkg-buildpackage-b -j<max>"
    echo ""
    echo "  DUE command:   due --build --build-command make all"
    echo "  duebuild runs: duebuild --build-command make all"
    echo "  Build command: make all"
    echo ""
    echo ""


}
# Demonstrate cases of script usage
function fxnHelpExamples()
{
    echo ""
    echo "duebuild examples of Debian package build options."
    echo ""
    echo "  --cbuild passes whatever args are after it directly to dpkg-buildpackage."
    echo "  The following pulls from the dpkg-buildpackage man pages for two different versions."
    echo ""
    echo "  So for example:"
    echo "  --cbuild examples. (versions earlier than 1.18.5, found in Jessie 8.)"
    echo "   $0 --cbuild takes:"
    echo "     -A - architecture independent (type 'all')"
    echo "     -B - architecture dependent (amd64, armel, etc)"
    echo "     -S - source only."
    echo "     -b - architecture dependent, independent, and no source."
    echo ""
    echo "  --cbuild examples. (versions later than 1.18.5, found in Stretch 9 + )"
    echo "   $0 --cbuild --build= takes as , separated list:"
    echo "     all    - architecture independent (type 'all')"
    echo "     any    - architecture dependent (amd64, armel, etc)"
    echo "     source - source only."
    echo "     binary - architecture dependent, independent, and no source."
    echo "     full   - build everything = source,any,all."
    echo "    So to build all binaries, no source:"
    echo "    $0 --cbuild  --build=any,all"
    echo
    echo "  Build source, arch specific and type 'all' (default -uc -us)"
    echo "   $0 --cbuild "
    echo ""
    echo "  Insert a string into the version"
    echo "   $0 --dev-version ~1234 --cbuild"
    echo ""
    echo "  Set DEB_BUILD_OPTIONS values:"
    echo "   $0 --deb-build-option debug --deb-build-option nostrip --cbuild "
    echo ""
    echo "  Store build products in a repository above the build directory for future builds"
    echo "  (i.e. building things that need other things built to build...)"
    echo "   $0 --use-local-repo myLocalRepo --cbuild "
    echo "   Or specify that repository with an absolute (container relative) path:"
    echo "   $0 --use-local-repo /path/to/myLocalRepo --cbuild "
    echo ""
    echo "  Do environmental setup and run dpkg-buildpackage -uc -us -j8"
    echo "   $0 --build-command dpkg-buildpackage -uc -us -j8"
    echo ""
    echo "  Examples from DUE:"
    echo "   Build package with 'nostrip' and 'debug' options, "
    echo "   unsigned source, unsigned changes file, build binary, 5 jobs,"
    echo "   and reference local package repository 'myLocalRepo'"
    echo "     due --build --jobs 5 --deb-build-option nostrip "
    echo "     --deb-build-option debug --use-local-repo myLocalRepo "
    echo "     --cbuild -us -uc -b"
    echo ""

}

# Set an exit trap to log completion.
function fxnExit()
{

    local returnCode="$?"

    if [ "$returnCode" = "0" ];then
        echo "Done - $0 [ $INVOKED_WITH ]"
    else
        echo "ERROR - $0 [ $INVOKED_WITH ] failed with return code [ $returnCode ]"
    fi

    return $returnCode
}
trap 'fxnExit $RETURN_CODE' EXIT

#
# Include script libraries for consistency, fxnPP, fxnEC, etc
#

# Clearly print what was passed, and any variables set.
# This makes debugging after the fact way easier
function fxnPrintConfig()
{
    echo " ______________________________________________________________________"
    echo "|"
    echo "| $0"
    echo "| Invoked with:      $INVOKED_WITH"
    if [ "$DO_DEFAULT" != "" ];then
        echo "| Building with default settings"
    fi
    if [ "$RAW_BUILD_COMMAND" != "" ];then
        echo "| build command:     $RAW_BUILD_COMMAND"
    else
        echo "| dpkg command:      $DPKG_BUILDENV dpkg-buildpackage  $BUILD_ARGS "
    fi
    echo "|"
    echo "| Build dir            [ $(pwd) ]"
    echo "| Local .deb repo      [ $LOCAL_REPO_NAME ]"
    if [ "$INSTALL_DEBS_DIR" != "" ];then
        echo "| Non-repo debs        [ $INSTALL_DEBS_DIR ]"
    fi
    if [ "$ADD_SOURCES_LIST" != "" ];then
        echo "| Extra sources.list   [ $ADD_SOURCES_LIST ]"
    fi
    echo "|"
    echo "| Reproducible stamp   [ $(date -d @"$SOURCE_DATE_EPOCH") ]"


    if [ "$DO_BUILD_ARCH_SPECIFIC" != "" ];then
        echo "| Build arch specific  [ $DO_BUILD_ARCH_SPECIFIC ]"
    fi
    if [ "$ADDITIONAL_BUILD_ARGS" != "" ];then
        # User is overriding defaults to dpkg-buildpackage
        echo "| dpkg-buildpackage args [ $ADDITIONAL_BUILD_ARGS ]"
    fi

    if [ "$DO_BUILD_TYPE_ALL" != "" ];then
        echo "| Build type 'all'     [ $DO_BUILD_TYPE_ALL ]"
    fi

    if [ "$BUILD_SOURCE_DSC_FILE" != "" ];then
        echo "| Build source pkg     [ $BUILD_SOURCE_DSC_FILE ]"
    fi
    if [ "$USE_JESSIE_DPKG" != "" ];then
        echo "| Older dpkg-build     [ $USE_JESSIE_DPKG ]"
    fi
    echo "| Build attempts       [ $BUILD_ATTEMPTS ]"
    echo "| Git 3.0 build        [ $DO_GIT_BUILD ]"

    # Print only if set
    if [ "$ADD_SOURCES_LIST" != "" ];then
        echo "| Extra sources.list    [ $PREBUILD_SCRIPT ]"
    fi

    if [ "$PREBUILD_SCRIPT" != "" ];then
        echo "| Pre build script      [ $PREBUILD_SCRIPT ]"
    fi
    if [ "$DEV_PACKAGE_VERSION" != "" ];then
        echo "| Version change       [ $DEV_PACKAGE_VERSION ]"
    fi

    if [ "$DO_BUILD" = "TRUE" ];then
        echo "| Build jobs           [ $BUILD_JOBS ]"
        echo "| Build start at       [ $(date) ]"
    fi


    echo "|_____________________________________________________________________"
}


#
# Create a version 1.0 source package that has no git in it.
# The resulting source should build anywhere.
#
function fxnDebianSourceOne()
{

    #
    # expect to be running in the directory that contains debian/
    # packageName should be the base name of the package ( no versions )
    #
    if [ ! -e ~/.git ];then
        fxnWARN "No .git directory found to remove. Skipping --source-format-one."
    fi

    # And make sure there's a format file before we start cleaning house.
    if [ ! -e debian/control  ];then
        fxnERR "Failed to find debian/control file off [ $(pwd) ] Build path looks wrong. Exiting."
        exit 1
    fi
    if [ ! -e debian/source ];then
        fxnWARN "No Debian source directory for [ $PACKAGE_NAME ]. Creating one."
        fxnEC mkdir -p debian/source || exit 1
    fi

    # Delete any nested .git files to keep them from being included
    fxnPP "Deleting .git and .gitignore files under [ $(pwd) ]"
    find ./ -name .git -exec rm -rf {} \;
    find ./ -name .gitignore -exec rm -rf {} \;

    # Packages that build with xz compression won't play with the 1.0 code
    if [ -e debian/source/options ];then
        result=$( grep "compression" debian/source/options )
        if [ "$result" != "" ];then
            fxnWARN "Package [ $PACKAGE_NAME ] uses compression [ $result ]. Overwriting with gz."
            echo "Compression swap to gz for [ $PACKAGE_NAME ]"  >> "${gTOP_DIR}"/PackagesWhereCompressionForcedToGZ.log
            fxnEC sed -i 's/^compression*//g' ./debian/source/options || exit 1
        fi
    fi
    # overwrite the format. Some packages, like hsflowd might not have one, so they get one now.
    fxnPP "Forcing source format to 1.0 "
    echo "1.0" > debian/source/format
    echo "" >> debian/source/format

    # if building tar files - this would continue. Commit once to save it
    return 0

}

# Print missing build dependencies
# Return: 0 if all dependencies are present
#         -1 otherwise
function fxnPrintMissingBuildDependencies()
{
    missingDepends=$( dpkg-checkbuilddeps 2>&1  | sed -e 's/^.*dependencies://g' )
    if [ "$missingDepends" = "" ];then
        echo " #  ----------------------------------------------------------------------------------"
        echo " #"
        echo " # All dependencies are accounted for."
        echo " #"
        echo " #  ----------------------------------------------------------------------------------"
        return 0
    else
        echo ""
        echo " # At least one of these build dependencies (via  dpkg-checkbuilddeps ) is missing"
        echo " #  ----------------------------------------------------------------------------------"
        echo " #"
        echo " #  $missingDepends"
        echo " #"
        echo " #   ------------------------- End list of missing dependencies ------------------------"
        echo ""
        return 1
    fi
}

#
# install all packages that can be installed to just print the ones that are a problem.
#
function fxnIsolatePackageDependencies()
{
    echo ""
    echo " # Installing packages one at a time to try to isolate failures."
    echo " #  ----------------------------------------------------------------------------------"
    # strip any version references that'll be garbage when parsed
    # Wipe out the terms in () by putting them 1 per line and matching
    # otherwise sed gets greedy and takes out everything between the first ( and the last )
    # Then reassemble as a list of packages.
    missingDepends=$( dpkg-checkbuilddeps 2>&1  \
                          | sed -e 's/^.*dependencies://g' \
                          |  tr ' ' '\n' \
                          | sed -e 's/^(.*//g' -e 's/.*)//g' \
                          | tr '\n' ' ')

    if [ "$missingDepends" = "" ];then
        echo "   Looks like all dependencies are present. Probably a local leftover depends package triggering this."
        return 0
    fi
    # Ignore shellcheck warning to "" missingDepends
    for pkg in ${missingDepends[@]}
    do
        echo " #"
        echo " # Attempting install of [$pkg]"
        echo " #"
        # 2 quiets to kill output
        sudo apt-get install --quiet --quiet --assume-yes --no-install-recommends "$pkg"
        if [ "$?" = "0" ];then
            echo "Installed [ $pkg ]"
        else
            # Run a loud install to highlight the failure
            fxnPP "Failed  [ $pkg ]. Retrying the install to get the errors."
            sudo apt-get install --assume-yes --no-install-recommends "$pkg"
        fi
    done

    # At this point, this list should be all install failures.
    fxnPrintMissingBuildDependencies
    return $?
}

# Takes: name to use for repository
#        Debian package  architecture ( or 'default' to determine it from the system.)
#        absolute path to a directory that contains files to add to the repository

# Does:  Creates a local package repository one directory up from
#        the current build directory (or uses it if it already exists)
#        and adds it to the build.
function fxnUseLocalPackageRepository()
{
    local repoName="$1"
    local useArchitecture="$2"
    local addContentsDir="$3"

    local repoPath=""

    if [ "$useArchitecture" = "default" ];then
        # Use target arch as that represents the end build product.
        useArchitecture=$( dpkg-architecture --query DEB_TARGET_ARCH )
    fi

    #
    # Is the repository given as an absolute path?
    # (it starts with a '/' ?)
    if [[ $repoName == /* ]];then
        # An absolute path
        repoPath="$repoName"
    else
        # A relative path. Drop it local.
        repoPath="$( realpath "${BUILD_DIR}/.." )/${repoName}"
    fi
    echo "$repoPath"

    if [ ! -e "$repoPath" ];then
        # create the repository if it does not exist
        fxnEC /usr/local/bin/due-manage-local-repo.sh  --name "$repoPath" --architecture "$useArchitecture" --create-repo || exit 1
    fi

    if [ "$addContentsDir" != "" ];then
        # if passed a directory full of debs, add them.
        fxnEC /usr/local/bin/due-manage-local-repo.sh  --name "$repoPath" --architecture "$useArchitecture" --add "$addContentsDir" || exit 1
    fi
}

# Function to replace sbuild
function fxnDoDebianBuild()
{
    local missingDepends=""
    local errorCode=""
    local failMsg=""

    fxnMSG "Resolving build dependencies."
    echo ""

    # Possibly a failed source extraction, or the fallout of a bad day.
    # Either way, this has nowhere to go now.
    if [ ! -e ./debian/control ];then
        fxnERR "Failed to find ./debian/control in [ $(pwd) ]. Exiting."
        exit 1
    fi

    # Always apt-get update
    fxnPP "Running apt-get update."
    if [ "$LOCAL_REPO_NAME" != "" ];then
        echo "Ignore 'Failed to stat' errors if there are no packages in local repository $LOCAL_REPO_NAME"
    fi
    sudo apt-get update || fxnWARN "apt-get update failed. Continuing with local index."

    fxnPP "Upgrading local packages so that any already installed build dependencies are up to date. Keeping existing config files."
    # stick with existing configurations if asked. Not entirely sure this is good in all cases.
    export DEBIAN_FRONTEND=noninteractive
    sudo DEBIAN_FRONTEND=noninteractive apt-get upgrade \
         -o Dpkg::Options::="--force-confdef" \
         -o Dpkg::Options::="--force-confold" \
         --assume-yes || fxnWARN "apt-get upgrade failed. Continuing with local index."

    # Make sure the environment has the tools to configure the environment.
    if [ ! -e /usr/bin/mk-build-deps ];then
        fxnPP "Failed to find mk-build-deps. Installing devscripts and equivs."
        fxnEC sudo DEBIAN_FRONTEND=noninteractive apt-get install --assume-yes equivs devscripts || exit 1
    fi


    # Create package with all needed build dependencies
    # install it
    # remove the .deb after to keep git from FREAKING OUT!
    sudo mk-build-deps --install --remove ./debian/control --tool "apt-get -y"
    errorCode="$?"
    echo ""
    if [ "$errorCode" != "0" ];then
        if [ "$errorCode" = "1" ];then
            # Failed to find dependencies

            # Print what was trying to be installed
            fxnPrintMissingBuildDependencies

            fxnPP "Running apt-get update to make sure package lists are up to date."
            sudo apt-get update

            # Go through the list. Since any one failure will break the installation of everything,
            # try installing one at a time to figure out what will install, and reduce debug down
            # to those packages that are truly broken.
            fxnIsolatePackageDependencies

            # Things should be as fixed as possible by this point, so run it again to highlight the errors,
            # or continue if everything fixed itself
            fxnIsolatePackageDependencies || exit 1


        elif [ "$errorCode" = "25" ];then
            fxnWARN "mk-build-deps error [ $errorCode ] finds no dependencies? Well, let's see where this goes...."
            errorCode=0
        elif [ "$errorCode" = "29" ];then
            fxnERR "mk-build-deps error [ $errorCode ] found dependencies but they failed to install properly. Your packages may be in an unstable state. Exiting."
            exit 1
        else
            #note that an error was hit in case it's not a lack of dependencies.
            fxnERR "mk-build-deps error [ $errorCode ] in : sudo mk-build-deps --install ./debian/control --tool apt-get -y."
            exit 1
        fi
    fi # errorCode non zero
    echo ""

    #
    # mk-build-deps doesn't always clean up...
    #
    rm -f due-build-deps_*.buildinfo
    rm -f due-build-deps_*.changes

    #
    # If setting source to 1.0 for universal rebuild
    #
    if [ "$DEBIAN_SOURCE_FORMAT_ONE" = "TRUE" ];then
        fxnPP "Erasing source history, as --source-format-one was passed."
        fxnEC fxnNoSourceHistory || exit 1
    fi

    while [ $(( BUILD_ATTEMPTS > 0 )) = 1 ]; do


        BUILD_ATTEMPTS=$(( BUILD_ATTEMPTS - 1 ))
        echo ""

        if [ "$RAW_BUILD_COMMAND" != "" ];then
            failMsg="$RAW_BUILD_COMMAND failed with error "
            fxnMSG "Building in [ $PACKAGE_DIR_NAME ] with [ $RAW_BUILD_COMMAND ].  Attempt [ $BUILD_ATTEMPTS ]. "
            echo "bash -c  $RAW_BUILD_COMMAND"
            bash -c " $RAW_BUILD_COMMAND"
        else
            failMsg="dpkg-buildpackage $BUILD_ARGS failed with error "
            fxnMSG "Building in [ $PACKAGE_DIR_NAME ] with [ $BUILD_ARGS ].  Attempt [ $BUILD_ATTEMPTS ]. "
            # And ADDITIONAL_BUILD_ARGS was set
            echo "bash -c  $DPKG_BUILDENV dpkg-buildpackage $BUILD_ARGS "
            bash -c " $DPKG_BUILDENV dpkg-buildpackage $BUILD_ARGS "
        fi
        result="$?"

        if [ "$result" != "0" ];then

            case $result in
                "2" )
                    if [ "$BUILD_ATTEMPTS" = "0" ];then
                        fxnERR "$failMsg [ $result ]"
                        exit $result
                    fi
                    ;;

                * )
                    # Retry until the tries run out.
                    if [ "$BUILD_ATTEMPTS" = "0" ];then
                        fxnERR "$failMsg [ $result ]"
                        exit $result
                    fi
            esac
        fi

    done
    echo ""


    fxnMSG "Built [ $PACKAGE_DIR_NAME ] - list follows:"
    # use grep to avoid errors when no .debs found
    ls -lrt ../ | grep ".deb"  | sed -e "s/^/${MSG_SPACER}/g"

    # if packages are being stored in a local repository
    if [ "$LOCAL_REPO_NAME" != "" ];then
        fxnPP "Adding debs from $(realpath ../) to local repository [ $LOCAL_REPO_NAME ]"
        fxnUseLocalPackageRepository "$LOCAL_REPO_NAME" "default" "$(realpath ../)"
    fi
    echo ""
}

#
# handle all build setup. Having this as a function simplifies
# redirecting output to the log file
#
function fxnDoDevBuild()
{

    #################################################################
    # Put a development string into the package version.            #
    #################################################################

    CHANGELOG_LINE=$( head -n 1 debian/changelog  )

    # First item on the line
    PACKAGE_NAME=$(  echo "$CHANGELOG_LINE" | awk '{print$1}' )

    # Get the current version
    # Delete all before first (
    # Delete all after first ~  OR first )
    DEBIAN_VERSION=$( echo "$CHANGELOG_LINE"  | sed -e 's/.*(//' | sed -e 's/\(~\|)\).*//'  )

    # Format of development string to append to existing version
    VERSION_SUFFIX="$DEV_PACKAGE_VERSION"

    #
    # TODO: if building a python package, hack it's version here.
    #       Left as an exercise for the reader as versioning in Python packages varies
    #

    #
    # Now that the dev string has been sorted,
    # update the debian/changelog with dch
    #

    fxnMSG "Running dch --package $PACKAGE_NAME --force-bad-version --newversion  $DEBIAN_VERSION~$VERSION_SUFFIX \"Added dev string for $DEBIAN_VERSION-$VERSION_SUFFIX\""
    fxnEC dch --package "$PACKAGE_NAME" \
          --force-bad-version \
          --newversion  "${DEBIAN_VERSION}${VERSION_SUFFIX}" "Added dev string for $DEBIAN_VERSION-$VERSION_SUFFIX" || return 1


    if [ "$DO_GIT_BUILD" = "TRUE" ];then
        fxnMSG "Committing dev string to change log. To undo, run: git reset --hard $CURRENT_GIT_COMMIT"
        # If python version files were modified, commit them here too
        fxnEC git commit debian/changelog  -m "Added dev string for $DEBIAN_VERSION-$VERSION_SUFFIX"  || return 1
    fi

    fxnMSG "Running fxnDoDebianBuild with $BUILD_ARGS"
    # --nolog - print everything to stdout
    fxnDoDebianBuild  "$BUILD_ARGS"

    #Reserve exit code through exit trap
    RETURN_CODE="$?"
    if [ "$RETURN_CODE" != "0" ];then
        echo ""
        echo "#"
        echo "# $0 [ $INVOKED_WITH ] failed with [ $RETURN_CODE ]. Build failure log is in [ $BUILD_LOG_FILE ]"
        echo "#"
        echo ""

    fi
    return $RETURN_CODE
}

#
# Do git reset to revert any version modifications
#  made to the changelog
#
function fxnResetGit()
{
    local returnCode="$?"

    if [ "$JUST_DEV_VERSION" = "TRUE" ];then
        # User wanted time stamp set and exit, so don't reset it.
        return 0
    fi
    # Revert the changelog to it's pre-modification state
    if [ "$CURRENT_GIT_COMMIT" != "" ];then
        # this is still a 3.0 (git) package
        if [ "$DEBIAN_SOURCE_FORMAT_ONE" = "FALSE" ];then
            # ...and build started as a 3.0 (git) package
            if [ "$DO_GIT_BUILD" = "TRUE" ];then
                # ...and a version to add to the change log was supplied
                if [ "$DEV_PACKAGE_VERSION" != "" ];then
                    fxnEC cd "$BUILD_DIR" || exit 1
                    # ...and there is a .git directory to revert
                    # (if this built with source history being erased, that's gone.)
                    if [ -d .git ];then
                        # OK, revert to prior saved commit.
                        fxnMSG "Reverting add of dev string to changelog."
                        fxnEC   git reset --hard "$CURRENT_GIT_COMMIT" || return 1
                        echo ""
                    fi
                fi
            fi
        fi #DEBIAN_SOURCE_FORMAT_ONE
    fi # CURRENT_GIT_COMMIT != ""
    # Mirror standard exit functionality
    echo "Done - $0 [ $INVOKED_WITH ] Return code [ $returnCode ]"
    return $returnCode
}


#
# set any default variables
#
# provide a version for use in upgrades
SCRIPT_VERSION="1.0"

TARGET_ARCHITECTURE=""

# Save the git commit before it gets a dev version string
CURRENT_GIT_COMMIT=""

# Default to one build attempt
BUILD_ATTEMPTS="1"

# Default to build after dev version string application
JUST_DEV_VERSION="FALSE"

# Default to this many build threads


# If nproc found, default to ALL THE CORES!
# Else try 4 cores.
BUILD_JOBS=$(nproc 2>/dev/null || echo "4" )

# Don't revert to building with debian/source/format 1.0
DEBIAN_SOURCE_FORMAT_ONE="FALSE"
##################################################
#                                                #
# MAIN  - script processing starts here          #
#                                                #
##################################################



if [ "$#" = "0" ];then
    # Require an argument for action.
    # Always trigger help messages on no action.
    fxnHelp
    exit 0
fi

# Track this. --jobs and -j* by themselves don't count as a non-default
#  build command.
TOTAL_COMMAND_LINE_ARGS="$#"

#
# Gather arguments and set action flags for processing after
# all parsing is done. The only functions that should get called
# from here are ones that take no arguments.
while [[ $# -gt 0 ]]
do
    term="$1"

    case $term in

        --script-debug )
            # Catch the debug flag here
            echo "[ $0 ] Script debug is ON"
            ;;
        --install-debs-dir )
            INSTALL_DEBS_DIR="$2"
            if [ ! -e "$INSTALL_DEBS_DIR" ];then
                fxnERR "Failed to find additional deb directory [ $INSTALL_DEBS_DIR ]"
                exit 1
            fi
            shift
            ;;

        --use-directory )
            # as package builds put the build products in the directory above
            # the source, the build may have been started a level above and
            # will have to go into that directory.
            BUILD_DIR="$2"
            shift
            ;;

        --just-dev-version )
            # apply dev string and exit
            JUST_DEV_VERSION="TRUE"
            ;;

        --prebuild-script )
            # Run this before starting package build. Probably contains commands
            # to generate ./debian/* files
            # Allow the option of it just being a placeholder if 'NONE' is passed.
            if [ "$2" != "NONE" ];then
                PREBUILD_SCRIPT="$2"
                shift
            fi
            ;;

        --add-sources-list )
            if [ ! -e "$2" ];then
                fxnERR "Container failed to find additional sources.list file [ $2 ]. Exiting."
                exit 1
            fi
            # Allow stacking multiple sources.list files to copy in.
            ADD_SOURCES_LIST+=" $2 "
            shift
            ;;

        --use-local-repo )
            # Use (or create if it does not exist) a local Debian
            # package repository to hold build products, and serve
            # them for subsequent builds.
            # Useful when building packages that depend on previously
            # built packages.
            if [ "$2" = "" ];then
                fxnERR "Failed to specify repository with --use-local-repo! Exiting."
                exit 1
            fi
            LOCAL_REPO_NAME="$2"
            # Local repo only applies if building, so keep defaults until told
            # otherwise and build.
            DO_BUILD="TRUE"
            shift
            ;;

        # take --build as it tends to get typed in container
        # --cbuild is clearer when invoking from outside the container
        -c | --cbuild | --build )
            # Default to build everything in container context
            DO_BUILD="TRUE"
            if [ "$2" != "" ];then
                # More arguments?
                # skip over --cbuild
                shift
                # The rest of the arguments passed should be given verbatim to
                # whatever the build command is.
                ADDITIONAL_BUILD_ARGS="$*"
            fi
            break
            ;;

        --build-command )
            # Default to build everything in container context
            DO_BUILD="TRUE"
            if [ "$2" != "" ];then
                # More arguments?
                # skip over --build-command
                shift
                # The rest of the arguments passed should be given verbatim to
                # whatever the build command is.
                RAW_BUILD_COMMAND="$*"
            fi
            break
            ;;

        --default )
            DO_DEFAULT="TRUE"
            # --default is an exported hook that can be be called by DUE
            # when this container is run. The intent is to do a very
            # basic build operation to demonstrate functionality
            # and hopefully cover common cases.
            # For Debian package build, the --cbuild option conveniently
            # does all this, so use it.
            DO_BUILD="TRUE"
            ;;

        --build-dsc )
            # Build based off a debian source package .dsc, tar.gz
            # Default to build everything
            DO_BUILD="TRUE"
            BUILD_SOURCE_DSC_FILE="$2"
            # Stop things here if the file does not exist.
            if [ ! -e "$BUILD_SOURCE_DSC_FILE" ];then
                fxnERR " --build-from-deb-source file [ $BUILD_SOURCE_DSC_FILE ] was not found in current directory."
                echo "Possible files in [ $PWD ]:"
                ls -l -- *.dsc
                echo ""
                exit 1
            fi
            # Some more sanity checking
            if [[ "$BUILD_SOURCE_DSC_FILE" != *".dsc" ]];then
                fxnERR " --build-from-deb-source file [ $BUILD_SOURCE_DSC_FILE ] does not end in .dsc. Exiting."
                exit 1
            fi
            shift
            ;;

        -j* )
            # number of build job threads
            BUILD_JOBS="${term#-j}"
            # If only jobs were specified, do default build
            if [ "$TOTAL_COMMAND_LINE_ARGS" = 1 ];then
                DO_DEFAULT="TRUE"
                DO_BUILD="TRUE"
            fi
            ;;

        --jobs )
            # number of build job threads
            BUILD_JOBS="$2"
            if [ "$2" = "" ];then
                fxnERR "--jobs requires a #"
                exit 1
            fi
            # If only jobs were specified, do default build
            if [ "$TOTAL_COMMAND_LINE_ARGS" = 2 ];then
                DO_DEFAULT="TRUE"
                DO_BUILD="TRUE"
            fi
            shift
            ;;


        --dev-version )
            # String to add to development version.
            # If $2 starts with ~, the packaging tools will see this version as
            # being _less_ than a version without one
            # Example:
            #   foo_1.2~123_amd64.deb < foo_1.2_amd64.deb
            # This allows for versions with a ~ to be seen as higher priority relative
            # to each other, but when the release comes, the ~ is pulled and that version
            # beats all the dev versions.  This way the changelog doesn't have to
            # increment its version until it's ready.
            DEV_PACKAGE_VERSION="$2"
            shift
            ;;

        --skip-tests )
            # Disable running of package tests
            DPKG_BUILDENV+=" DEB_BUILD_OPTIONS=nocheck "
            ;;

        --deb-build-option )
            # add term to a list that will follow DEB_BUILD_OPTIONS
            DEB_OPTION_LIST+=" $2 "
            shift
            ;;

        --source-date-epoch )
            # Force the timestamp ( in date +%s format ) that files
            # and directories in the package will have.
            # Debian tools key off the last changelog entry by default
            # This will default to the current time, if this argument is
            # not passed.
            SOURCE_DATE_EPOCH="$2"
            shift
            ;;

        --set-git-hash )
            # force a Git hash string to use, overriding
            # auto detect. Needed if building scrubbed source
            # packages where the original .git dir is gone
            # by build time
            CURRENT_GIT_COMMIT="$2"
            DO_GIT_BUILD="FALSE"
            shift
            ;;

        --source-format-one )
            # remove git source history from package and build as 1.0 format deb
            DEBIAN_SOURCE_FORMAT_ONE="TRUE"
            ;;

        --build-attempts )
            # Sometimes the first try isn't the charm.
            BUILD_ATTEMPTS="$2"
            if [ "$2" = "" ];then
                fxnERR "--build-attempts requires a number. Ex: --build-attempts 2.  Exiting."
                exit 1
            fi
            shift
            ;;


        --version )
            # Track version for upgrade purposes
            echo "$SCRIPT_VERSION"
            exit 0
            ;;

        -h|--help)
            fxnHelp
            exit 0
            ;;

        --help-examples)
            # Show examples of script invocation
            fxnHelpExamples
            exit 0
            ;;

        --help-build-targets)
            # Examples of what gets invoked for each build option.
            fxnHelpBuildTargets
            exit 0
            ;;

        --verbose )
            # Unused for now
            DO_VERBOSE="TRUE"
            ;;

        --quiet )
            # Unused for now
            DO_QUIET="TRUE"
            ;;

        *)
            fxnHelp
            echo "Unrecognized option [ $term ]. Exiting"
            exit 1
            ;;

    esac
    shift # skip over argument

done

# Building from top level if not specified otherwise
if [ "$BUILD_DIR" = "" ];then
    BUILD_DIR="$gTOP_DIR"
fi

#
# Earlier versions of dpkg-buildpackage have slightly
# different arguments than the 1.19 series that both
# Ubuntu 18 and Debian 9 Stretch use
#
version=$( dpkg-buildpackage --version | grep " 1.17." )
if [ "$version" != "" ];then
    fxnMSG "Jessie era dpkg detected. Updating arguments."
    USE_JESSIE_DPKG="TRUE"

    # Jessie apt has a bug where it tries to close 100K plus files in the shell.
    # Set bash ulimit to max at 10000 to speed this up.
    ulimit -n 10000
fi

# Arguments passed to the dpkg build. This gets
# appended by fxnParseBuildArgs

# Default to not building source packages, just the binaries
#  This assumes that the user usually doesn't care about changing the source,
#  and if they are at that level, they'll add the additional arguments.
if [ "$DO_BUILD" = "TRUE" ];then
    if [ "$USE_JESSIE_DPKG" = "TRUE" ];then
        BUILD_ARGS=" -b  -uc  "
        # Build source and binaries
        #BUILD_ARGS=" -us  -uc  "

    else
        BUILD_ARGS=" --build=binary  --unsigned-changes "
        # Build source and binaries
        #BUILD_ARGS=" --unsigned-source  --unsigned-changes "
    fi

    if [ "$ADDITIONAL_BUILD_ARGS" != "" ];then
        # User is overriding defaults to dpkg-buildpackage, so
        # take it literally with no defaults.
        BUILD_ARGS="$ADDITIONAL_BUILD_ARGS"
    fi

fi

# Jessie
#  build source -S
#  build arch specific -B

# Stretch - options can , separate
#   build any --build any
#   build all --build all
#   build source --build source

# Add parallel build
if [ "$BUILD_JOBS" != "" ];then
    BUILD_ARGS+=" -j${BUILD_JOBS} "
fi

# Add sources.list files and apt-update BEFORE using a local
# package repository. If the local repo was just created and
# is empty, it throws errors on apt-get update.
if [ "$ADD_SOURCES_LIST" != "" ];then
    for srcfile in $ADD_SOURCES_LIST ; do
        fxnPP "Copying $srcfile to /etc/apt/sources.list.d"
        fxnEC sudo cp "$srcfile" /etc/apt/sources.list.d  || exit 1
    done
    # If any added sources.list file is bad, fail immediately.
    fxnEC sudo apt-get update || exit 1

fi

# Use/create local repository to store build products.
# May be empty at start, expecting build products to be added.
# NOTE: it will cause errors if apt-get is run while it is empty.
if [ "$LOCAL_REPO_NAME" != "" ];then
    # Leave last parameter out as there are no packages, but
    # the repository references should be added.
    fxnUseLocalPackageRepository "$LOCAL_REPO_NAME" "default"
fi


# if the package source is of type 'git', any version changes
# need to be committed to the changelog and then reverted out
# otherwise the build will complain about unexpectedly modified
# files in the build directory
grep -iq "git" ./debian/source/format 2>/dev/null
if [ $? = 0 ];then
    DO_GIT_BUILD="TRUE"
    # save the current state of git for restore
    CURRENT_GIT_COMMIT=$( git rev-parse --short HEAD )
    trap 'fxnResetGit $RETURN_CODE' EXIT
fi

if [ "$BUILD_DIR" != "" ];then
    if [ ! -e "$BUILD_DIR" ];then
        fxnERR "--use-directory [ $BUILD_DIR ] does not exist in $(pwd)"
        exit 1
    fi
    fxnEC cd "$BUILD_DIR" || exit 1
fi


# Files in the deb (use dpkg -c <debname> will have this date
# This is the SOURCE_DATE_EPOCH variable from reproducible builds
#    echo "| Reproducible stamp   [ $(dpkg-parsechangelog -SDate) ]"

if [ "$SOURCE_DATE_EPOCH" = "" ];then
    # default to the current time if not set
    SOURCE_DATE_EPOCH=$(date +%s)
    export SOURCE_DATE_EPOCH
fi


if [ "$DEB_OPTION_LIST" != "" ];then
    # If there were deb options set, create the string to apply them here.
    fxnPP "Using passed DEB_BUILD_OPTIONS [ $DEB_OPTION_LIST ]"
    DPKG_BUILDENV="DEB_BUILD_OPTIONS='$DEB_OPTION_LIST'"
fi

#
# Dump what's being run
#
fxnPrintConfig

if [ "$PREBUILD_SCRIPT" != "" ];then
    fxnHeader "Running pre build script [ $PREBUILD_SCRIPT ]"
    bash "$PREBUILD_SCRIPT"
fi

if [ "$INSTALL_DEBS_DIR" != "" ];then
    fxnPP "Installing additional *.debs from [ $INSTALL_DEBS_DIR ]. "
    fxnWARN "Ignore dependency warnings - they will be resolved."

    curDir="$(pwd)"
    fxnEC cd "$INSTALL_DEBS_DIR" || exit 1
    result=$( ls -l -- *.deb 2> /dev/null )
    if [ "$result" != "" ];then
        fxnPP "Found the following additional debs to install:"
        echo "----------------------------------------------------------------------------"
        echo ""
        echo "$result"
        echo ""
        echo "----------------------------------------------------------------------------"

        export DEBIAN_FRONTEND=noninteractive
        sudo DEBIAN_FRONTEND=noninteractive dpkg --force-confnew -i -- *.deb
        fxnPP "Ignore any install dependency issues from the above. Resolving with [ apt install -y -f ]"

        #       sudo  apt-get install -y -f -o Dpkg::Options::="--force-confdef" || exit 1

        fxnEC sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -f || exit 1

    else
        fxnPP "No *.debs in [ $INSTALL_DEBS_DIR ]. Continuing..."
    fi

    fxnEC cd "$curDir" || exit 1
fi


#
# Take actions now that all arguments have been passed.
#

if [ "$DO_BUILD" = "TRUE" ];then

    # if this is building a debian source package and not a
    # git checkout, there should be only one each of .dsc and .tar.gz file
    # Extract the code so there's a debian/control file and proceed.


    if [ "$DEV_PACKAGE_VERSION" != "" ];then
        # Without a clean way to revert this (as is the case with git packages that take --revert )
        # The package version strings will keep appending in the changelog, creating a mess.
        # So warn about that.
        fxnWARN "Adding new [ $DEV_PACKAGE_VERSION ] to debian/changelog. These additions are cumulative."
    fi

    if [ "$BUILD_SOURCE_DSC_FILE" != "" ];then
        fxnPP "Extracting source with dpkg-source -x $BUILD_SOURCE_DSC_FILE"
        dpkg-source -x "$BUILD_SOURCE_DSC_FILE"
        retCode=$?

        case $retCode in
            255 )
                fxnWARN "Directory already exists. Continuing."
                ;;
            0 )
                fxnPP "Extraction successful. Continuing."
                ;;
            * )
                fxnERR "Got dpkg-source -x return code [ $retCode ]. Exiting."
                exit 1
                ;;
        esac

        # Time to guess the name of the package build directory, as these things are
        # occasionally inconsistent
        # Hope it's the name of the orig.tar.gz file, with:
        #  the _ changed to -
        #  and the orig.tar.gz gone
        # The above dpkg-source -x should have generated this directory.
        # If not, we've no idea where to go, and are done.

        # Get the name of the file (in case it has a '_'_ )
        packageName=$(basename "$BUILD_SOURCE_DSC_FILE")
        # Now get just the name.
        packageName=${packageName%%_*}
        
        # list most recent directory
        buildDir=$( ls -1rd -- */ | grep "$packageName" | tail -n 1)

        if [ ! -e "$buildDir" ];then
            fxnERR "Failed to find expected source directory [ $buildDir ] in [ $(pwd) ]. Exiting."         
            exit 1
        fi

        # hop in the build directory and let it go from there
        fxnPP "Setting current working directory to [ $buildDir ]"
        fxnEC cd "$buildDir" || exit 1
    fi # BUILD_SOURCE_DSC
fi # if DO_BUILD

if [ ! -e debian/control ];then
    # Debian build directories don't seem to be here.
    fxnERR "Failed to find debian/control directory in [ $(pwd) ]. Use --build-dsc if using a .dsc and tar file."
    echo "Current directory: [ $(pwd) ]"
    echo "Contents:"
    ls -l
    echo "Exiting."
    exit 1
fi

#
# Figure out what sbuild parameters were passed and translate
# them to a dpkg-buildpackage command line
#

PACKAGE_DIR_NAME="$(basename "$(dirname "$(pwd)")")"
BUILD_LOG_FILE="$(dirname "$(pwd)")/${PACKAGE_DIR_NAME}_$(date +%s)_${TARGET_ARCHITECTURE}.build"

fxnMSG "Logging to [ $BUILD_LOG_FILE ]"


if [ "$INSTALL_DEBS_DIR" != "" ];then
    # If we're building dependent packages, toast all debs in the directory above
    # so we don't have multiple versions of the same deb to try and install.
    # otherwise just leave them
    fxnWARN "Deleting ../*deb files for a clean copy to [ $INSTALL_DEBS_DIR ]"
    rm ../*deb
fi

#
# actually do the build
#

if [ "$DEV_PACKAGE_VERSION" = "" ];then
    # This is the default, don't have to advertise.
    #fxnPP "Building without a dev version string."
    # --nolog - print everything to stdout
    fxnDoDebianBuild --nolog  | tee -a "$BUILD_LOG_FILE"
else
    fxnPP "Building with dev version $DEV_PACKAGE_VERSION"
    fxnDoDevBuild  2>&1 | tee -a  "$BUILD_LOG_FILE"
fi

# Preserve exit code through exit trap
RETURN_CODE=${PIPESTATUS[0]}

# step out of debian
cd ..


if [ "$RETURN_CODE" = "0" ];then
    if [ "$INSTALL_DEBS_DIR" != "" ];then
        fxnPP "Copying built .debs into [ $INSTALL_DEBS_DIR ] for future dependency builds."
        echo "Debs to copy in [ $(pwd) ]:"
        ls -l
        echo "To [ $INSTALL_DEBS_DIR ]"
        ls -l "$INSTALL_DEBS_DIR"
        # *deb should get udebs as well
        fxnEC cp -- *.deb "$INSTALL_DEBS_DIR" || exit 1
        echo "Contents of $INSTALL_DEBS_DIR:"
        ls -lrt "$INSTALL_DEBS_DIR"

        fxnPP "Build finished at [ $(date) ]"
    fi
else
    fxnERR "Build FAILED with return code [ $RETURN_CODE ] at [ $(date) ]"
fi

exit "$RETURN_CODE"
