Cake (C# Make) provides a wonderfully straightforward and extensible means of automating your software builds.
In my previous article, I provided some background on what Cake is and how to get started with it. I walked through how to set up Cake on your system and how to create and run your first build script. If you haven’t gotten Cake set up yet I recommend that you check out my previous article first.
In this article, I am continuing my coverage of Cake with practical examples of several useful tasks you can get Cake to perform for you as part of an automated build process.
Aliases
To effectively use Cake, it’s important to know what ‘aliases’ are and be familiar with the most common aliases.
In simple terms, aliases are convenience methods that are built into Cake and help to make build-related tasks easier to accomplish with a simplified API surface.
I have listed a few of the most frequently used aliases below for reference.
It should be possible to guess from the names of the above aliases what they can be used for. Aliases form part of the Cake DSL (Domain-Specific Language), helping to make build scripts simpler and more readable.
Having said that, it’s important to bear in mind that aliases are C# methods at the end of the day and there is no magic involved.
Cleaning up files
Below is a quick example of a ‘Clean-Files’ task that uses some Cake aliases to delete a file and empty a directory.
Note that I will cover tasks in more detail shortly.
Task("Clean-Files")     .Does(() => { var lockFile = "./lockfile.txt";     if (FileExists(lockFile)) { DeleteFile(lockFile); }     CleanDirectory("./temp"); });
In the above example, if the file named ‘lockfile.txt’ exists in the same directory as the build script, the file will be deleted. The CleanDirectory
alias will then remove all files and subdirectories from the directory named ‘temp’. If this temp directory does not already exist, it will be created automatically.
Zipping directories
Cake provides a very convenient means of zipping up directories using the Zip
alias.
Task("Zip-Directory") Â Â Â Â .Does(() => { Zip("./tools", "./tools.zip"); });
The simplest Zip
alias method overload specifies two parameters; the path to the directory to zip and the path to the resulting zip file. In the above example, the contents of a ‘tools’ directory located at the same level as the build script are copied into a ‘tools.zip’ file.
To complement the Zip
alias there is also the Unzip
alias.
Now that we’ve seen some examples of Cake aliases in action, let’s move on to the implementation of some practical build tasks.
Tasks
Tasks in Cake are where the real build action happens.
The general idea when creating a build script is that you define several individual tasks and then chain these together to define the overall script logic. Loosely speaking, these tasks should be broken down into the individual steps that you would undertake if you had to carry out your build process manually.
Now might be a good time to think about what the manual steps would be for building and deploying your software and work out the order in which these steps would need to be performed. You can then start using some of the examples shown further below as inspiration and build on them further.
Note that this article assumes you are developing a build script for a .NET Core or .NET 5.0+ application. If you are building a .NET Framework application you may need to use alternative aliases such as NuGetRestore
instead of DotNetRestore
.
Build information
Let’s warm up with a basic task to kick things off.
It can be useful when your build script starts running to see some information that verifies that the correct Cake version is being used and that argument values have been passed into the script correctly.
The example script below contains the declaration and assignment of the arguments and environment variables that are used by the script to provide some context. You can assume that the remaining task examples will make use of these values too.
///////////////////////////////////////////////////////////////////////////////
//Â ARGUMENTS
///////////////////////////////////////////////////////////////////////////////
var target = Argument("target", "Default"); var configuration = Argument("configuration", "Release"); ///////////////////////////////////////////////////////////////////////////////
// ENVIRONMENT VARIABLES
/////////////////////////////////////////////////////////////////////////////// var solutionFileName = EnvironmentVariable("SolutionFileName") ?? "MyApp.sln";
///////////////////////////////////////////////////////////////////////////////
// TASKS
///////////////////////////////////////////////////////////////////////////////
Task("Build-Information") .Does(()Â =>Â
{ DisplayCakeVersion(); NewLine(); Information($"Target:Â {target}"); Information($"Configuration:Â {configuration}"); Information($"SolutionFileName:Â {solutionFileName}"); });
Task("Default")
.IsDependentOn("Build-Information")
.Does(() =>
{
});
RunTarget(target);
///////////////////////////////////////////////////////////////////////////////
// METHODS
///////////////////////////////////////////////////////////////////////////////
void DisplayCakeVersion() { string cakeVersion = typeof(ICakeContext).Assembly.GetName().Version.ToString(); Information($"Building using version {cakeVersion} of Cake"); } void NewLine() { Information(""); }
In the above example, I’d like you to focus on the ‘Build-Information’ task.
The first thing the task does is display information about the current Cake version using a method called DisplayCakeVersion
. This method has been defined at the bottom of the script and acts as a convenience method. I’ll show you a way to separate your own convenience methods into a separate file later in the article.
You should be able to tell from the DisplayCakeVersion
method implementation that any valid C# code will work in a Cake script. It doesn’t pose a problem at all if you need to reach into the .NET BCL to perform more advanced logic. In this case, the methods and properties of the .NET Assembly
class are used to extract the version number of the main Cake assembly. The Information
alias is then used to log this information to the console.
NewLine
is another convenience method that simply calls the Information
alias to write a new blank line to the console to help separate the output for us.
The rest of the ‘Build-Information’ task uses the Information
alias to output the values of the two arguments and the single environment variable that the script uses. Logging the values of script inputs like this can be extremely valuable, as it lets you verify that the values are what you were expecting them to be.
Below is an example of how to call a Cake build script from a command (.cmd) file, setting an environment variable value and passing along an argument value to the script.
SET SolutionFileName=MyOtherApp.sln powershell -File "./build.ps1" -Arguments "--target Build-Information" pause
Alternatively, you could bypass the Cake bootstrapper PowerShell file and call the .NET Cake Tool directly, as follows.
$env:SolutionFileName = 'MyOtherApp.sln'
dotnet cake --target Build-Information
Along with other information, your terminal should display the output from the ‘Build-Information’ task, which should be similar to the following.
========================================
Build-Information
========================================
Building using version 2.2.0.0 of Cake
Target: Default
Configuration: Release
SolutionFileName: MyOtherApp.sln
Now that you know the basics, you can start adding to the ‘Build-Information’ task and include the information that you feel is relevant to your situation.
Let’s move on to our next task.
Clean
It’s common practice to clear out the application bin directory before proceeding to the main build tasks. This provides a clean baseline to start from before building the solution. This is a similar concept to the ‘Rebuild Solution’ feature within Visual Studio, whereby the bin directory is emptied before the solution is built.
The below example demonstrates how to clean the project bin directory.
Task("Clean") .IsDependentOn("Build-Information") .WithCriteria(c => HasArgument("rebuild")) .Does(() => { CleanDirectory($"./MyApp/bin/{configuration}"); Information("Clean completed"); });
In the above code, the ‘Clean’ task has been chained to the ‘Build-Information’ task using the IsDependentOn
method. This means that when we run the ‘Clean’ task the ‘Build-Information’ task will be called first. You’ll see why this is useful when we start chaining other more important tasks together.
The WithCriteria
method allows us to specify criteria that must be met in order for the task to be executed, otherwise, the task will be skipped. In this case, the ‘Clean’ task will only be executed if a ‘rebuild’ argument has been specified when running the build script (e.g. dotnet cake --target Clean --rebuild
).
The implementation of the ‘Clean’ task is very basic. The CleanDirectory
alias is used to empty the bin directory. The Information
alias is then used to output that the cleaning operation has been completed successfully.
Note that in the example, part of the directory path is hardcoded. I strongly recommend creating a separate ‘paths.cake’ file containing your directory and file paths. You can then reference these paths from your main build script (more to follow on this).
Restore
Although the .NET Core build process will automatically restore NuGet packages if required, you may still prefer to create a separate build task for restoring packages. The example below demonstrates how you can do this.
Task("Restore") .IsDependentOn("Clean") .Does(() => { DotNetRestore(solutionFileName); });
The ‘Restore’ task is dependent on the ‘Clean’ task. However, note that if the ‘Clean’ task is skipped because the criteria to run it wasn’t met, the ‘Restore’ task will still execute.
The DotNetRestore
alias takes care of abstracting away the necessary command-line calls to restore NuGet packages for us. If required, you can pass in an instance of the DotNetRestoreSettings
class if you need more control over the restore process.
Build
Now for the most important task, below is a simple example of how to build our application.
Task("Build")     .IsDependentOn("Restore")     .Does(() => {     DotNetBuild(solutionFileName, new DotNetBuildSettings     {         Configuration = configuration, NoRestore = true     }); });
The DotNetBuild
alias is used to build our solution. An instance of the DotNetBuildSettings
class is used to customise how the build is carried out. The Configuration
property is set according to the configuration specified by the relevant build script argument i.e. ‘Debug’ or ‘Release’. The NoRestore
property is set to true since we’ve already restored NuGet packages in the ‘Restore’ task.
There are lots of other settings you can configure, including MSBuildSettings
. These give you a lot of flexibility for cases where you need to ensure that specific platform and target settings are used, for example.
Note that you can pass a settings object to most Cake aliases and the settings class typically derives ultimately from a base class called ToolSettings
which exposes an ArgumentCustomization
property. This is a Func
property that allows you to customise the arguments that are passed to the underlying tooling.
Test
After building our application we can proceed to run unit tests. Below is an example of how to do this.
Task("Test")     .IsDependentOn("Build")     .Does(() => {     DotNetTest(solutionFileName, new DotNetTestSettings     {         Configuration = configuration,         NoBuild = true     }); });
The DotNetTest
alias provides a convenient means of running tests. In the above example, the Configuration
property is set according to the configuration specified by the relevant build script argument. The NoBuild
property is set to true since we have just built the application in the previous task.
Deploy
Usually, the last part of the build process is the deployment of the compiled application.
Deployment for your particular application could mean creating a setup file or zip file and transferring it somewhere, or uploading your build files to a web server and restarting the website process.
Cake provides several aliases that can help you with deployment. However, because deployment processes vary so greatly, most deployment methods are provided via Cake ‘addins’. An example of such an addin is the Cake WebDeploy addin which can be used to publish to IIS or Azure App Services etc.
Aside from this, there are built-in aliases and community addins available specifically for OctopusDeploy, Azure DevOps and many other platforms.
Considerations
Before wrapping up this article, I want to mention a couple of things to bear in mind as you write your build scripts.
Separate files
It’s a good idea to move parts of your build script into separate files. This helps to keep your script files smaller and easier to comprehend.
For example, you could create a file called ‘extension-methods.cake’ and move the convenience methods we covered earlier into this as follows.
///////////////////////////////////////////////////////////////////////////////
// METHODS
///////////////////////////////////////////////////////////////////////////////
public static void DisplayCakeVersion(this ICakeContext context) { string cakeVersion = typeof(ICakeContext).Assembly.GetName().Version.ToString(); context.Information($"Building using version {cakeVersion} of Cake"); } public static void NewLine(this ICakeContext context) { context.Information(""); }
Notice that the methods are now marked as public static
and are defined as extension methods on the ICakeContext
interface. When we want to use a Cake alias method we must invoke it on the context object.
To use these convenience methods from our main Cake build script, we must make a few changes there too.
///////////////////////////////////////////////////////////////////////////////
// LOAD
///////////////////////////////////////////////////////////////////////////////
#load extension-methods.cake
// Other code left out for brevity...
///////////////////////////////////////////////////////////////////////////////
// TASKS
///////////////////////////////////////////////////////////////////////////////
Task("Build-Information")
.Does(()Â =>Â
{
Context.DisplayCakeVersion();
Context.NewLine();
Information($"Target:Â {target}");
Information($"Configuration:Â {configuration}");
Information($"SolutionFileName:Â {solutionFileName}");
});
Notice in the above build script how we use the #load
directive to load the file containing the convenience methods.
Note that if the file were located in a different directory we would need to specify the relative or full path instead of just the filename.
Also notice that instead of just writing DisplayCakeVersion();
as before, we now need to write Context.DisplayCakeVersion();
so that we can invoke the alias methods correctly.
Paths
Further to my advice regarding separate files, it’s a good idea to have a separate file containing any paths that are specific to your application. Below is a very brief example of the contents of a file called ‘paths.cake’.
public static class Paths { public static string SolutionFileName { get; set; } public static string VisualStudioFilePath => @"C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\devenv.com\"; // Other paths and filenames... }
The property values can be set directly if they don’t need to change on a case-by-case basis, as per the VisualStudioFilePath
property. Alternatively, you can set the property values in your main build script, for example, in the Setup
hook based on argument or environment variable values. You can then add properties that combine the values of other properties to build different directory paths or file paths.
Note that the Cake for Visual Studio extension doesn’t always get the syntax highlighting right, so some of the colours in the example code used in this article may look a little off. I have kept this as is so that you know what to expect.
Addins
Depending on your specific scenario, you’ll probably not be able to achieve everything you need to do using only the officially supported Cake aliases. At some point, you’ll need to turn to community-developed addins.
Aside from the Cake WebDeploy addin that was mentioned previously, a great example of this is the Cake Git addin.
Below is an example of how to reference and use the Cake Git addin.
///////////////////////////////////////////////////////////////////////////////
// ADDINS
///////////////////////////////////////////////////////////////////////////////
#addin nuget:?package=Cake.Git&version=2.0.0
///////////////////////////////////////////////////////////////////////////////
// TASKS
///////////////////////////////////////////////////////////////////////////////
Task("Check-Git-Status") .Does(() => { bool uncommittedChanges = GitHasUncommitedChanges("."); if (uncommittedChanges) Warning("There are uncommitted changes"); });
The #addin
directive is used to reference the addin and a particular version of the addin is specified. This helps to improve the reliability of the script. If we don’t specify a version Cake will pull down the latest addin version automatically which could introduce breaking changes.
In the above example, the GitHasUncommitedChanges
method that is defined as part of the addin is used to check if there are any uncommitted changes in the local Git repository. If so, a warning is logged using the Warning
alias. If you wanted the script to fail in this scenario you could simply throw an Exception
in the same way you normally would in a C# program.
The Cake Git addin provides lots of other useful methods such as GitPull
which could be useful for continuous integration to make sure that the latest code has been merged before building the software.
I encourage you to check out the great Cake Reference page to see the full list of Cake aliases and addins that are available. Whatever problem you’re trying to solve likely has a solution already available as part of Cake.
Summary
In this article, I have continued the Cake journey by covering practical examples of Cake tasks that you can use and extend as part of your own automated build process.
I started by looking at some examples of built-in Cake aliases that act as convenience methods to help make our scripts easier to build and comprehend.
I then proceeded to document several different examples of Cake tasks that you can include in your own build scripts and adapt according to your needs.
Lastly, I cover some considerations you should make when creating Cake builds scripts in relation to separate files, directory paths and file paths, and Cake addins for more custom scenarios.
Comments