In this post I show two ways to use the Cake build system to build .NET Core projects on Linux: using the Cake.CoreCLR library, or the Cake.Tool .NET Core global tool. This post only deals with bootstrapping Cake, it doesn't describe how to write Cake build scripts themselves. For that, I suggest reading Muhammad Rehan Saeed's post which provides a Cake build script, or my previous post on using Cake in Docker.

Cake, Cake.CoreCLR, and Cake.Tool

In a previous post, I described using Mono to run the full .NET Framework version of Cake on Linux. That was fine for my purposes then, as I was using Mono anyway to build libraries that targeted .NET Framework on Linux, and I wanted to run full framework tests on Linux.

However, if you only need to build (and not run), you can now use the Microsoft.NetFramework.ReferenceAssemblies NuGet packages to build .NET Framework libraries on Linux, without having to explicitly install Mono. For that reason, I think the full .NET Framework version of Cake is no longer the best option for building on Linux.

Luckily, there are currently three different versions of Cake:

  • Cake.Tool: a .NET Core global tool, targeting .NET Core 2.1
  • Cake.CoreCLR: a .NET Core console app targeting .NET Core 2.0
  • Cake: a .NET Framework console app targeting .NET Framework 4.6.1/Mono

Which option is best for you will likely depend on your exact environment. The good thing about these tools is that you should be able to switch between them without having to change your actual build.cake script at all. The difference is primarily in how you acquire and run Cake, i.e. the bootstrapping scripts you use.

Note that in this post I assume you already have the correct version of .NET Core installed. For examples of installing the .NET Core SDK as part of the bootstrapping script, see this example from the Cake project itself.

Building on Linux with Cake.CoreCLR

My first approach in converting away from Mono-based Cake was to use the Cake.CoreCLR library. I felt like this would be an easy drop in replacement, though it took a bit of googling to find some suggested bootstrapping scripts. The bootstrapping scripts effectively took one of two approaches:

  • Use the .NET Core CLI to restore the Cake.CoreCLR package
  • Use curl to download the Cake.CoreCLR package, and manually extract it

The first of these approaches is interesting. The dotnet CLI allows you to restore NuGet packages that have been added to a project, but doesn't allow you to restore arbitrary packages. To work around this, you can do something like the following:

dotnet new classlib -o "$TEMP_DIR" --no-restore
dotnet add "$TEMP_PROJECT" package Cake.CoreCLR --package-directory "$TOOLS_DIR" --version "$CAKE_VERSION"
rm -rf "$TEMP_DIR"

This does four things:

  • Creates a temporary .NET Core project using dotnet new in the $TEMP_DIR directory
  • Adds the Cake.CoreCLR NuGet package to the project
  • Implicitly restores the package to a specific directory, $TOOLS_DIR
  • Deletes the temporary project

After running this script, the Cake.CoreCLR NuGet package has been downloaded and extracted to the tools directory.

The following bash script shows how this fits in to the overall bootstrapping script. This is just the version I came up with; I've listed a variety of example scripts at the end of this post.

#!/usr/bin/env bash

# Define directories.
SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
TOOLS_DIR=$SCRIPT_DIR/tools
TEMP_DIR=$TOOLS_DIR/build
TEMP_PROJECT=$TEMP_DIR/build.csproj

# Define default arguments.
SCRIPT="build.cake"
CAKE_VERSION="0.33.0"
CAKE_ARGUMENTS=()

# Parse arguments.
for i in "[email protected]"; do
    case $1 in
        -s|--script) SCRIPT="$2"; shift ;;
        --cake-version) CAKE_VERSION="$2"; shift ;;
        --) shift; CAKE_ARGUMENTS+=("[email protected]"); break ;;
        *) CAKE_ARGUMENTS+=("$1") ;;
    esac
    shift
done

CAKE_PATH="$TOOLS_DIR/cake.coreclr/$CAKE_VERSION/Cake.dll"

if [ ! -f "$CAKE_PATH" ]; then
    echo "Restoring Cake..."

    # Make sure the tools folder exists
    if [ ! -d "$TOOLS_DIR" ]; then
        mkdir "$TOOLS_DIR"
    fi

    # Build the temp project and restore Cake
    dotnet new classlib -o "$TEMP_DIR" --no-restore
    dotnet add "$TEMP_PROJECT" package Cake.CoreCLR --package-directory "$TOOLS_DIR" --version "$CAKE_VERSION"

    rm -rf "$TEMP_DIR"
fi

# Start Cake
exec dotnet "$CAKE_PATH" "$SCRIPT" "${CAKE_ARGUMENTS[@]}"

The first half of the script is parsing command-line arguments and defining defaults. The path we expect to find Cake.dll is checked, and if it's not found, we use the previous technique to restore it. Finally, we execute cake using the form dotnet Cake.dll.

Notice that I've "pinned" the Cake.CoreCLR version to 0.33.0 in the script, to ensure we get a consistent version of Cake on the build server.

When you run this script, you'll see output similar to the following, showing how the process works:

Restoring Cake...
Getting ready...
The template "Class library" was created successfully.
  Writing /tmp/tmp8yvA9D.tmp
info : Adding PackageReference for package 'Cake.CoreCLR' into project '/sln/tools/build/build.csproj'.
info : Restoring packages for /sln/tools/build/build.csproj...
info :   GET https://api.nuget.org/v3-flatcontainer/cake.coreclr/index.json
info :   OK https://api.nuget.org/v3-flatcontainer/cake.coreclr/index.json 438ms
info :   GET https://api.nuget.org/v3-flatcontainer/cake.coreclr/0.33.0/cake.coreclr.0.33.0.nupkg
info :   OK https://api.nuget.org/v3-flatcontainer/cake.coreclr/0.33.0/cake.coreclr.0.33.0.nupkg 25ms
info : Installing Cake.CoreCLR 0.33.0.
info : Package 'Cake.CoreCLR' is compatible with all the specified frameworks in project '/sln/tools/build/build.csproj'.
info : PackageReference for package 'Cake.CoreCLR' version '0.33.0' added to file '/sln/tools/build/build.csproj'.
info : Committing restore...
info : Generating MSBuild file /sln/tools/build/obj/build.csproj.nuget.g.props.
info : Generating MSBuild file /sln/tools/build/obj/build.csproj.nuget.g.targets.
info : Writing assets file to disk. Path: /sln/tools/build/obj/project.assets.json
log  : Restore completed in 2.95 sec for /sln/tools/build/build.csproj.

This is quite a clever way to fetch the NuGet package, but it's probably a bit overly complicated. Another option is to directly download the NuGet and unzip it. You could replace the dotnet new approach in the above script with the following instead:

curl -Lsfo "$TOOLS_DIR/cake.coreclr.zip" "https://www.nuget.org/api/v2/package/Cake.CoreCLR/$CAKE_VERSION" \
    && unzip -q "$TOOLS_DIR/cake.coreclr.zip" -d "$TOOLS_DIR/cake.coreclr/$CAKE_VERSION" \
    && rm -f "$TOOLS_DIR/cake.coreclr.zip"

if [ $? -ne 0 ]; then
    echo "An error occured while installing Cake."
    exit 1
fi

This directly downloads the NuGet as a .zip file, extracts it, and deletes the zip file.

Note that you must have the unzip command available in your environment - I don't believe it's generally installed by default.

Either of these approaches work, and enable you to download the Cake.CoreCLR NuGet package. The alternative is to install the .NET Core global tool.

Building on Linux with the Cake global tool

.NET Core 2.1 introduced the concept of global tools. These are CLI tools that can be installed globally on your machine, and are effectively just console apps. I described how to create a .NET Core global tool in a previous post. Cake provides a global tool that can be used with .NET Core 2.1+.

Depending on your use case, you may or may not want to actually install the Cake tool globally. Global tools can also be installed to a specific folder by passing the --tool-path argument, in which case you can have multiple instances of the tool installed.

My preference is to install global tool locally for each solution, to remove the dependence on outside tooling. This has the advantage of making each project self contained, and allowing different versions of the tool per-solution. The down-side is that you're using more disk space by installing the tool multiple times.

To install Cake as a global tool, into a local tools folder, you can run

dotnet tool install Cake.Tool --tool-path ./tools --version 0.33.0

You can then invoke the tool using

exec ./tools/dotnet-cake

Note that we installed the tool using --tool-path, so we have to use the path to dotnet-cake directly; if you installed it globally, you could use dotnet-cake (or dotnet cake) from any folder.

Adding those commands to the bootstrapping script gives the following:

#!/usr/bin/env bash

# Define directories.
SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
TOOLS_DIR=$SCRIPT_DIR/tools

# Define default arguments.
SCRIPT="build.cake"
CAKE_VERSION="0.33.0"
CAKE_ARGUMENTS=()

# Parse arguments.
for i in "[email protected]"; do
    case $1 in
        -s|--script) SCRIPT="$2"; shift ;;
        --cake-version) CAKE_VERSION="--version=$2"; shift ;;
        --) shift; CAKE_ARGUMENTS+=("[email protected]"); break ;;
        *) CAKE_ARGUMENTS+=("$1") ;;
    esac
    shift
done

# Make sure the tools folder exists
if [ ! -d "$TOOLS_DIR" ]; then
    mkdir "$TOOLS_DIR"
fi

CAKE_PATH="$TOOLS_DIR/dotnet-cake"
CAKE_INSTALLED_VERSION=$($CAKE_PATH --version 2>&1)

if [ "$CAKE_VERSION" != "$CAKE_INSTALLED_VERSION" ]; then
    if [ -f "$CAKE_PATH" ]; then
        dotnet tool uninstall Cake.Tool --tool-path "$TOOLS_DIR" 
    fi

    echo "Installing Cake $CAKE_VERSION..."
    dotnet tool install Cake.Tool --tool-path "$TOOLS_DIR" --version $CAKE_VERSION

    if [ $? -ne 0 ]; then
        echo "An error occured while installing Cake."
        exit 1
    fi
fi


# Start Cake
exec "$CAKE_PATH" "$SCRIPT" "${CAKE_ARGUMENTS[@]}"

As before, the first half of the script is parsing command line arguments and setting up defaults. We then check to see if the correct version of the Cake global tool is installed. If the wrong version is installed, we uninstall the old version first, otherwise we install the correct version. Finally we execute the script using the path to the global tool.

If you're using this script in Docker containers there's pretty much no chance of there being a different version of the Cake tool installed, so you could remove the version check section if you prefer.

The first time you run the script, you'll see the global tool installed:

Installing Cake 0.33.0...
You can invoke the tool using the following command: dotnet-cake
Tool 'cake.tool' (version '0.33.0') was successfully installed.

Subsequent executions will execute the tool directly, without needing to install the tool.

Personally, I think the global tool is the way to go in all cases where you can use it (.NET Core 2.1+). Global tools are due to be updated in .NET Core 3.0, but I expect that this will remain the canonical way to use Cake on Linux/Mac.

Summary

In this post I showed two ways to run Cake on Linux: using Cake.CoreCLR, or the .NET Core global tool Cake.Tool. Generally speaking I would suggest using the global tool where possible, as it seems to be the preferred approach - even the Cake project itself is built using the .NET Core global tool!

Resources