#!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']) {

			Settings = prepare_build()

			node(Settings.Architecture.node) {
				BuildDir  = pwd tmp: true
				SrcDir    = pwd tmp: false

				clean()

				checkout()

				build()

				test()

				benchmark()

				build_doc()

				publish()
			}

			// Update the build directories when exiting the node
			BuildDir  = pwd tmp: true
			SrcDir    = pwd tmp: false
		}
	}

	//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
		email(log_needed)

		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 || Settings.RunBenchmark ) {
				targets="--with-target-hosts='host:debug,host:nodebug'"
			} else {
				targets="--with-target-hosts='host:debug'"
			}

			sh "${SrcDir}/configure CXX=${Settings.Compiler.CXX} CC=${Settings.Compiler.CC} ${Settings.Architecture.flags} ${targets} --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 timeouts="--timeout=600" all-tests debug=yes'
				sh 'make --no-print-directory -C tests timeouts="--timeout=600" 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"
		}
	}
}

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
		if( !Settings.RunBenchmark ) {
			echo 'No results to publish!!!'
			return
		}

		//Then publish the results
		do_plot('compile', 'Compilation', 'Compilation')

		do_plot('ctxswitch', 'Concurrency', 'Context Switching')

		do_plot('mutex', 'Concurrency', 'Mutual Exclusion')

		do_plot('signal', 'Concurrency', 'Internal and External Scheduling')
	}
}

//===========================================================================================================
//Routine responsible of sending the email notification once the build is completed
//===========================================================================================================
def GitLogMessage() {
	if (!Settings || !Settings.GitOldRef || !Settings.GitNewRef) return "\nERROR retrieveing git information!\n"

	sh "${SrcDir}/tools/PrettyGitLogs.sh ${SrcDir} ${BuildDir} ${Settings.GitOldRef} ${Settings.GitNewRef}"

	def gitUpdate = readFile("${BuildDir}/GIT_UPDATE")
	def gitLog    = readFile("${BuildDir}/GIT_LOG")
	def gitDiff   = readFile("${BuildDir}/GIT_DIFF")

	return """
<pre>
The branch ${env.BRANCH_NAME} has been updated.
${gitUpdate}
</pre>

<p>Check console output at ${env.BUILD_URL} to view the results.</p>

<p>- Status --------------------------------------------------------------</p>

<p>BUILD# ${env.BUILD_NUMBER} - ${currentBuild.result}</p>

<p>- Log -----------------------------------------------------------------</p>

<pre>
${gitLog}
</pre>

<p>-----------------------------------------------------------------------</p>
<pre>
Summary of changes:
${gitDiff}
</pre>
"""
}

//Standard build email notification
def email(boolean log) {
	//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 = """<p>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 C∀ repository.</p>
""" + GitLogMessage()

	def email_to = !Settings.IsSandbox ? "cforall@lists.uwaterloo.ca" : "tdelisle@uwaterloo.ca"

	if( Settings && !Settings.Silent ) {
		//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 name
	public String CXX
	public String CC

	CC_Desc(String name, String CXX, String CC) {
		this.name = name
		this.CXX = CXX
		this.CC = CC
	}
}

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

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

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', 'x64')
			break
			case 'x86':
				this.Architecture = new Arch_Desc('x86', '--host=i386', 'x86')
			break
			default :
				error "Unhandled architecture : ${arch}"
		}

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

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

		this.DescLong = """Compiler 	         : ${ this.Compiler.name } (${ this.Compiler.CXX }/${ this.Compiler.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,  								\
				],												\
			],
		]])

	// It's unfortunate but it looks like we need to checkout the entire repo just to get the pretty git printer
	checkout scm

	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 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 build/*.log'
	}
	finally {
		if (err) throw err // Must re-throw exception to propagate error
	}
}

def do_plot(String file, String group, String title) {
	def data = readFile "${BuildDir}/benchmark/${file}.csv"
	echo data
	writeFile file: 'data.csv', text: data

		plot csvFileName: "cforall-${env.BRANCH_NAME}-${file}.csv",
			csvSeries: [[
				file: 'data.csv',
				exclusionValues: '',
				displayTableFlag: false,
				inclusionFlag: 'OFF',
				url: ''
			]],
			group: "${group}",
			title: "${title}",
			style: 'lineSimple',
			exclZero: false,
			keepRecords: false,
			logarithmic: false,
			numBuilds: '120',
			useDescr: true,
			yaxis: '',
			yaxisMaximum: '',
			yaxisMinimum: ''
}
