DEV Community

Cover image for Constantly Delivering my Unity Project builds with Jenkins
Pedro Pereira Lourenço
Pedro Pereira Lourenço

Posted on

Constantly Delivering my Unity Project builds with Jenkins

Hi!

In this document, I will explain how I managed to create my own continuous integration and delivery pipeline using Jenkins for my projects in Unity. All the code mentioned here will be available on my GitHub account for free use. There may be various ways to implement what I am presenting here, so feel free to share any thoughts on how I can improve in the future. I would love to hear your feedback.

One of the many factors that influenced me to learn more about how I can build this Continuous Integration and Continuous Delivery (CICD) process was my old spare notebook that I have at home. To prevent it from collecting dust in my closet, I decided to turn it into a building machine. This way, I could easily connect to it using any of those remote desktop apps and build my applications. Within a matter of minutes, I would have it ready to be tested on any shared devices.

At this moment, the Pipeline is running in four phases: checkout, setup, build, and publish. I will document each one of these phases and explain how it was implemented.

Using Jenkins

For the Pipelines, I decided to use Jenkins, an open-source automation server. It assists in automating various aspects of software development, including builds, testing, and deployment, thereby simplifying the process of continuous integration and delivery.

For the sake of simplicity, I have opted to run Jenkins locally on my machine without utilizing any Jenkins image within a separate Docker container. Since it was my first time doing this, I went through various tests and process definitions, involving constant "add" and "remove" actions on files and codes. Additionally, given that my knowledge of Docker is not that advanced, using it would have caused a delay in achieving what I was attempting to do.

During my research, videos from DevOps Journey and CloudBeesTV helped me a lot to understand a little bit more about Jenkins. It didn't take me too long to get comfortable using this awesome tool.

I have created a repository on GitLab containing all the code needed to run the pipeline. Once Jenkins starts the build, it downloads this repository and runs the main.groovy script. This file is the beginning of the pipeline, running all the steps, from build to uploading to Google Drive. As I said before, this pipeline is split into 4 phases: checkout, setup, build, and publish. These phases run essential commands for generating the build. The diagram of these phases is represented below for a summary.

Image description

node()
{
    stage('checkout')
    {
        // ...
    }
    stage('Setup')
    {
        // ...
    }
    stage('Build')
    {
        // ...
    }
    stage('Publish')
    {
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

A closer look at the phases

Checkout

First phase, it is required to update the Pipeline to the most recent version. It must stay updated before any builds in case it has changed.

Setup

The Unity project has its repository. When starting the pipeline, Jenkins updates this repository to the most recent version and then starts the build.

In this phase, I encountered an issue that started occurring after git had its vulnerability correction, beginning with version Git v2.35.2. In short, when git attempts to run a command inside another repository, it begins respecting any configurations in that git directory. This could lead to a security problem, especially in cases where sensitive configurations are defined.

As my pipeline already runs inside its repository, when attempting to execute git checkout <branch> from within Unity's repository, it triggers the fatal error unsafe repository. The error itself suggests a solution that would likely resolve this issue, which is to add Unity's repository to a list of trusted folders. This should allow me to run the command again. However, the fatal error persists.

After numerous attempts, I successfully resolved it by creating a simple C# console directly within Unity's repository. This program then executes the git command, specifying the target branch to checkout. With this workaround, I could update the project, leaving it ready for building in the next phase.

Here is an example of how this program runs the Git command, wrapping every Git functionality that might be necessary for this phase:

public class Program
{
    public static int Main(string[] args)
    {
        try
        {
            string branchName = args[0];

            Git git = new Git(branchName);
            git.FetchAll();
            git.Checkout();

            return 0;
        }
        catch(IndexOutOfRangeException e)
        {
            Console.WriteLine("**ERROR:** There are no arguments provided specifying the desired branch to checkout!");
            return 1;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In the end, the Jenkins commands for this phase ended up like this, as shown below:

    stage('Setup')
    {
        def cmd = Get('cmd');
        echo "Updating unity project by using git solution"
        dir("${env.UNITY_PROJECT_PATH}/git_solution/")
        {
            cmd.Do("dotnet run ${env.GIT_BRANCH_TO_CHECKOUT}");
        }
    }
Enter fullscreen mode Exit fullscreen mode

Build

As the name already suggests, this phase generates the Unity project's build. Unity offers us a way to interact with its editor by providing arguments from the command line. This simplifies the process when working to achieve continuous integration for your game. All the information about how you can interact with the Unity Editor using only command lines is available in Unity's official documentation.

Building a Unity project via the command line looks like this:

"C:\Program Files\Unity\Hub\Editor\<version>\Editor\Unity.exe" -quit -batchmode -projectPath "C:\path\for\Unity\Project" -executeMethod <Namespace.Class.MethodName>
Enter fullscreen mode Exit fullscreen mode

Taking a look at the shell command above, the argument -batchmode implies that Unity will run in batch mode, and every interaction will be through command lines, ignoring any human action in the Editor. This also suppresses any pop-ups that might appear during the build. Following the next command, I informed the projectPath to open. In the end, when the project finishes loading, it executes the method passed in the executeMethod argument.

Before Jenkins starts the build, I would like to have control over more aspects of how the build will be generated. It would be great to increase the build version, specify whether it will be an .apk or .aab, indicate if it needs to split application binaries and configure other settings as my project grows. To encapsulate all these settings before the build, I have created Unity Builder, a package that streamlines the Unity build generation process by executing all necessary steps beforehand.

Unity Builder has the method UnityBuilder.BuildCmd.Build that is passed in the -executeMethod argument in the shell command above. When running this method, Unity loads the ScriptableObject BuildCmd, which holds every configuration defined by the user on how it needs to be generated. It also increases the version as programmed by the user.

Every build from Unity Builder is saved in the path specified in the environment variable UNITY_BUILDER_ROOT. It is necessary to have this environment variable defined; otherwise, the build process will end with the result as INVALID_ENVIRONMENTS. UNITY_BUILDER_ROOT also has the .json file versionSettings:

versionSettings
{
    "isRelease": false,
    "path": "Path from the last generated build"
}
Enter fullscreen mode Exit fullscreen mode

The isRelease parameter informs whether UnityBuilder needs to increase the main version number or not (v.MainVersionNumber.BuildNumber, v.1.1). The path parameter indicates where the last build by Unity Builder was stored. This field will later be used by Jenkins when publishing to Google Drive.

At the end, the Jenkins commands for this phase ended up like this, as shown below:

stage('Build')
{
    echo "Building Unity project..."
    def cmd = Get('cmd');
    def unity = Get('unity');
    cmd.Do(unity.Build())
}
Enter fullscreen mode Exit fullscreen mode

Publish

In the final phase of the pipeline, at this moment, Jenkins sends the new fresh build to Google Drive. While I plan to use this phase in the future for uploading builds to stores, currently, for internal tests and since my project is in its early stages, I have opted to use only Google Drive. This way, it will be easier to share my builds with other people for testing purposes.

In this phase, I have created two more C# programs that make the process easy when uploading to Google Drive: the drive_uploader and upload_build_to_drive. The drive_uploader is a service that implements the Google Drive API. It works by simply running the following command: dotnet run -file <path> -mime <type>, where <path> is the path of the file that will be sent to Drive, and <type> is the type of file that will be sent. This service is also available on my GitHub for free use.

Now, the upload_build_to_drive uses this service by loading the versionSettings.json mentioned above and passing the path of the recently generated build in the <path> argument. As for the -mime parameter, there are two values that can be passed:

  • For .aab files, <type> will be application/x-authorware-bin
  • For .apk files, <type> will be application/vnd.android.package-archive

At the end, the Jenkins commands for this phase ended up like this, as shown below:

stage('Publish')
{
    def cmd = Get('cmd');
    echo "Publishing app to drive"
    dir(env.DRIVE_UPLOADER_PATH)
    {
        cmd.Do("dotnet run");
    }
 }
Enter fullscreen mode Exit fullscreen mode

In conclusion

Several improvements can be applied during the presented process. But as a first attempt, I am happy with the result I managed to achieve. Now, for any change I make in my project that requires mobile testing, I can obtain it with just one click. This document was not written to teach how to create your own Continuous Integration and Continuous Delivery Pipeline, but rather to document the processes I performed to achieve this result. Feel free to provide any constructive feedback for improvements in what has been presented.

Thanks!

You can also find me in:
LinkedIn
GitHub
Youtube
Lecture about OOP Applied in Games

Top comments (0)