This is the second issue in a series around designing a build system for .NET projects
- The build submodule & Versioning dependencies
- Build, test and pack .NET projects by convention using Cake
- Build and pack .NET applications ready for deployment
- Introducing build configuration options and smart defaults
- Versioning strategy using GitVersion and auto-tagging from your CI build
- Templating new .NET solutions from our build submodule
Previously we began our journey in designing a build system by setting up a GIT submodule
build and adding support for centralised NuGet dependency versioning. In this issue, we'll introduce the Cake build tool, and look at how we can build a convention-based build system.
A reminder of the available repositories:
- The parent repo - https://github.com/Antaris/example-repo-with-build-system
The Cake build tool
Cake is a build tool that allows you to write C# build files (
.cake files). It is a flexible scripting system that has first class support for building .NET projects. Cake makes uses of the Roslyn compiler to compiler your Cake files into runnable code - for building, testing and packaging code.
Our use of Cake is exactly this, but what I want to do is introduce a build script that builds by convention. To get started, let's install the Cake CLI as a global tool
dotnet tool install --global Cake.Tool
At the time of writing, the current version of
0.35.0 ( NuGet ).
Next, we can create our first Cake script. So create a file at
So there are a few things going on here
- We define a set of arguments, in this case
configurationwhich defaults to
targetwhich defaults to
- We define a set of tasks, one named
Buildand another named
Default. The latter will execute
- We run task represented by the
Let's try executing this, using the
dotnet-cake command we installed previously (from the
cd build dotnet-cake
If all goes well, it will execute the tasks, which results in the following output:
======================================== Build ======================================== ======================================== Default ======================================== Task Duration -------------------------------------------------- Build 00:00:00.0093558 -------------------------------------------------- Total: 00:00:00.0108493
Although quite uninspiring at this stage, these are building blocks to building something bigger.
You can execute a specific target, or use a specific configuration:
dotnet-cake -Target=Build -Configuration=Debug
By having our build script centred within our
build submodule, this means we can approach designing our script by convention. We want our
build submodule to apply the same conventions to any project that we need.
For this to work, it requires those consumer projects to utilise the same layout. So the conventions we will be using are:
- Libraries (those destined for NuGet package feeds) are located under
- Apps (those destined for deployment somewhere) are located under
- Tests are located under
- Build outputs (artefacts) will be saved to
We can embody this as an anonymous object:
Now, we need to determine how we are going to build our solution. But to answer that question for a convention-based build system, we need some logic to determine what the correct solution will be.
Initially this could be an input argument:
If the user provides this argument, we can use this directly, e.g.:
Otherwise, we need to figure out the solution. The rules are fairly simple, if there is exactly one solution, we use that, otherwise we throw an error, as we do not know which solution we should build. Let's define a function to handle this:
We're making use of some of Cake's primitives -
FilePath. These types normalise a lot of the difference between the path systems of different OSes. Now we can resolve our solution file, we can update our
Let's try executing our solution now:
======================================== Build ======================================== Using solution file: C:/blog/example/Example.sln Microsoft (R) Build Engine version 16.4.0+e901037fe for .NET Core Copyright (C) Microsoft Corporation. All rights reserved. Restore completed in 172.66 ms for C:\blog\example\src\ExampleLib\ExampleLib.csproj. ExampleLib -> C:\blog\example\src\ExampleLib\bin\Release\netstandard2.0\ExampleLib.dll Build succeeded. 0 Warning(s) 0 Error(s) Time Elapsed 00:00:01.93 ======================================== Default ======================================== Task Duration -------------------------------------------------- Build 00:00:02.3856024 -------------------------------------------------- Total 00:00:02.3856024
Success! An initial solution build!
Cleaning stale artefacts
When we repeat builds, ideally we would like the result to be idempotent when nothing has changed. To support this, it is important for us to sweep away any previously built artefacts, we can do this by defining a new task:
This task does two things:
- Cleans away stale artefacts under
- Removes any build outputs from our configuration-specific builds at the standard output locations. So for our example project running in
Releaseconfiguration, this would mean deleting everything under
We now need to tell Cake that this task is a dependency for the
Build task, so let's update the definition:
By adding this as a per-task dependency, it allows you to execute individual tasks and ensuring the correct prerequisite tasks are run in order. This means if I decided to run a specific target:
I can be assured that my
Clean tasks is executed before build.
Running unit tests
We do not currently have a unit test project defined, so let's go ahead and do that now - from the root of your repo, run the following:
mkdir tests cd tests dotnet new xunit --name ExampleLib.Tests cd .. dotnet sln add ./tests/ExampleLib.Tests/ExampleLib.Tests.csproj
We're not too concerned with having actual tests at this stage, we testing a build system, not actual code. So we can create an empty Xunit project (or NUnit/MSTest project if you are inclined) within our
tests\ directory. We need to add this to our solution so when we perform a build, the unit test project is built.
As a bonus you can add a reference to our
ExampleLib class library, but its not critical:
cd tests\ExampleLib.Tests dotnet add reference ../../src/ExampleLib/ExampleLib.csproj
Now, we can define out
A couple things going on here, let's break it down:
- We'll enumerate each test project under
dotnet testusing Cake's built in aliases.
- We don't bother re-building the libraries as they have been built previously in the
- We output the test results in VS Test Results File, and move this to
artefacts\test-results. This will allow a CI/CD tool to pull the test results to be reported.
Creating NuGet packages
As part of our convention based build, we need to package up any libraries under
src as NuGet packages. We can make use of Cake's native support for
dotnet pack, so let's add a new task
The pack command creates a NuGet package from any project under
src\. Again, much like the
Test task, we do not need to build the project again, as this has already completed as part of the
Before we can run this build, we need to update our
Default task definition:
It's import to understand the dependency chain we've constructed:
Default -> Pack-Libraries -> Test -> Build -> Clean
Executing any one of these targets, will execute the dependency chain from that task onwards. If we run our build again, we should also now see our output NuGet packages:
Takeaways and next steps
Another jam packed issue, however our Cake build is not complete. We still need to build and package up our Apps. Because of the length of this issue, I've split this in two, and will follow up the build steps for the Apps in the next issue.
Let's recap what we've done in this issue:
- We installed the Cake global tool for executing
dotnet tool install --global Cake.Tool
- We have created our build script within our
buildsubmodule - we've called this
build.cakewhich is the default filename supported by the Cake build system
- We've defined a set of conventions for our project layout, embodied as an anonymous object of directory paths
- We've defined a number of tasks to build, test and pack our .NET projects under the
Next up in the series, we'll continue evolving our build script to build and pack Apps, with support for custom per-App build scripts.