#!groovy

//===========================================================================================================
// Main loop of the compilation
//===========================================================================================================
node ('master'){

	// Globals
	BuildDir  = pwd tmp: true
	SrcDir    = pwd tmp: false
	Settings  = null
	StageName = ''

	// Local variables
	def err = null
	def log_needed = false

	currentBuild.result = "SUCCESS"

	try {
		//Wrap build to add timestamp to command line
		wrap([$class: 'TimestamperBuildWrapper']) {

			notify_server(0)

			Settings = prepare_build()

			clean()

			checkout()

			notify_server(0)

			build()

			test()

			benchmark()

			build_doc()

			publish()

			notify_server(45)
		}
	}

	//If an exception is caught we need to change the status and remember to
	//attach the build log to the email
	catch (Exception caughtError) {
		//rethrow error later
		err = caughtError

		echo err.toString()

		//An error has occured, the build log is relevent
		log_needed = true

		//Store the result of the build log
		currentBuild.result = "${StageName} FAILURE".trim()
	}

	finally {
		//Send email with final results if this is not a full build
		if( Settings && !Settings.Silent ) {
			email(log_needed, Settings.IsSandbox)
		}

		echo 'Build Completed'

		/* Must re-throw exception to propagate error */
		if (err) {
			throw err
		}
	}
}

//===========================================================================================================
// Main compilation routines
//===========================================================================================================
def clean() {
	build_stage('Cleanup') {
		// clean the build by wipping the build directory
		dir(BuildDir) {
			deleteDir()
		}
	}
}

//Compilation script is done here but environnement set-up and error handling is done in main loop
def checkout() {
	build_stage('Checkout') {
		//checkout the source code and clean the repo
		final scmVars = checkout scm
		Settings.GitNewRef = scmVars.GIT_COMMIT
		Settings.GitOldRef = scmVars.GIT_PREVIOUS_COMMIT

		echo GitLogMessage()
	}
}

def build() {
	build_stage('Build') {
		// Build outside of the src tree to ease cleaning
		dir (BuildDir) {
			//Configure the conpilation (Output is not relevant)
			//Use the current directory as the installation target so nothing escapes the sandbox
			//Also specify the compiler by hand
			targets=""
			if( Settings.RunAllTests ) {
				targets="--with-target-hosts='host:debug,host:nodebug'"
			} else {
				targets="--with-target-hosts='host:debug'"
			}

			sh "${SrcDir}/configure CXX=${Settings.Compiler.cpp_cc} ${Settings.Architecture.flags} ${targets} --with-backend-compiler=${Settings.Compiler.cfa_cc} --quiet"

			//Compile the project
			sh 'make -j 8 --no-print-directory'
		}
	}
}

def test() {
	build_stage('Test') {

		dir (BuildDir) {
			//Run the tests from the tests directory
			if ( Settings.RunAllTests ) {
				sh 'make --no-print-directory -C tests all-tests debug=yes'
				sh 'make --no-print-directory -C tests all-tests debug=no '
			}
			else {
				sh 'make --no-print-directory -C tests'
			}
		}
	}
}

def benchmark() {
	build_stage('Benchmark') {

		if( !Settings.RunBenchmark ) return

		dir (BuildDir) {
			//Append bench results
			sh "make --no-print-directory -C benchmark jenkins githash=${gitRefNewValue} arch=${Settings.Architecture} | tee ${SrcDir}/bench.json"
		}
	}
}

def build_doc() {
	build_stage('Documentation') {

		if( !Settings.BuildDocumentation ) return

		dir ('doc/user') {
			make_doc()
		}

		dir ('doc/refrat') {
			make_doc()
		}
	}
}

def publish() {
	build_stage('Publish') {

		if( !Settings.Publish ) return

		//Then publish the results
		sh 'curl --silent --show-error -H \'Content-Type: application/json\' --data @bench.json https://cforall.uwaterloo.ca:8082/jenkins/publish > /dev/null || true'
	}
}

//===========================================================================================================
//Routine responsible of sending the email notification once the build is completed
//===========================================================================================================
def gitUpdate(String gitRefOldValue, String gitRefNewValue) {
	def update = ""
	sh "git rev-list ${gitRefOldValue}..${gitRefNewValue} > GIT_LOG";
	readFile('GIT_LOG').eachLine { rev ->
		sh "git cat-file -t ${rev} > GIT_TYPE"
		def type = readFile('GIT_TYPE')

		update += "       via  ${rev} (${type})\n"
	}
	def rev = gitRefOldValue
	sh "git cat-file -t ${rev} > GIT_TYPE"
	def type = readFile('GIT_TYPE')

	update += "      from  ${rev} (${type})\n"
	return update
}

def gitLog(String gitRefOldValue, String gitRefNewValue) {
	sh "git rev-list --format=short ${oldRef}...${newRef} > ${BuildDir}/GIT_LOG"
	return readFile("${BuildDir}/GIT_LOG")
}

def gitDiff(String gitRefOldValue, String gitRefNewValue) {
	sh "git diff --stat ${newRef} ${oldRef} > ${BuildDir}/GIT_DIFF"
	return readFile("${BuildDir}/GIT_DIFF")
}

def GitLogMessage() {
	if (!Settings || !Settings.GitOldRef || !Settings.GitNewRef) return "\nERROR retrieveing git information!\n"

	return """
The branch ${env.BRANCH_NAME} has been updated.
${gitUpdate(Settings.GitOldRef, Settings.GitNewRef)}

Check console output at ${env.BUILD_URL} to view the results.

- Status --------------------------------------------------------------

BUILD# ${env.BUILD_NUMBER} - ${currentBuild.result}

- Log -----------------------------------------------------------------
${gitLog(Settings.GitOldRef, Settings.GitNewRef)}
-----------------------------------------------------------------------
Summary of changes:
${gitDiff(Settings.GitOldRef, Settings.GitNewRef)}
"""
}

//Standard build email notification
def email(boolean log, boolean bIsSandbox) {
	//Since tokenizer doesn't work, figure stuff out from the environnement variables and command line
	//Configurations for email format
	echo 'Notifying users of result'

	def project_name = (env.JOB_NAME =~ /(.+)\/.+/)[0][1].toLowerCase()
	def email_subject = "[${project_name} git][BUILD# ${env.BUILD_NUMBER} - ${currentBuild.result}] - branch ${env.BRANCH_NAME}"
	def email_body = """This is an automated email from the Jenkins build machine. It was
generated because of a git hooks/post-receive script following
a ref change which was pushed to the Cforall repository.
""" + GitLogMessage()

	def email_to = "cforall@lists.uwaterloo.ca"

	if( Settings && !Settings.IsSandbox ) {
		//send email notification
		emailext body: email_body, subject: email_subject, to: email_to, attachLog: log
	} else {
		echo "Would send email to: ${email_to}"
		echo "With title: ${email_subject}"
		echo "Content: \n${email_body}"
	}
}

//===========================================================================================================
// Helper classes/variables/routines
//===========================================================================================================
//Description of a compiler (Must be serializable since pipelines are persistent)
class CC_Desc implements Serializable {
	public String cc_name
	public String cpp_cc
	public String cfa_cc

	CC_Desc(String cc_name, String cpp_cc, String cfa_cc) {
		this.cc_name = cc_name
		this.cpp_cc = cpp_cc
		this.cfa_cc = cfa_cc
	}
}

//Description of an architecture (Must be serializable since pipelines are persistent)
class Arch_Desc implements Serializable {
	public String name
	public String flags

	Arch_Desc(String name, String flags) {
		this.name  = name
		this.flags = flags
	}
}

class BuildSettings implements Serializable {
	public final CC_Desc Compiler
	public final Arch_Desc Architecture
	public final Boolean RunAllTests
	public final Boolean RunBenchmark
	public final Boolean BuildDocumentation
	public final Boolean Publish
	public final Boolean Silent
	public final Boolean IsSandbox
	public final String DescLong
	public final String DescShort

	public String GitNewRef
	public String GitOldRef

	BuildSettings(java.util.Collections$UnmodifiableMap param, String branch) {
		switch( param.Compiler ) {
			case 'gcc-6':
				this.Compiler = new CC_Desc('gcc-6', 'g++-6', 'gcc-6')
			break
			case 'gcc-5':
				this.Compiler = new CC_Desc('gcc-5', 'g++-5', 'gcc-5')
			break
			case 'gcc-4.9':
				this.Compiler = new CC_Desc('gcc-4.9', 'g++-4.9', 'gcc-4.9')
			break
			case 'clang':
				this.Compiler = new CC_Desc('clang', 'clang++', 'gcc-6')
			break
			default :
				error "Unhandled compiler : ${cc}"
		}

		switch( param.Architecture ) {
			case 'x64':
				this.Architecture = new Arch_Desc('x64', '--host=x86_64')
			break
			case 'x86':
				this.Architecture = new Arch_Desc('x86', '--host=i386')
			break
			default :
				error "Unhandled architecture : ${arch}"
		}

		this.RunAllTests        = param.RunAllTests
		this.RunBenchmark       = param.RunBenchmark
		this.BuildDocumentation = param.BuildDocumentation
		this.Publish            = param.Publish
		this.Silent             = param.Silent
		this.IsSandbox          = (branch == "jenkins-sandbox")

		def full = param.RunAllTests ? " (Full)" : ""
		this.DescShort = "${ this.Compiler.cc_name }:${ this.Architecture.name }${full}"

		this.DescLong = """Compiler 	         : ${ this.Compiler.cc_name } (${ this.Compiler.cpp_cc }/${ this.Compiler.cfa_cc })
Architecture            : ${ this.Architecture.name }
Arc Flags               : ${ this.Architecture.flags }
Run All Tests           : ${ this.RunAllTests.toString() }
Run Benchmark           : ${ this.RunBenchmark.toString() }
Build Documentation     : ${ this.BuildDocumentation.toString() }
Publish                 : ${ this.Publish.toString() }
Silent                  : ${ this.Silent.toString() }
"""

		this.GitNewRef = ''
		this.GitOldRef = ''
	}
}

def prepare_build() {
	// prepare the properties
	properties ([ 													\
		[$class: 'ParametersDefinitionProperty', 								\
			parameterDefinitions: [ 									\
				[$class: 'ChoiceParameterDefinition',						\
					description: 'Which compiler to use',					\
					name: 'Compiler',									\
					choices: 'gcc-6\ngcc-5\ngcc-4.9\nclang',					\
					defaultValue: 'gcc-6',								\
				],												\
				[$class: 'ChoiceParameterDefinition',						\
					description: 'The target architecture',					\
					name: 'Architecture',								\
					choices: 'x64\nx86',								\
					defaultValue: 'x64',								\
				],												\
				[$class: 'BooleanParameterDefinition',  						\
					description: 'If false, only the quick test suite is ran', 		\
					name: 'RunAllTests', 								\
					defaultValue: false,  								\
				], 												\
				[$class: 'BooleanParameterDefinition',  						\
					description: 'If true, jenkins also runs benchmarks', 		\
					name: 'RunBenchmark', 								\
					defaultValue: false,  								\
				], 												\
				[$class: 'BooleanParameterDefinition',  						\
					description: 'If true, jenkins also builds documentation', 		\
					name: 'BuildDocumentation', 							\
					defaultValue: true,  								\
				],												\
				[$class: 'BooleanParameterDefinition',  						\
					description: 'If true, jenkins also publishes results', 		\
					name: 'Publish', 									\
					defaultValue: false,  								\
				],												\
				[$class: 'BooleanParameterDefinition',  						\
					description: 'If true, jenkins will not send emails', 		\
					name: 'Silent', 									\
					defaultValue: false,  								\
				],												\
			],
		]])

	final settings = new BuildSettings(params, env.BRANCH_NAME)

	currentBuild.description = settings.DescShort
	echo                       settings.DescLong

	return settings
}

def build_stage(String name, Closure block ) {
	StageName = name
	echo " -------- ${StageName} -------- "
	stage(name, block)
}

def notify_server(int wait) {
	sh """curl --silent --show-error --data "wait=${wait}" -X POST https://cforall.uwaterloo.ca:8082/jenkins/notify > /dev/null || true"""
	return
}

def make_doc() {
	def err = null
	try {
		sh 'make clean > /dev/null'
		sh 'make > /dev/null 2>&1'
	}
	catch (Exception caughtError) {
		err = caughtError //rethrow error later
		sh 'cat *.log'
	}
	finally {
		if (err) throw err // Must re-throw exception to propagate error
	}
}