#!groovy

import groovy.transform.Field

//===========================================================================================================
// Main loop of the compilation
//===========================================================================================================

// Globals
BuildDir  = null
SrcDir    = null
Settings  = null
Tools     = null

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

currentBuild.result = "SUCCESS"

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

	node(Settings.Architecture.node) {
		//Wrap build to add timestamp to command line
		wrap([$class: 'TimestamperBuildWrapper']) {
			BuildDir  = pwd tmp: true
			SrcDir    = pwd tmp: false
			currentBuild.description = "${currentBuild.description} on ${env.NODE_NAME}"

			Tools.Clean()

			Tools.Checkout()

			build()

			test()

			benchmark()

			build_doc()

			publish()
		}
	}
}

//If an exception is caught we need to change the status and remember to
//attach the build log to the email
catch (Exception caughtError) {
	// Store the result of the build log
	currentBuild.result = "FAILURE"

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

	// rethrow error later
	err = caughtError

	// print the error so it shows in the log
	echo err.toString()
}

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 build() {
	debug = true
	release = Settings.RunAllTests || Settings.RunBenchmark
	Tools.BuildStage('Build : configure', true) {
		// Configure must be run inside the tree
		dir (SrcDir) {
			// Generate the necessary build files
			sh './autogen.sh'
		}

		// Build outside of the src tree to ease cleaning
		dir (BuildDir) {
			//Configure the compilation (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} AR=gcc-ar RANLIB=gcc-ranlib ${targets} --quiet --prefix=${BuildDir}"

			// Configure libcfa
			sh 'make -j $(nproc) --no-print-directory configure-libcfa'
		}
	}

	Tools.BuildStage('Build : cfa-cpp', true) {
		// Build outside of the src tree to ease cleaning
		dir (BuildDir) {
			// Build driver
			sh 'make -j $(nproc) --no-print-directory -C driver'

			// Build translator
			sh 'make -j $(nproc) --no-print-directory -C src'
		}
	}

	Tools.BuildStage('Build : libcfa(debug)', debug) {
		// Build outside of the src tree to ease cleaning
		dir (BuildDir) {
			sh "make -j \$(nproc) --no-print-directory -C libcfa/${Settings.Architecture.name}-debug"
		}
	}

	Tools.BuildStage('Build : libcfa(nodebug)', release) {
		// Build outside of the src tree to ease cleaning
		dir (BuildDir) {
			sh "make -j \$(nproc) --no-print-directory -C libcfa/${Settings.Architecture.name}-nodebug"
		}
	}

	Tools.BuildStage('Build : install', true) {
		// Build outside of the src tree to ease cleaning
		dir (BuildDir) {
			sh 'make -j $(nproc) --no-print-directory install'
		}
	}
}

def test() {
	try {
		// Print potential limits before testing
		// in case jenkins messes with them
		sh 'free -h'
		sh 'ulimit -a'

		jopt = '-j $(nproc)'

		Tools.BuildStage('Test: Debug', true) {
			dir (BuildDir) {
				//Run the tests from the tests directory
				sh """make ${jopt} --no-print-directory -C tests timeout=600 global-timeout=14400 tests debug=yes archive-errors=${BuildDir}/tests/crashes/full-debug"""
			}
		}

		Tools.BuildStage('Test: Release', Settings.RunAllTests) {
			dir (BuildDir) {
				//Run the tests from the tests directory
				sh """make ${jopt} --no-print-directory -C tests timeout=600 global-timeout=14400 tests debug=no archive-errors=${BuildDir}/tests/crashes/full-nodebug"""
			}
		}
	}
	catch (Exception err) {
		echo "Archiving core dumps"
		dir (BuildDir) {
			def exists = fileExists 'tests/crashes'
			if( exists ) {
				sh """${SrcDir}/tools/jenkins/archive-gen.sh"""
				archiveArtifacts artifacts: "tests/crashes/**/*,lib/**/lib*.so*,setup.sh", fingerprint: true
			}
		}
		throw err
	}
}

def benchmark() {
	Tools.BuildStage('Benchmark', Settings.RunBenchmark) {
		dir (BuildDir) {
			//Append bench results
			sh "make --no-print-directory -C benchmark jenkins arch=${Settings.Architecture.name}"
		}
	}
}

def build_doc() {
	Tools.BuildStage('Documentation', Settings.BuildDocumentation) {
		dir ('doc/user') {
			make_doc()
		}

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

def publish() {
	Tools.BuildStage('Publish', true) {

		if( Settings.Publish && !Settings.RunBenchmark ) { echo 'No results to publish!!!' }
	}
}

//===========================================================================================================
//Routine responsible of sending the email notification once the build is completed
//===========================================================================================================
//Standard build email notification
def email(boolean log) {
	node {
		//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\u2200 repository.</p>

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

<p>BUILD# ${env.BUILD_NUMBER} - ${currentBuild.result}</p>
<p>Check console output at ${env.BUILD_URL} to view the results.</p>
""" + Tools.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
	public String lto

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

//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-4.9':
			//	this.Compiler = new CC_Desc('gcc-4.9', 'g++-4.9', 'gcc-4.9', '-flto=auto')
			// break
			// case 'gcc-5':
			//	this.Compiler = new CC_Desc('gcc-5', 'g++-5', 'gcc-5', '-flto=auto')
			// break
			// case 'gcc-6':
			//	this.Compiler = new CC_Desc('gcc-6', 'g++-6', 'gcc-6', '-flto=auto')
			// break
			// case 'gcc-7':
			//	this.Compiler = new CC_Desc('gcc-7', 'g++-7', 'gcc-7', '-flto=auto')
			// break
			// case 'gcc-8':
			//	this.Compiler = new CC_Desc('gcc-8', 'g++-8', 'gcc-8', '-flto=auto')
			// break
			case 'gcc-9':
				this.Compiler = new CC_Desc('gcc-9', 'g++-9', 'gcc-9', '-flto=auto')
			break
			case 'gcc-10':
				this.Compiler = new CC_Desc('gcc-10', 'g++-10', 'gcc-10', '-flto=auto')
			break
			case 'gcc-11':
				this.Compiler = new CC_Desc('gcc-11', 'g++-11', 'gcc-11', '-flto=auto')
			break
			case 'gcc-12':
				this.Compiler = new CC_Desc('gcc-12', 'g++-12', 'gcc-12', '-flto=auto')
			break
			case 'clang':
				this.Compiler = new CC_Desc('clang', 'clang++', 'clang', '-flto=thin -flto-jobs=0')
			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
			// case 'arm64':
			// 	this.Architecture = new Arch_Desc('arm64', '--host=aarch64', 'arm64')
			// break
			default :
				error "Unhandled architecture : ${arch}"
		}

		this.IsSandbox          = (branch == "jenkins-sandbox")
		this.RunAllTests        = param.RunAllTests
		this.RunBenchmark       = param.RunBenchmark
		this.BuildDocumentation = param.BuildDocumentation
		this.Publish            = param.Publish
		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 ([ 										\
		buildDiscarder(logRotator(							\
			artifactDaysToKeepStr: '',						\
			artifactNumToKeepStr: '',						\
			daysToKeepStr: '730',							\
			numToKeepStr: '1000'							\
		)),										\
		[$class: 'ParametersDefinitionProperty', 					\
			parameterDefinitions: [ 						\
				[$class: 'ChoiceParameterDefinition',				\
					description: 'Which compiler to use',			\
					name: 'Compiler',					\
					choices: 'gcc-9\ngcc-10\ngcc-11\ngcc-12\nclang',	\
					defaultValue: 'gcc-9',					\
				],								\
				[$class: 'ChoiceParameterDefinition',				\
					description: 'The target architecture',			\
					name: 'Architecture',					\
					choices: 'x64\nx86\narm64',				\
					defaultValue: 'x64',					\
				],								\
				[$class: 'BooleanParameterDefinition',  			\
					description: 'If false, the test suite is only ran in debug', 	\
					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,  					\
				],								\
			],
		]])
					// choices: 'gcc-4.9\ngcc-5\ngcc-6\ngcc-7\ngcc-8\ngcc-9\ngcc-10\ngcc-11\nclang',
					// defaultValue: 'gcc-8',

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

	Tools = load "Jenkins/tools.groovy"

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

	currentBuild.description = settings.DescShort
	echo                       settings.DescLong

	return settings
}

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