#!/bin/bash
# sbmake - invoke make within a sandbox
# Copyright (C) 2012  - 2019 FARGOS Development, LLC
# $Id$
# Invoke make within a sandbox:
# + makefile can be retrieved from a backing tree
# + parallel jobs performed by default, limited by number of processors
#   and available memory
# + runs cleanCompletionLocks on error
# + will run cleanDependencies if backing tree's lastFileDeleted.timestamp is
#   modified to allow regeneration of local Makefile dependency files.
# + can automatically regenerate Makefile if it is programatically
#   derived from another source.
# + will output error message to TTY if make fails, allowing output to be
#   redirected to a log file.
originDir=`dirname $0`
clearLDPATH=${CLEAR_LD_PATH:-1}
if test "${clearLDPATH}" = "1"
then
	# We don't expect any build tools to require a special LD_LIBRARY_PATH
	unset LD_LIBRARY_PATH
fi

if test -z "${SB_MAKE_COMMAND}"
then
	# skip-alias will fail if which is implemented via busybox
	makeCmd=`which --skip-alias ${SB_DEFAULT_MAKE_CMD:-make} 2>/dev/null`
	if test -z "${makeCmd}"
	then
		makeCmd=`which ${SB_DEFAULT_MAKE_CMD:-make} 2>/dev/null`
	fi
	# if makeCmd did not get set at all, give up on absolute path
	SB_MAKE_COMMAND="${makeCmd:-make}"
	export SB_MAKE_COMMAND
fi
makeCmd="${SB_MAKE_COMMAND}"

# Normally, make sets RM to "rm -f", which causes the PATH to be searched
RM="${RM:-/bin/rm -f}"; export RM
searchList=""
makeArgs=""
curDir=`pwd`
parallel=""
outSync="${DEFAULT_MAKE_OUTPUT_SYNC:--output-sync=target}"
autoRegen=${SBMAKE_AUTOREGEN:-0}
makeLevel=${MAKELEVEL:-0}
vimRCset=${MYVIMRC:+1}
noTimestamp=${vimRCset:-0}

while test $# -gt 0
do
	case "${1}" in
	-h | -help | --help)
		printf "usage: %s [-auto] [{-j jobs|-l load}] [-m makeCmd] [-C dir] [-f file][...]\n" "${0}" >&2
		exit 1
		;;
	-auto)
		autoRegen=1
		;;
	-noauto)
		autoRegen=0
		;;
	-notimestamp)
		noTimestamp=1
		;;
	-f)
		searchList="${searchList}${searchList:+ }${2}"
		shift
		;;
	-C)
		cd "${2}"
		${originDir}/colorText -green `basename ${0}`": Enterered" `pwd`", left ${curDir}" -reset
		curDir=`pwd`
		shift
		;;
	-m)
		makeCmd="${2}"
		shift
		;;
	--output-sync=* | -O)
		outSync="${1}"
		shift
		;;
	-j) # value passed as second argument
		parallel="--jobs=${2}"
		shift
		;;
	-j[0-9]*) # number concatenated with flag
#		num=`expr substr ${1} 3 '(' length ${1} - 2 ')'`
		num="${1:2}"
		parallel="--jobs=${num}"
		;;
	--jobs=*)
#		num=`expr substr ${1} 8 '(' length ${1} - 7 ')'`
		num="${1:7}"
		parallel="--jobs=${num}"
		;;
	-l)
		parallel="--load-average=${2}"
		shift
		;;
	*)
		makeArgs="${makeArgs}${makeArgs:+ }$1"
		;;
	esac
	shift
done
#set -x
if test ${makeLevel} -eq 0 -a -n "${STO}"
then # topmost invocation in sandbox
	lastFileDeleteTimestamp="${BT_FINAL}/obj/${SB_TARGET_ARCH}/lastFileDelete.timestamp"
	lastCleanDependenciesTimestamp="${STO}/lastCleanDependencies.timestamp"
	if test '(' ! -r ${lastCleanDependenciesTimestamp} ')' -o ${lastFileDeleteTimestamp} -nt ${lastCleanDependenciesTimestamp} -o ${BT_FINAL}/src -nt ${lastCleanDependenciesTimestamp}
	then
		${originDir}/colorText -blue "${0}: performing" -green "cleanDependencies" -blue "due to source file removal" -reset >&2
		cleanDependencies
		touch ${lastCleanDependenciesTimestamp}
	fi
fi

SBMAKE_AUTOREGEN=${autoRegen}; export SBMAKE_AUTOREGEN
# export name of this program
SBMAKE_PROG="${0}"; export SBMAKE_PROG

# If no specification and topmost make, use available CPUs
if test -z "${parallel}" -a ${makeLevel} = "0"
then
	processors=`getconf _NPROCESSORS_ONLN 2>/dev/null`
	if test -z "${processors}"
	then # MacOS
		processors=`getconf NPROCESSORS_ONLN 2>/dev/null`
	fi
	pages=`getconf _PHYS_PAGES 2>/dev/null`
	if test -z "${pages}"
	then
		pages=`getconf PHYS_PAGES 2>/dev/null`
		if test -z "${pages}"
		then # assume MacOS - use vm_stat
			pages=`vm_stat -c 1 | awk '/Pages free:/ { print substr($NF, 1, length($NF) - 1); exit; }'`
			if test -z "${pages}"
			then # assume enough memory
				pages=`expr ${processors} '*' 500000`
			fi
		fi
	fi
	maxJobsForMemory=`expr ${pages} / 500000`
	parallelJobs=`expr ${processors:-1} - 1`
	if test ${parallelJobs} -gt ${maxJobsForMemory}
	then
		parallelJobs="${maxJobsForMemory}"
	fi
	if test ${parallelJobs} -lt 2
	then # lower bound is 2
		parallelJobs=2
	fi
	parallel="-j ${parallelJobs}"
fi

if test -z "${searchList}"
then
	searchList="sb.Makefile Makefile makefile"
fi

for searchFor in ${searchList} # list of files names
do
	makeFile=`${originDir}/findFileInSB -q -d . -all ${searchFor}`
	if test -n "${makeFile}"
	then
		break
	fi
done

if test -z "${makeFile}"
then
	${originDir}/colorText -red "${0}: cannot locate any ${searchList} in sandbox in" `pwd` -reset >&2
	if test ! -t 2
	then
		ttyName=`tty`
		if test -n "${ttyName}"
		then
			${originDir}/colorText -red "${0}: cannot locate any ${searchList} in sandbox in" `pwd` -reset > ${ttyName}
		fi
	fi
	exit 2
fi

make_fileName=`basename ${makeFile}`
##make_prefix=`expr substr "${make_fileName}" 1 3`
make_prefix="${make_fileName:0:3}"
if test "${make_prefix}" = "sb."
then
##	orig_make_fileName=`expr substr "${make_fileName}" 4 '(' length "${make_fileName}" - 3 ')'`
	orig_make_fileName="${make_fileName:3}"
	# To match sb.Makefile derived from "makefile" rather than "Makefile"
	# convert first character to lowercase and see if filename is different
##	altFilePrefix=`expr substr ${orig_make_fileName} 1 1 | tr '[:upper:]' '[:lower:]'`
	altFilePrefix=`echo ${orig_make_fileName:0:1} | tr '[:upper:]' '[:lower:]'`
##	altFileSuffix=`expr substr ${orig_make_fileName} 2 '(' length ${orig_make_fileName} - 1 ')'`
	altFileSuffix="${orig_make_fileName:1}"
	altFile="${altFilePrefix}${altFileSuffix}"
	if test "${altFile}" = "${orig_make_fileName}"
	then # same, so we do not look for it
		altFile=""
	fi
	for searchFor in ${orig_make_fileName} ${altFile} # list of files names
	do
		origMakeFile=`${originDir}/findFileInSB -q -d . -all ${searchFor}`
		if test -n "${origMakeFile}"
		then
			break
		fi
	done
	if test -n "${origMakeFile}"
	then # found original Makefile
		if test ${makeFile} -ot ${origMakeFile}
		then
			${originDir}/colorText -red "WARNING: ${makeFile} is older than ${origMakeFile}" -reset >&2
			if test ${autoRegen} -eq 0
			then
				${originDir}/colorText -cyan 'Suggest:  genNewMakefile -f' ${origMakeFile} '>' sb.${orig_make_fileName} -reset >&2
			else
				${originDir}/colorText -yellow "Attempting automatic regeneration of ${makeFile} from ${origMakeFile}" -reset >&2
				makeFile="sb.${orig_make_fileName}"
				${originDir}/colorText -cyan "Invoking genNewMakefile -f ${origMakeFile} -o sb.${orig_make_fileName}" -reset
				${originDir}/genNewMakefile -f ${origMakeFile} -o sb.${orig_make_fileName}
				if test sb.${orig_make_fileName} -ot ${origMakeFile}
				then # nothing changed
					 ${originDir}/colorText -red "${0}:  FAILED to regenerate ${makeFile}" -reset >&2
					exit 1
				fi
			fi
		fi
	fi
fi

# found file
if test ${noTimestamp} -eq 0
then
	nowTime=`date +%T`
	startSeconds=`date +%s`
	${originDir}/colorText -blue "${nowTime}" In ${curDir} Invoking ${makeCmd} ${parallel} ${outSync} -f ${makeFile} ${makeArgs} -reset
fi
trap "${originDir}/cleanCompletionLocks" INT QUIT TERM
/usr/bin/nice ${makeCmd} ${parallel} ${outSync} -f ${makeFile} ${makeArgs}
rc=$? # save exit status from make
if test ${rc} -ne 0
then
	if test "${makeLevel}" = "0"
	then
		${originDir}/colorText -red "Cleaning locks on error exit" -reset
		${originDir}/cleanCompletionLocks
		if test -t 0
		then # standard in associated with terminal
			ttyName=`tty`
			if test -n "${ttyName}"
			then
			${originDir}/colorText -red "${makeCmd} -f ${makeFile} ${makeArgs}" -blink "exited with error code ${rc}" -reset > ${ttyName}
			fi
		fi
	else
		${originDir}/colorText -red "In nested make at level ${makeLevel} in ${curDir} with path ${MAKEFILE_LIST}, deferring lock cleanup." -reset
	fi
fi
if test ${noTimestamp} -eq 0
then
	nowSeconds=`date +%s`
	duration=`expr ${nowSeconds} - ${startSeconds}`
	nowTime=`date +%T`
	${originDir}/colorText -cyan "${nowTime} In ${curDir} build ${makeCmd} -f ${makeFile} ${makeArgs} required ${duration} seconds" -reset
fi
exit ${rc} # return exit status from make
