#!/bin/bash
# SCRIPT_PURPOSE: Build abstratction tool for Suse RPM packages.
# DUE_VERSION_COMPATIBILITY_TRACKING=1.0.0

# Copyright 2021,2022 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 "Usage  : $(basename "$0"): [options] [--cbuild | --default | --build-command <args> ]"
    echo "  Script to support building for the container's target."
    echo ""
    echo "  Build targets:"
    echo "   -c|--cbuild <args>          Default container build command."
	echo "                               <args> is a list of additional build commands. Must be last argument."
	echo "      --default                Try to build with default settings."
    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 ""
    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 " RPM build options (use before --build)"
    echo "   --rpm-build-option <opt>    Add 'opt' to rpmbuild. Use once per option."
    echo ""
    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 an RPM from a source RPM."
    echo ""
	echo ""
	echo " Examples"
    echo "  DUE command:   due --build <src rpm>"
    echo "  duebuild runs: duebuild --default"
    echo "  Build command: rpmbuild -bb <src rpm>"
    echo ""
	
}	
# Demonstrate cases of script usage
function fxnHelpExamples()
{
    echo ""
    echo " Examples:"
    echo ""
    echo "  Build."
    echo "   $0 --cbuild "
	echo ""
	echo "  Build default - a simple/standard build case."
    echo "   $0 --cbuild --default"
    echo ""
    echo "  Build binary from source RPM"
    echo "   $0 --cbuild foo.src.rpm"
    echo ""
    echo "  Build binary and source rpm source RPM"
    echo "   $0 --cbuild --rpm-build-option -ba --cbuild foo.src.rpm"
    echo ""
	echo "  Pass additional arguments to build."
	echo "   $0 --cbuild  REPLACE_THIS build example"
    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 "| build command:      REPLACE"
    fi	
    echo "|"
    echo "| Build dir            [ $(pwd) ]"
    echo "| Build attempts       [ $BUILD_ATTEMPTS ]"
    if [ "$PREBUILD_SCRIPT" != "" ];then
        echo "| Pre build script      [ $PREBUILD_SCRIPT ]"
    fi
    if [ "$DO_BUILD" = "TRUE" ];then
        echo "| Build jobs           [ $BUILD_JOBS ]"
        echo "| Build start at       [ $(date) ]"
    fi
    echo "|_____________________________________________________________________"
}


#
# rpm tips
#
# Find what package provides a missing file (on a registered system)
#  zypper whatprovides <term>
#
# Download an RPM and everything that it depends on
#  zypper download <package> --resolve
#
# Install local rpms (otherwise rpm does not use local rpms to satisify dependencies.)
#  sudo zypper/yum install *rpm
#
# Built packages will end up here
#

# If -b - use rpm spec file
# if -r - use source file
# Spec file options
# -bp to unpack and apply patches
# -bc compile sources
# -bs build source package
# -bb build binaries only
# -ba build all
RPM_BUILD_OPTIONS_DEFAULT=' -bb '
# hold any user set options
RPM_BUILD_OPTIONS=""

# Function to build whatever the target is
function fxnDoBuild()
{

    ######################################
    # Sanity check the build environment
    ######################################

    #
    # Does a mockbuild user exist?
    #
    grep 'mockbuild' /etc/passwd > /dev/null
    if [ $? != 0 ];then
        echo "Adding mockbuild user and mock group to keep complaints down."
        # this will remove 'harmless' mock user errors
        sudo useradd -s /sbin/nologin mockbuild       
        sudo groupadd mock
    else
        echo "Mockbuild user and mock group found. Continuing."
    fi


    if [ "$OS_TYPE" = "RedHat" ];then
        $PACKAGE_APP  list installed yum-utils > /dev/null
        if [ $? != 0 ];then
            echo "Installing build packages: $BUILD_PACKAGES."
            sudo $PACKAGE_APP install $BUILD_PACKAGES
        else
            echo "Build packages are installed. Continuing."
        fi
    fi
    

    # If user invoked --build-command, don't try to fill in the blanks.
    if [ "$RAW_BUILD_COMMAND" != "" ];then
        echo " --build-command was used, so NOT parsing arguments or checking dependencies."
        buildCommand="$RAW_BUILD_COMMAND"
    else
        
        ######################################
        # Sanity check arguments
        ######################################

        echo "SOURCE_RPM = $SOURCE_RPM"
        echo "ADDITIONAL_BUILD_ARGS = $ADDITIONAL_BUILD_ARGS"
        
        # Sanity check
        if [ "$SOURCE_RPM" = "" ];then
            echo "Error - no source rpm to --build  was supplied. Exiting."
            exit 1
        fi

        # Cut any path info, and everything after the first '-'
        PACKAGE_NAME="$( basename $SOURCE_RPM | sed -e 's/-[0-9].*//g' )"

        echo "Package name is [ $PACKAGE_NAME ]"


        if [ ! -e "${BUILD_TARGET_DIR_NAME}/SPECS/${PACKAGE_NAME}.spec" ];then
            echo "Creating $BUILD_TARGET_DIR_NAME directory for [ $SOURCE_RPM ]"
            # This should create a ~/rpmbuild directory
            fxnEC rpm $BUILD_ROOT_COMMAND --install --verbose --hash $SOURCE_RPM || exit 1

            if [ ! -e "$BUILD_TARGET_DIR_NAME" ];then
                fxnERR "Failed to create [ $BUILD_TARGET_DIR_NAME ]"
            fi
        else
            echo "Found existing $BUILD_TARGET_DIR_NAME directory for [ $SOURCE_RPM ]"
        fi
        
        ######################################
        # Update dependencies
        ######################################

        # Install build dependencies:

        # May need to run 'zypper repos' to confirm repo-source is enabled, if getting a source package."
        # sudo zypper mr -e repo-source  <- to enable
        # Installing package sources, which should include dependencies
        sudo zypper source-install "$SOURCE_RPM"
        echo "Installing build dependencies for $PACKAGE_NAME"
        #fxnEC sudo $RESOLVE_DEPENDENCIES ~/rpmbuild/SPECS/${PACKAGE_NAME}.spec || exit 1


        fxnEC sudo zypper in -d "$PACKAGE_NAME" || exit 1


        BUILD_START_TIME=$(date)
        if [ "$RPM_BUILD_OPTIONS" = "" ];then
            # If not explicitly stated by the user, default.
            RPM_BUILD_OPTIONS="$RPM_BUILD_OPTIONS_DEFAULT"
        fi
        buildCommand="rpmbuild $RPM_BUILD_OPTIONS $BUILD_TARGET_DIR_NAME/SPECS/${PACKAGE_NAME}.spec"
    fi    
    ######################################
    # Build
    ######################################

    if [ "$(which rpmbuild)" = "" ];then
        # get the rpmbuild binary
        sudo zypper install rpm-build
    fi
    sourceRPMDir="$(pwd)"
    while [ $(( BUILD_ATTEMPTS > 0 )) = 1 ]; do
        fxnMSG "Building source from [ $sourceRPMDir ] in [ $BUILD_TARGET_DIR_NAME ] with [ $BUILD_ARGS ].  Attempt [ $BUILD_ATTEMPTS ]. "

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

        # And BUILD_ARGS was set

		if [ "$RAW_BUILD_COMMAND" != "" ];then
            failMsg="$RAW_BUILD_COMMAND failed with error "
            fxnMSG "Building in [ $BUILD_TARGET_DIR_NAME ] with [ $RAW_BUILD_COMMAND ].  Attempt [ $BUILD_ATTEMPTS ]. "
			# Run a non-default build command after setup
            echo "bash -c  $RAW_BUILD_COMMAND"
            bash -c " $RAW_BUILD_COMMAND"
        else
            failMsg="dpkg-buildpackage $BUILD_ARGS failed with error "
            fxnMSG "Building in [ $BUILD_TARGET_DIR_NAME ] with [ $BUILD_ARGS ].  Attempt [ $BUILD_ATTEMPTS ]. "
            # And ADDITIONAL_BUILD_ARGS was set
            echo "bash -c $buildCommand "
			bash -c "$buildCommand"
        fi

        #        bash -c " echo 'Build command goes here in duebuild script.'"
        result="$?"


        case "$result" in
            0 )
                echo "Success: [ $buildCommand ]"
                BUILD_ATTEMPTS='0'
                ;;
            * )
                # Retry until the tries run out.
                echo "Build failed with error code [ $result ]."
                if [ "$BUILD_ATTEMPTS" = "0" ];then
                    fxnERR "Build $BUILD_ARGS failed with [ $result ]"
                    exit $result
                fi
        esac

    done
    echo ""


    fxnMSG "Built [ $BUILD_TARGET_DIR_NAME ] - list follows:"
    
    find "$BUILD_TARGET_DIR_NAME" -iname \*.rpm

    echo ""
}

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

# Default to one build attempt
BUILD_ATTEMPTS="1"

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

# RPMs default to building in the user's home directory.
BUILD_TARGET_DIR_NAME="${HOME}/rpmbuild"


##################################################
#                                                #
# 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"
            ;;

        --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
            ;;

		# Take --build as a synonym for --cbuild, although --cbuild is clearer.
        -c | --cbuild | --build  )
            # Default to build everything in container context.
            # skip over --cbuild	
            DO_BUILD="TRUE"
			if [ "$2" != "" ];then
                SOURCE_RPM="$2"
                shift
                if [ "$2" != "" ];then
				    # The rest of the arguments passed should be given verbatim to
				    # whatever the build command is.
				    ADDITIONAL_BUILD_ARGS="$*"
                fi
			fi
            ;;

        --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
            ;;

        --rpm-build-option )
            # additional options to rpmbuild
            RPM_BUILD_OPTIONS+=" $2 "
            shift
            ;;

        
		--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"
			;;

        -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
            ;;

		
        --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
            ;;
		
        --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


# Arguments passed to the dpkg build. This gets
# appended by fxnParseBuildArgs
if [ "$DO_BUILD" = "TRUE" ];then

    if [ "$ADDITIONAL_BUILD_ARGS" != "" ];then
        # User is overriding defaults.
        # take it literally with no defaults.
        BUILD_ARGS="$ADDITIONAL_BUILD_ARGS"
    fi

fi

# Add parallel build
if [ "$BUILD_JOBS" != "" ];then
    BUILD_ARGS+=" -j${BUILD_JOBS} "
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

#
# Dump what's being run
#
fxnPrintConfig

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

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

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

	BUILD_LOG_FILE="example-build.log"
    #
    # actually do the build
    #

    fxnPP "Building with dev version $DEV_PACKAGE_VERSION"
    fxnDoBuild  2>&1 | tee -a  "$BUILD_LOG_FILE"
fi    

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

if [ "$RETURN_CODE" = "0" ];then
    fxnPP "Success"
	# Add post build actions
    fxnPP "Build finished at [ $(date) ]"
else
    fxnERR "Build FAILED with return code [ $RETURN_CODE ] at [ $(date) ]"
fi

exit "$RETURN_CODE"
