From Cake to Nuke - #1 Cleaning things up

Distracted Boyfriend meme from cake to nuke

Cake, it's been great, however after watching Matthias Koch's presentation — NUKE — a modern build system for C#/.NET, I knew it was time to part ways.

Why would I invest the time in replacing my existing Cake build scripts with the newer Nuke system?

  • The Path construction and Value injections are both ideas that I find quite powerful and simplify things greatly.
  • The Build "script" is literally just a .NET Core Console application. This means that errors can be caught at compile time, not at runtime as with Cake's preprocessor system.
  • Much better tooling support (Intellisense, Visual Studio, Visual Studio Code, Rider and Resharper). Unlike Cake's build scripts that must be preprocessed into

First, as our existing Cake build scripts are located in a 'Build' folder, which Nuke also uses, let's rename our 'Build' folder to 'BuildCake' so that it doesn't conflict with the new system. Doing so also allows us to reference the existing Cake build script as we work on the new Nuke build system.

If you haven't already, install the Nuke global tool by running the following command in PowerShell or a Command Prompt.

dotnet tool install Nuke.GlobalTool --global

Next, lets use Nuke's command line wizard to generate the base build project for us. In the solution folder, simply run the 'nuke' command and answer the questions as they are asked:

Cake initial setup questions

Note, when using Nuke version 0.24.2, in the generated Build.cs file of the _build project, I needed to add the following using statement so that the 'AbsolutePath' type could be found.

using Nuke.Common.IO;

To run a build from within Visual Studio, you will need to install the 'NUKE Support' extension. After doing so, you can open the 'Task Runner Explorer' window and perform a build for the targets to show.

Let's start by implementing one of the easier existing build targets I have in Cake already, the 'Clean' target. Currently the existing Cake target looks like this:

Task("Clean")
  .Does(() => {
    CleanDirectories("./../**/bin");
    CleanDirectories("./../**/obj");

    DeleteFiles("./*.nupkg");
    DeleteFiles(releaseNotesFilePath);

    DeleteFiles("./*Results.xml");
});

As you can see in my Cake target above, I like to clean the bin and obj folders as part of my clean targets. While Nuke does provide equivalent methods for one by one Delete and Clean operations, it currently does not support Glob Patterns on these operations. However, Nuke does expose respective GlobFiles() and GlobDirectories() methods so you are able to chain these two operations together.

For example, to CleanDirectories as above, you could simply chain Nuke GlobDirectories and EnsureCleanDirectories commands together as follows:

EnsureCleanDirectories(GlobDirectories(RootDirectory, @"**/bin", @"**/obj"));

With the introduction of Nuke however, the build project's bin and obj folders are required to run the build itself, so we need to ignore these in the Clean target otherwise we will encounter file in use exceptions when running this target. Since there wasn't an existing EnsureCleanDirectores/DeleteDirectories method in Nuke with an ignore list, I simply created my own as follows:

private void DeleteDirectories(string directory, string[] globPatterns, string[] ignoreList = null)
   {
      if (ignoreList == null)
      {
         ignoreList = new string[0];
      }

      var toDelete = GlobDirectories(directory, globPatterns);
      toDelete.ForEach(_ =>
      {
         if (ignoreList.Any(_.Contains))
         {
            Console.WriteLine($"DeleteDirectories: Ignoring '{_}' directory...");
            return;
         }

         DeleteDirectory(_);
      });
   }

Likewise, as although there is a GlobFiles method, there isn't a DeleteFiles method in Nuke, I used above DeleteDirectories method as the basis for a new DeleteFiles method as follows:

private void DeleteFiles(string directory, string[] globPatterns, string[] ignoreList = null)
   {
      if (ignoreList == null)
      {
         ignoreList = new string[0];
      }

      var toDelete = GlobFiles(directory, globPatterns);
      toDelete.ForEach(_ =>
      {
         if (ignoreList.Any(_.Contains))
         {
            Console.WriteLine($"DeleteFiles: Ignoring '{_}' file...");
            return;
         }

         DeleteFile(_);
      });
   }

Putting it all together, my new Nuke Clean target now looks like this:

Target Clean => _ => _
      .Before(Restore)
      .Executes(() =>
      {
         EnsureCleanDirectory(OutputDirectory);

         DeleteDirectories(RootDirectory, new string[] {@"**/bin", @"**/obj"}, new string[] {"/build/", @"\build\"});

         DeleteFiles(RootDirectory, new string[] { "./*.nupkg", ReleaseNotesFilePath, "./*Results.xml" });
      });

To run the Clean target from a PowerShell terminal, simply navigate to the Solution folder, where the build.nuke file exists and run the command:

.\build -Target Clean

So far so good. Next up in the series, creating the overall build plan for our build script.

Further Reading: Paul Knopf's You don't need Cake anymore; the way to build .NET projects going forward.