For a while now it’s been bothering me that I don’t have a proper build pipeline for Unity, so over a bank holiday weekend, I decided to set one up, I’ll go through what I created later in this post but first…
What is a build pipeline?
I suppose different people have different meanings of what constitutes a build pipeline, but for me it’s a system that does the following:
- Creates a build for each commit
- For every platform that you’re shipping on
- With every variation (e.g. Demo/Full game/Languages/etc.)
- Validates the build (buy running the tests, deploying, etc.)
- Notifies you if there’s anything wrong
- Archives the build
- Does all of the above automatically without any developer intervention
Why make one?
I enjoy automating things, it’s one of the reasons I became a programmer, I want to remove as much of the boring repetitive stuff as possible and let people get on with creating cool stuff. That said, there are plenty of tasks where the effort of automating them vastly exceeds any time saved (obligatory xkcd link).
However, I think (and I’m not alone in this) that automating the build process is incredibly valuable, for the following reasons:
Creating a build is complex
Especially when you’re shipping on multiple platforms and multiple variants on each platform (demo/full-version, different locales, etc.) Some engines make this easier than others, but even when it’s just clicking a button in an editor, it’s better to just automate it than run the risk of a human making a mistake.
Creating a build is tedious
It tends to take a while, it involves doing precise steps in a precise order, and it involves doing all these things multiple times (often at the end of a deadline).
Creating a build exposes problems
Maybe someone forgot to check an asset in, or that middleware only works on one persons machine, or maybe the person making the build messed up git and has an old version of something. None of these issues are insurmountable, but they’re much easier to fix at the point the mistake was made, rather than a month down the line when you need this build for the demo, essentially, making a build is painful, and if it hurts, do it more often.
It’s one of the components of Continuous Delivery
Continuous Delivery helps teams iterate quickly, which in turn helps make sure they’re shipping the right thing (or making a fun game). It does this by making sure that there is always a build that works (note, it might not be feature complete, but it will at-least be playable). CD has a few other aspects, and some of them are harder than others, but automating the build is an easy first step that delivers a fair amount of value.
Making and maintaining an automated build pipeline can be tricky, but once you use a good one I doubt you’ll want to go back to making builds by hand.
How to make one?
There are many ways to make an automated build pipeline, I’m going to present what I used below, but please do experiment to see what works for you.
For my build server I use Team City mostly because it’s free (with some limitations) and because I’m familiar with it. It also works on Windows, OSX, and Linux, so should likely cover you whatever your build environment is.
I’m not going to detail how to set-up TeamCity here, there’s plenty of docs for that and it’s not too difficult, but essentially, there is a ‘server’ machine that hosts the web-interface you use to things up and monitor things, and the ‘client’ machines which actually do the work. These two components can be on the same machine, but it is advisable for them not to be your dev machine (this helps catch issues with people not checking things in, etc.)
Step One: Command-line Build
We’ll leave testing aside for now and concentrate on making a build. Thankfully Unity already supports this, the docs here have much redacted example of how to create a script that can build the project, and an example command-line to run that script. You’ll likely want to modify it to suit you better, but here’s the C# code that I started with:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | public static void Build() { string[] scenes = { "Assets/Scenes/Main.unity", }; var buildOptions = BuildOptions.None; var buildDir = ""; var devBuild = false; var args = Environment.GetCommandLineArgs(); for (var i = 0; i < args.Length; ++i) { if (args[i] == "--BuildDir") { buildDir = args[i + 1]; } if (args[i] == "--MakeDevBuild") { devBuild = true; } } if (devBuild) { buildOptions |= (BuildOptions.Development | BuildOptions.AllowDebugging); } const BuildTarget buildTarget = BuildTarget.StandaloneWindows64; var fullBuildPath = Path.Combine(buildDir, Path.Combine(buildTarget.ToString(), "TestProject.exe")); var buildError = BuildPipeline.BuildPlayer(scenes, fullBuildPath, buildTarget, buildOptions); if (!string.IsNullOrEmpty(buildError)) { throw new Exception("Error building " + buildTarget + ": " + buildError); } } |
Obviously this code can be improved in numerous ways, but it’ll do for starting with. What we do first is get a list of scenes we want to include in the build, currently this is hard-coded, it might stay like that (that way it mirrors how you’d make a build manually) or we could change to something that finds all the .unity files we’re interested in automatically.
Next we check the for any additional command-line arguments, we use the double leading dash and upper cammel case to differentiate our added command-line args from the ones Unity provides. We’re looking for 2 command line arguments, one to tell us where to put the build, and one to say if it’s a development build or not.
Then we say which platform we want to build to, again, this is hard-coded but likely wants to be made in to a list at some point.
Finally we call the UnityEditor method that actually does the build, check if that has an errors, and throw an exception if it does.
We stick this code in an ‘Editor’ folder somewhere in our project, I’ve put it in a class called ‘CommandLine’, but you can be more invetive.
That done, we can call this locally to check that it works, you’ll want to run the following command:
1 | "C:\Program Files\Unity\Editor\Unity.exe" -batchmode -quit -logFile C:\BuildLog.txt -nographics -projectPath "C:\Test Project" -executeMethod CommandLine.Build --BuildDir C:\build --MakeDevBuild |
Hopefully, that got you a build in C:\build\StandaloneWindows64 and a lively verbose log file of what it was doing at C:\BuildLog.txt
Step 2: Build on TeamCity
Now that we have our command-line build working, it’s pretty simple to set it up on TeamCity. Create a new project, and a build configuration for the build job (see the TeamCity documentation for details)
In the ‘General Settings’ section of the build-configuration, you want to set up 2 artifacts. In TeamCity, an artifact is anything that a build might produce that you want to store. These artifacts are uploaded to the server and kept (by default, they’re kept forever). For the build job, we want both a zip file containing all the builds, and the BuildLog.txt we made Unity output as artifacts (uploading the build log will mean that if anything goes wrong we’ll be able to get more detailed information). To do that, we specify the following 2 paths
1 2 | %build.dir%/**/*=>%result.zip% %build.log.file.path% |
The first one will find anything under the ‘build.dir’ path, and zip it in to ‘result.zip’. The second one will just upload the file at ‘build.log.file.path’. All of these are parameters that will will fill-in later.
The only change I tend to make to the ‘Version Control Settings’ is to make it check-out on the client instead of the server, this isn’t required, but does save the server some work.
Now we need to fill in the build step, it will look remarkably similar to the one we ran on our local machine, but will use some of the parameters we’ve used in the Artifact Paths. You want to create a new build step with a Command Line runner, for the Commnad Executable I use ‘%env.UNITY_PATH%’ which means that it will look in the environment variables on the agent for a variable UNITY_PATH, and use the value of that.
As for the Command Parameters I use the following:
1 | -batchmode -quit -logFile "%build.log.file.path% -nographics -projectPath "%project.path%" -executeMethod CommandLine.Build --BuildDir "%build.dir%" --MakeDevBuild |
Now on to the triggers, just create a new VCS trigger and make it create run a build on each check-in.
I didn’t add any custom failure conditions yet, if the build makes an error and the exception is thrown it will already fail the build with the default settings.
We don’t need any build features for this job.
Don’t need any dependencies either.
Finally we’re on to the Parameters section, this will be pre-populated with the ones we’ve already used, but highlighted red, because we need to specify the values, here’s what I used:
Name | Value |
---|---|
build.dir | %env.TEMP%\build |
build.log.file.path | %env.TEMP%\BuildLog.txt |
project.path | %teamcity.build.checkoutDir% |
result.zip | Build_%build.number%.zip |
I make sure the artifacts (the actual build, and the log file) go in the temp directory, since that’s cleaned on the agent each build (and thus stops stale artifacts being delivered). The project.path is set to be the directory that TeamCity checks out to, this assumes that the unity project is at the root of your git (or other) repo. Finally I use the build.number parameter for the result.zip, this means that each zip file containing a build will automatically be numbered and the numbers increment for each build. Such a scheme is very helpful when talking about builds with others as not only does each build have a unique name, but the name of the build gives you a rough idea of how old it is.
All that’s left to do now is run it, and congratulations, you’ve set up the first part of your automated build pipeline. Your days of manually making builds are officially over.
In the next post I’ll look at validating the build by automatically running tests.