Jenkinsfile

I wanted to explain how to write Jenkinsfiles in my last post but I blurted on too much. So here we go.

Pipeline vs Scripted

When you read the official docs about writing Jenkinsfiles you get confused because there’s two versions. The newer version starts with “pipeline”. This is the one they want you to use, and it’s also the one that is quite hard to find any information on so I’m not going to be using it here.

The tldr of it is that the scripted way isn’t going anywhere, it’s the “advanced” way, and the “pipeline” mode is built on top of it.

Basic

Your basic jenkinsfile looks like this.

node
{
  stage( 'Stage Name' )
  {
    // do stuff here
  }
}

So generally you do your work inside of stages.

Working Example 1 (UE4)

Non working examples are bullshit, so here’s a look at some of our real world examples.

node( 'ue4 && vs2017' )
{
	stage ( 'Checkout' ) 
	{
		checkout scm
	}
		
	stage ( 'Build' )
	{
		bat 'Build.bat'
	}
	
	stage ( 'Upload' )
	{
		def vdf = 'Steamworks/app.' + env.BRANCH_NAME + '.vdf'
		if ( fileExists( vdf ) )
		{
			SteamUpload( "$WORKSPACE/" + vdf )
		}
	}
}

So in this you notice the node has arguments. This is telling Jenkins to build this on a node (a build server) which has the ue4 and vs2017 tags. When we set our build servers up we tag them with what’s on them, so it doesn’t try to build this project on a linux server with nothing on it.

The checkout stage calls “checkout scm”, which downloads everything from git into the workspace folder on the target build server.

The next stage runs a bat file. In this UE4 project we build using a bat file, so that developers can test a standalone build easily by just running the bat file.

The upload section uploads it to Steam. SteamUpload is a function we defined in our own library (I’ll do another post about defining your own libraries if anyone gives a shit). This stage has a bit more logic. The Steamworks vdf file “app.master.vdf” uploads to the master branch on Steam. Using this logic we could make the git branch “experimental” upload to the “experimental” branch by adding a app.experimental.vdf.

Working Example 2 (Facepunch.Steamworks)

node ( 'vs2017' )
{
	stage 'Checkout'
		checkout scm

	stage 'Restore'
		bat 'nuget restore Facepunch.Steamworks.sln'
		
	stage 'Build Release'
		bat "\"${tool 'MSBuild'}\" Facepunch.Steamworks/Facepunch.Steamworks.csproj /p:Configuration=Release /p:ProductVersion=1.0.0.${env.BUILD_NUMBER}"
		
	stage 'Build Debug'
		bat "\"${tool 'MSBuild'}\" Facepunch.Steamworks/Facepunch.Steamworks.csproj /p:Configuration=Debug /p:ProductVersion=1.0.0.${env.BUILD_NUMBER}"
		
	stage 'Build Release NetCore'
		bat "dotnet restore Facepunch.Steamworks/Facepunch.Steamworks.NetCore.csproj"
		
	stage 'Build Release NetCore'
		bat "dotnet build Facepunch.Steamworks/Facepunch.Steamworks.NetCore.csproj --configuration Release"
		
	stage 'Build Debug NetCore'
		bat "dotnet build Facepunch.Steamworks/Facepunch.Steamworks.NetCore.csproj --configuration Debug"

	stage 'Archive'
		archiveArtifacts artifacts: 'Facepunch.Steamworks/bin/**/*'
}

So kind of the same routine. You can see we checkout, we call nuget restore to restore packages.

In the build section you see we do ${tool ‘MSBuild’}, this returns a string location of the MSBuild tool (which is set on the build server). You can also see we set the product version to the build number.

Strings in groovy can take some getting used to, here’s a cheatsheet.

def name = 'Mike'
def GetName() { return 'Dave' }

def str = "Hello"
def str = 'Hello'
def str = "Hello " + "Mike"
def str = "Hello " + 'Mike'
def str = "Hello " + name
def str = "Hello $name" // Hello Mike
def str = 'Hello $name' // Hello $name
def str = "Hello ${name}" // Hello Mike
def str = "Hello ${GetName()}" // Hello Dave
def str = "Hello $GetName()" // Error
def str = "Hello ${ 10 * 5 }" // Hello 50

So that’s all standard, then you see at the bottom, we do archiveArtifacts. This collects all the files defined in the path and stores them.

Working Example 3 Unity3d

node( 'unity' )
{   
	def unity = new facepunch.Unity()
	unity.Setup( '5.6.0f3' )
	
	stage( 'Checkout' )
	{
		checkout scm
	}
	
	stage( 'BuildInfo.json' )
	{
		dir ( "Assets/Resources" )
		{
			writeFile file:'BuildInfo.json', text: GetBuildJson()
		}
	}
	
	stage( 'Facepunch.Build.Win32' )
	{
		unity.Batch( "$WORKSPACE", "Facepunch.Build.Win32" )
	}
		
	stage( 'Facepunch.Build.Win64' )
	{
		unity.Batch( "$WORKSPACE", "Facepunch.Build.Win64" )
	}
		
	stage( 'Facepunch.Build.Osx' )
	{
		unity.Batch( "$WORKSPACE", "Facepunch.Build.Osx" )
	}
		
	stage( 'Facepunch.Build.Linux' )
	{
		unity.Batch( "$WORKSPACE", "Facepunch.Build.Linux" )
	}
	
	stage( 'Steam Upload' )
	{
		dir( 'dev/steamworks' )
		{
			if ( env.BRANCH_NAME == "master" )
			{
				SteamUpload( "$WORKSPACE/dev/steamworks/steamworks.app.vdf" )
			}
		}
	}
}

So, node should have Unity, then we create a Unity class. This is a class I made which basically holds onto the Unity version and launches Unity for us. Again – if anyone wants a tutorial for libraries, hit up the comments and I’ll make one.

Next we create build info.. again this is a library I made that spews a json file about the build. This means we can show scm and build into in the game when it’s on Steam.

Then we build the different unity platforms. Unity can only build one at a time, so we build the one by one. This uses the Facepunch.UnityBatch.exe – which wraps Unity.exe (because unity.exe doesn’t print to stdout, we read the log file and manually print it out so it all shows in the jenkins logs).

Then in the Steam Upload section we change the directory (the dir command changes the directory for everything in the block), then we upload it to Steam.

Rust

I’ll let you figure out this one for yourself

parallel(

	"client" : 
	{
		node( "unity && steam && heavy" )
		{
			echo "Starting Client Build"
			
			def unity = new facepunch.Unity()
			unity.Setup( '5.6.0f3' )
			
			def plastic = new facepunch.Plastic()
			
			def PlasticWorkspace = 'RustMain'
			def SteamBranch = 'staging'
			def ProjectFolder = "$WORKSPACE/${PlasticWorkspace}"
			
			stage ( 'Checkout' ) { plastic.Checkout( "rust_reboot", PlasticWorkspace )}
			
			stage ( 'Revert' ) { plastic.RevertChanges() }
			
			stage( 'BuildInfo.json' )
			{
		        dir ( PlasticWorkspace + "/Assets/Resources" )
				{
					writeFile file:'BuildInfo.json', text: GetBuildJson()
				}
			}
				
			stage ( 'GameManifest' ){ unity.BatchTo( "${ProjectFolder}", "GameManifest.DoGenerate", "build/client/${SteamBranch}/StandaloneWindows64" ) }
				
			stage ( 'CL_Win64' ){ unity.BatchTo( "${ProjectFolder}", "Build.Client_Win64", "build/client/${SteamBranch}/StandaloneWindows64" ) }
			stage ( 'CL_Win64d' ){ unity.BatchTo( "${ProjectFolder}", "Build.Client_Win64", "build/client/${SteamBranch}/StandaloneWindows64Debug", "-compileDebugMode" ) }
				
			stage ( 'CL_Win32' ){ unity.BatchTo( "${ProjectFolder}", "Build.Client_Win32", "build/client/${SteamBranch}/StandaloneWindows" ) }
			stage ( 'CL_Win32d' ){ unity.BatchTo( "${ProjectFolder}", "Build.Client_Win32", "build/client/${SteamBranch}/StandaloneWindowsDebug", "-compileDebugMode" ) }
			
			stage ( 'CL_OSX' ){ unity.BatchTo( "${ProjectFolder}", "Build.Client_OSX64", "build/client/${SteamBranch}/StandaloneOSXIntel64" )}
			stage ( 'CL_OSXd' ){ unity.BatchTo( "${ProjectFolder}", "Build.Client_OSX64", "build/client/${SteamBranch}/StandaloneOSXIntel64Debug", "-compileDebugMode" ) }
			
			stage ( 'CL_Linux' ){ unity.BatchTo( "${ProjectFolder}", "Build.Client_Linux64", "build/client/${SteamBranch}/StandaloneLinux64" ) }
			stage ( 'CL_Linuxd' ){ unity.BatchTo( "${ProjectFolder}", "Build.Client_Linux64", "build/client/${SteamBranch}/StandaloneLinux64Debug", "-compileDebugMode"  ) }
			
			stage ( 'CL_Bundles' ) { unity.BatchTo( "${ProjectFolder}", "BuildAssetBundles.BuildForClient", "build/client/${SteamBranch}/Common/Bundles" ) }
			
			stage ( 'Signing' )
			{
				SignTool( PlasticWorkspace + '/build/client/**/*.dll' )
				SignTool( PlasticWorkspace + '/build/client/**/*.exe' )
			}
			
			stage ( 'Eac Hashing' )
			{			
				parallel(
					"Win64": { DoEacHashing( ProjectFolder + "/Prerequisites/EasyAntiCheat.HashTool", ProjectFolder + "/build/client/${SteamBranch}/StandaloneWindows64" ) },
					"Win64d": { DoEacHashing( ProjectFolder + "/Prerequisites/EasyAntiCheat.HashTool", ProjectFolder + "/build/client/${SteamBranch}/StandaloneWindows64Debug" ) },
					
					"Win32": { DoEacHashing( ProjectFolder + "/Prerequisites/EasyAntiCheat.HashTool", ProjectFolder + "/build/client/${SteamBranch}/StandaloneWindows" ) },
					"Win32d": { DoEacHashing( ProjectFolder + "/Prerequisites/EasyAntiCheat.HashTool", ProjectFolder + "/build/client/${SteamBranch}/StandaloneWindowsDebug" ) },
					
					"Linux": { DoEacHashing( ProjectFolder + "/Prerequisites/EasyAntiCheat.HashTool", ProjectFolder + "/build/client/${SteamBranch}/StandaloneLinux64" ) },
					"Linuxd": { DoEacHashing( ProjectFolder + "/Prerequisites/EasyAntiCheat.HashTool", ProjectFolder + "/build/client/${SteamBranch}/StandaloneLinux64Debug" ) },
					
					"Osx": { DoEacHashing( ProjectFolder + "/Prerequisites/EasyAntiCheat.HashTool", ProjectFolder + "/build/client/${SteamBranch}/StandaloneOSXIntel64" ) },
					"Osxd": { DoEacHashing( ProjectFolder + "/Prerequisites/EasyAntiCheat.HashTool", ProjectFolder + "/build/client/${SteamBranch}/StandaloneOSXIntel64Debug" ) },
				)
			}
			
			dir( PlasticWorkspace )
			{
				stage( 'Upload Client' )
				{
					SteamUpload( ProjectFolder + "/Steamworks/rust_cl_staging.vdf" )
				}
				
				stage( 'Upload Client Debug' )
				{
					SteamUpload( ProjectFolder + "/Steamworks/rust_cl_staging-debug.vdf" )
				}
			}
		}
	},
  
	"server" :
	{
		node( "unity && steam && heavy" )
		{
			echo "Starting Server Build"
					
			def unity = new facepunch.Unity()
			unity.Setup( '5.6.0f3' )
			
			def plastic = new facepunch.Plastic()
			
			def PlasticWorkspace = 'RustServer'
			def SteamBranch = 'staging'
			def ProjectFolder = "$WORKSPACE/${PlasticWorkspace}"
			
			stage ('SV Checkout') { plastic.Checkout( "rust_reboot", PlasticWorkspace ) }

			stage ('Revert') { plastic.RevertChanges() }
			
			stage( 'BuildInfo.json' )
			{
		        dir ( PlasticWorkspace + "/Assets/Resources" )
				{
					writeFile file:'BuildInfo.json', text: GetBuildJson()
				}
			}
				
			stage ( 'GameManifest' ) { unity.BatchTo( "${ProjectFolder}", "GameManifest.DoGenerate", "build/server/${SteamBranch}/StandaloneWindows64" ) }
			stage ( 'SV_Bundles' ) { unity.BatchTo( "${ProjectFolder}", "BuildAssetBundles.BuildForServer", "build/server/${SteamBranch}/Common/Bundles" ) }
			
			stage ( 'SV_Win64' ){ unity.BatchTo( "${ProjectFolder}", "Build.Server_Win64", "build/server/${SteamBranch}/StandaloneWindows64" ) }
			stage ( 'SV_Win64d' ){ unity.BatchTo( "${ProjectFolder}", "Build.Server_Win64", "build/server/${SteamBranch}/StandaloneWindows64Debug", "-compileDebugMode"  ) }
			stage ( 'SV_Osx' ){ unity.BatchTo( "${ProjectFolder}", "Build.Server_OSX64", "build/server/${SteamBranch}/StandaloneOSXIntel64" ) }
			stage ( 'SV_Osxd' ){ unity.BatchTo( "${ProjectFolder}", "Build.Server_OSX64", "build/server/${SteamBranch}/StandaloneOSXIntel64Debug", "-compileDebugMode"  ) }
			stage ( 'SV_Linux' ){ unity.BatchTo( "${ProjectFolder}", "Build.Server_Linux64", "build/server/${SteamBranch}/StandaloneLinux64" ) }
			stage ( 'SV_Linuxd' ){ unity.BatchTo( "${ProjectFolder}", "Build.Server_Linux64", "build/server/${SteamBranch}/StandaloneLinux64Debug", "-compileDebugMode"  ) }

			dir( PlasticWorkspace )
			{
				stage( 'Upload Server' )
				{
					SteamUpload( ProjectFolder + "/Steamworks/rust_ds_staging.vdf" )
				}
				
				stage( 'Upload Server Debug' )
				{
					SteamUpload( ProjectFolder + "/Steamworks/rust_ds_staging-debug.vdf" )
				}
			}
		}
	}
)

6 thoughts on “Jenkinsfile

  1. First, thanks for giving us a bit of an insight on what’s going on behind the scenes :)

    I’d be curious to know how you setup the “facepunch.Unity()” class you are using within your Jenkinsfile. Is it a custom Jenkins plugin you created that exposes some methods, or is it part of the Jenkinsfile?

  2. I must say that Jenkins pipeline/workflow system is quite nice, makes it possible for you to handle both projects with long-running steps as well as ones that should run quickly instead.

    It’s nice of them to let you use a full programming language as well, and by letting you run parts of your scripts outside of the sandbox. Taken heavy usage of both those features to do pipeline templates at work, though with a different syntax to the official pipeline DSL. (The one that starts with “pipeline {“)

    I feel like I should ask though, have you had any thoughts on the ‘parallel’ feature? Or is there a lack of build server hardware to make that useful in your case?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s