Using Cake to Build C # Code
Hello! I want to talk about a tool like Cake (C # Make).
So what is Cake?
Cake is a cross-platform build system that uses DSL with C # syntax to do things such as compiling binaries from source codes, copying files, creating / clearing / deleting folders, archiving artifacts, packing nuget packets, unit runs -tests and more. Cake also has a developed add-on system (just C # classes, often packaged in nuget). It is worth noting that a large number of useful functions are already built into Cake, and even more, for almost all occasions, they are written by the community and are quite successfully distributed.
Take uses a programming model called dependency based programming , similar to other similar systems like Rake or Fake . The essence of this model is that we define tasks and dependencies between them to execute our program. You can read more about this model at Martin Fowler .
A similar model makes us imagine our assembly process as some tasks and dependencies between them. In this case, the execution is logical in the reverse order: we indicate the task that we want to perform and its dependencies, while Cake determines which tasks can be performed (dependencies are allowed or missing) and executes them.
So, for example, we want to execute A, however, it depends on B and C, and B depends on D. Thus, Cake will execute them in the following order:
- C or D
- B
- A
The Task in Cake is usually a complete piece of assembly / testing / packaging work. Declared as follows
Task("A") // Название
.Does(() =>
{
//Реализация Task A
});
To indicate that task A is dependent on, for example, task B, using the IsDependentOn method :
Task("A") // Название
.IsDependentOn("B")
.Does(() =>
{
//Реализация Task A
});
You can also easily set the conditions under which the task will or will not be performed using the WithCriteria method :
Task("B") // Название
.IsDependentOn("C")
.WithCriteria(DateTime.Now.Second % 2 == 0)
.Does(() =>
{
//Реализация Task A
});
If some task depends on task B, and the criterion is false, then task B will not be executed, however, the execution flow will go further and execute the tasks that B depends on.
There is also an overload of the WithCriteria method , which takes as a parameter a function that returns bool. In this case, the expression will be counted only when the queue reaches the task, and not at the time of building the task tree.
Cake also supports some specific preprocessor directives, including load , reference , tool and break . You can read more about them on the corresponding page of the documentation.
I think that there are not so many people who collect their projects with their hands in the DevOps era. The advantage of any assembly system compared to manual assembly is obvious - an automatically configured process is always better than manual manipulation.
Cake Benefits When Developing In C
Why use Cake, since there are so many alternatives? If you are not developing in C #, then most likely there is nothing. And if you are developing, it seems reasonable to write assembly scripts in the same language that the main project is written in, since you do not need to learn another programming language and produce their zoo within the same code base. That's why assembly systems like Rake (Ruby) , Psake (Powershell) or Fake (F #) began to appear .
Cake is certainly not the only way to build a C # project. As options, you can use pure MSBuild, Powershell, Bat scripts or CI Server like Teamcity or Jenkins as an example, however, all of them have both advantages and disadvantages:
- Powershell scripts, just like Bat / Bash, are pretty hard to maintain
- C # based DSL is nicer in the syntax of XML based DSL from MSBuild. In addition, support for MSBuild on Linux / Mac appeared not so long ago.
- The CI server implements Vendor-lock and often requires "programming with the mouse," and therefore unbinds the version of the build process from the version of the code in the repository, although some CI systems allow you to store files describing the build process along with the code.
- Using CI does not allow you to collect code locally in the same way as on the CI server
Cake Installation
Now let's talk about how to execute scripts with tasks. Cake has a bootloader that will do everything for you. You can download it either manually or with the following powershell command:
Invoke-WebRequest http://cakebuild.net/download/bootstrapper/windows -OutFile build.ps1
The downloaded build.ps1 file will then download the necessary cake.exe, if it is not already loaded, and execute the cake script (by default, it is build.cake ) if we call it with the following command:
Powershell -File".\build.ps1" -Configuration"Debug"
We can also pass command line arguments to build.ps1 , which will then be executed. They can be either built-in, for example, configuration , which is usually responsible for the configuration of the assembly, or set independently - in this case, there are two ways:
- Pass as the value of the ScriptArgs parameter :
Powershell -File ".\build.ps1" -Script "version.cake" -ScriptArgs '--buildNumber="123"'
- Modify build.ps1 so that it forwards the passed cake.exe argument.
Examples
Well, now let's move on to practice. One can easily imagine a typical nuget package build cycle:
- We build using MSBuild from dll sources
- We run unit tests
- Putting it all together in nuget according to the nuspec description
- Nuget feed
Build dll
To collect our solution from source, you need to do 2 things:
- Restore the nuget packages that our solution depends on using the NuGetRestore function
- Build the solution by default with the DotNetBuild function built into cake by passing the configuration parameter to it .
We describe the task of building a solution on cake-dsl:
var configuration = Argument("configuration", "Debug");
Task("Build")
.Does(() =>
{
NuGetRestore("../Solution/Solution.sln");
DotNetBuild("../Solution/Solution.sln", x => x
.SetConfiguration(configuration)
.SetVerbosity(Verbosity.Minimal)
.WithTarget("build")
.WithProperty("TreatWarningsAsErrors", "false")
);
});
RunTarget("Build");
The assembly configuration, respectively, is read from the arguments by the command line using the Argument function with the default value of "Debug". The RunTarget function launches the specified task, so that we can immediately verify the correct operation of our cake script.
Unit tests
To run unit tests compatible with nunit v3.x, we need the NUnit3 function , which is not included with Cake and therefore requires connection through the #tool preprocessor directive. The #tool directive allows you to connect tools from nuget packages, which we will use:
#tool "nuget:?package=NUnit.ConsoleRunner&version=3.6.0"
Moreover, the task itself turns out to be extremely simple. Do not forget, of course, that it depends on the Build task:
#tool "nuget:?package=NUnit.ConsoleRunner&version=3.6.0"
Task("Tests::Unit")
.IsDependentOn("Build")
.Does(()=>
{
NUnit3(@"..\Solution\MyProject.Tests\bin\" + configuration + @"\MyProject.Tests.dll");
});
RunTarget("Tests::Unit");
We pack everything in nuget
To package our assembly in nuget, we use the following nuget specification:
<?xml version="1.0" encoding="utf-8"?><packagexmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"><metadata><id>Solution</id><version>1.0.0</version><title>Test solution for demonstration purposes</title><description>
Test solution for demonstration purposes
</description><authors>Gleb Smagliy</authors><owners>Gleb Smagliy</owners><requireLicenseAcceptance>false</requireLicenseAcceptance><tags></tags><references><referencefile="MyProject.dll" /></references></metadata><files><filesrc=".\MyProject.dll"target="lib\net45"/><filesrc=".\MyProject.pdb"target="lib\net45"/></files></package>
Put it in the folder with the build.cake script. When executing the Pack task, we will transfer all the necessary artifacts for packaging to the ".. \. Artefacts" folder. To do this, make sure that it exists (and if not, create it) using the EnsureDirectoryExists function and clean it using the CleanDirectory function built into Cake. With the help of the file copying functions, we will move the dll and pdb we need to the arefactory folder.
By default, the assembled nupkg will be in the current folder, so we will specify the folder ".. \ package" as the OutputDirectory , which we also created and cleaned.
Task("Pack")
.IsDependentOn("Tests::Unit")
.Does(()=>
{
var packageDir = @"..\package";
var artefactsDir = @"..\.artefacts";
MoveFiles("*.nupkg", packageDir);
EnsureDirectoryExists(packageDir);
CleanDirectory(packageDir);
EnsureDirectoryExists(artefactsDir);
CleanDirectory(artefactsDir);
CopyFiles(@"..\Solution\MyProject\bin\" + configuration + @"\*.dll", artefactsDir);
CopyFiles(@"..\Solution\MyProject\bin\" + configuration + @"\*.pdb", artefactsDir);
CopyFileToDirectory(@".\Solution.nuspec", artefactsDir);
NuGetPack(new FilePath(artefactsDir + @"\Solution.nuspec"), new NuGetPackSettings
{
OutputDirectory = packageDir
});
});
RunTarget("Pack");
Publish
To publish packages, the NuGetPush function is used , which takes the path to the nupkg file, as well as settings: a link to nuget feed and the API key. Of course, we will not store the API Key in the repository, but will pass it on the outside again using the Argument function . For nupkg, we’ll just take the first file in the package directory , matching the mask using GetFiles . We can do this because the directory has been previously cleaned before packaging. So, the publishing task is described by the following dsl:
var nugetApiKey = Argument("NugetApiKey", "");
Task("Publish")
.IsDependentOn("Pack")
.Does(()=>
{
NuGetPush(GetFiles(@"..\package\*.nupkg").First(), new NuGetPushSettings {
Source = "https://www.nuget.org/api/v2",
ApiKey = nugetApiKey
});
});
RunTarget("Publish");
Simplify your life
While debugging a cake script, and just to debug a nuget package, you can not publish it every time to a remote feed. This is where the WithCriteria function that we examined will come to our aid . We will pass the PublishRemotely flag to the script with the parameter (set to false by default) in order to determine whether to put the package in the remote feed by the value of this flag. However, cake will not execute the script if we skip the task that the RunTarget functions specified . Therefore , we will start a fictitious empty task BuildAndPublish , which will depend on Publish :
Task("BuildAndPublish")
.IsDependentOn("Publish")
.Does(()=>
{
});
RunTarget("BuildAndPublish");
And add a condition to the Publish task:
var nugetApiKey = Argument("NugetApiKey", "");
var publishRemotely = Argument("PublishRemotely", false);
Task("Publish")
.IsDependentOn("Pack")
.WithCriteria(publishRemotely)
.Does(()=>
{
NuGetPush(GetFiles(@"..\package\*.nupkg").First(), new NuGetPushSettings {
Source = "https://www.nuget.org/api/v2",
ApiKey = nugetApiKey
});
});
The script for building and publishing the nuget package is almost ready, it remains only to combine all the tasks together. The final version of the code can be found in the github repository .
Conclusion
We looked at the simplest example of using cake. This could add integration with slack, monitoring code coverage with tests, and much more. With a rich add-on system, an active community, and pretty good documentation, cake is a pretty good alternative to CI systems and MSBuild for building C # code.