Have your CAKE and eat it–implementing a C# build script at Huddle

Liam Westley
huddle-engineering
Published in
6 min readAug 1, 2018

tldr; a build script should achieve several things; it can build and test your code locally, it can run (virtually unchanged) on a build server and it should be easy to understand for the developer of the application.

The Huddle Desktop application is written in C# for Windows and Mac. With CAKE we can finally have a single cross-platform build script, written in the language used to develop the actual application.

https://www.huddle.com/

What Is CAKE?

Cake (C# Make) is a cross-platform build system using a C# DSL — built on top of the Roslyn compiler and available on Windows, Linux and macOS (https://cakebuild.net/). It is completely open source and hosted on GitHub.

You can get started by cloning an example repo, which is described over here; https://cakebuild.net/docs/tutorials/getting-started.

There are three key files — build.ps1 (PowerShell bootstrapper for Windows), build.sh (bash shell bootstrapper for Linux and macOS) and build.cake — which is your actual build script which the bootstrappers run. The bootstrappers download all the various tools and packages required to run cake.

What did Huddle use before CAKE?

Huddle has used various build mechanisms, dependent on project requirements, which have include rake (Ruby), psake (PowerShell) as well as plain old batch/bash scripts.

For Huddle Desktop, there were batch and bash scripts, maintained separately for the two platforms. Eventually these were deprecated for individually configured steps within our build server, TeamCity.

These steps varied considerably for the Windows and macOS versions, were not capable of being run locally on developer machines, tied us to a specific build server and due to be configured in the build server, isolated the build steps from our git repository.

The goal was to bring both platform builds into a single script, albeit with different platform specific tasks, capable of being run locally on development machines. We also wanted to gain the ability to run individual steps within the build, either locally or easily configured as separate builds on the build server.

Anatomy of a cake script

1 — Arguments

At the top of the script you can parse arguments passed into the build script. Suggested default arguments of target (the first task to be run) and configuration (Release/Debug) are provided in the example repository. You can rename these but it’s handy to leave them as is. Arguments are referenced by name and can have a default value. The target argument refers to the first task to be run in the script.

We also have a platform argument in our script,

var platform = Argument(“platform”, “windows”).Trim().ToLower();

2 — Setup and Teardown

Cake supports the concept of setup and teardown which are run independently of which tasks are being executed (as long as a call to RunTarget is executed somewhere in the script). Setup and Teardown should be defined in the script above RunTarget.

At Huddle we use Setup to identify whether we are running locally or under a TeamCity agent (see Build server integration below).

3 — Tasks

Tasks are the actual actions performed by the build script, and we try to isolate them into atomic, single purpose tasks, such as cleaning the build directory,

Task(“Clean”)
.Does(() =>
{
CleanDirectory(buildDir);
});

The default target task, AllStepsWindows, chains the atomic tasks together to provide a full build.

Task(“AllStepsWindows”)
.IsDependentOn(“KillHuddle”)
.IsDependentOn(“UpdateInstallerProject”)
.IsDependentOn(“MsBuildWindows”)
.IsDependentOn(“UnitTestsWindows”)
.IsDependentOn(“UnitTestsOffice”)
.IsDependentOn(“UnitTestsIntegration”)
.IsDependentOn(“ImportUnitTestResults”)
.IsDependentOn(“VerifyWindowsCodeSigning”)
.Does(() =>
{
});

The chaining of tasks means we can create a task that can build and run only unit tests (for quick compile/test runs), as well as a longer running task that builds and runs all tests including acceptance tests.

4 — RunTarget

At the very end of the script is the RunTarget method that, in our build script, runs the target argument we received on the command line, which we default to AllStepsWindows as our default build.

RunTarget(target);

Build server integration

CAKE provides support for build servers such as TeamCity. We can detect that we are running the build on a TeamCity agent, and then collect relevant information for customising the build.

if (BuildSystem.TeamCity.IsRunningOnTeamCity)
{
Information(
@”Environment:
PullRequest: {0}
Build Configuration Name: {1}
TeamCity Project Name: {2}
Build Number {3}
“,
BuildSystem.TeamCity.Environment.PullRequest.IsPullRequest,
BuildSystem.TeamCity.Environment.Build.BuildConfName,
BuildSystem.TeamCity.Environment.Project.Name,
BuildSystem.TeamCity.Environment.Build.Number
);
isTeamCityBuild = BuildSystem.TeamCity.IsRunningOnTeamCity;
Buildnumber = BuildSystem.TeamCity.Environment.Build.Number;
}
else
{
Information(“Not running on TeamCity”);
}

Build server integration includes being able to report issues and fail builds, such as code signing failures,

if (isTeamCityBuild) TeamCity.BuildProblem(“Code signing failed on MSI or EXE”, “Code signing failed on MSI or EXE”);

How did we create our build script?

Within TeamCity the Windows build consisted of eight distinct steps, in addition to the build server handling checking out the source code and collecting the output artifacts.

The macOS build consisted of two steps, with a large amount of logic placed in a standard make file.

Windows first

We decided to tackle the Windows build first, given that it was completely external to the git repo. We performed a lift and shift of every step into the build.cake file as skeleton tasks. The tasks were then fleshed out individually and debugged locally before attempting any builds on the build server itself. Not only was there no need to keep testing the script on the actual build server, steps could be debugged individually, greatly speeding up the process.

Visual Studio Code provides an ideal editing environment with a cake extension providing syntax highlighting, access to intellisense, commands for cake-izing an existing project and debugging a cake script.

What were the hard bits?

With the old TeamCity builds we had various configuration parameters and environment variables used to customise different builds. Luckily TeamCity can pass these easily to the PowerShell script as arguments.

Normal configuration parameters can be referenced directly,

-configuration=”%MSBuildConfiguration%”

whereas environment variables require the env. prefix,

-environment=”%env.Environment%”

Advanced installer

We use Advanced Installer to package our Windows application as a setup.exe as well as more enterprise friendly MSI package. We have to configure Advanced Installer for versioning (read from TeamCity) as well as configuration values for the various environments we target.

Fortunately Advanced Installer provides command line tools that meant we could use the inbuilt StartProcess method to run the command line tool with appropriate parameters

Old version of NUnit — manually gathering results

Cake provides support for unit testing frameworks which includes support for the older NUnit 2.6.4 (as well as the more modern NUnit v3). Due to a requirement to use the older version of NUnit we had to include a step to import the XML output of the tests when running under TeamCity using the integration libraries, specifically TeamCity.ImportData.

When running locally the NUnit tests display the results to the console, so the import is not required.

Failing the build if incorrectly code signed

The final step was to verify that all the relevant setup and application files had been signed correctly with a code signing certificate. By using StartProcess we could execute the signtool.exe directly, collecting the exit codes and failing the build if any part of the application was not signed correctly by calling TeamCity.BuildProblem.

What have we left to do?

We still have to complete the macOS sections of the CAKE script, although the scaffolding was included as the CAKE script was debugged for Windows.

It helped that we had already moved some projects to use CAKE as a build script, so I could lean on Toby Henderson for a skeleton script, and guidance on how to achieved what I wanted.

Generally, using CAKE has been an enjoyable experience so far, and has resulted in a much cleaner build process, maintained outside of any specific build server, but retaining the feedback and build server integration to which we have become accustomed. Roll on the macOS build!

Originally published at blog.liamwestley.co.uk on August 1, 2018.

--

--

Liam Westley
huddle-engineering

.NET wrangler, Application Architect at Huddle, ex .NET MVP, speaker at user groups and conferences. Keen on cheese.