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" )
}
}
}
}
)
Add a Comment