NAnt was quite a popular build tool used by projects of all sizes targeting .NET Framework on Windows. I have written many complex build systems using NAnt in the past. But XML based DSL was quite clunky to use and maintain. Besides, it was always a daunting task to explain to a new dev on the team. With .NET Core/.NET becoming a cross-platform framework, CAKE and FAKE gained a lot of adoption providing a C# and F# based DSL for build tasks accordingly.
I personally advocate Bullseye in combination with SimpleExec as a build tool for .NET projects due to its sheer simplicity by providing a bare bones API to define the tasks and their dependent tasks in plain C# code. Both these libraries are build by Adam Ralph. It does not enforce any specific model or structure to write your build tasks. When someone runs through the build script, it is fairly straight forward to understand whats going on. We use this build tool setup for Marten for both local dev and CI build tasks and has worked well so far. Also many OSS projects have adopted it.
We will take a simple sample .NET project available in GitHub which has the following setup and then attempt to add a build script for it:
src/WebAppfolder contains a .NET 6 web app with a Razor page to serve the index. The project uses minimal API.
src/WebApp/static/css/site.csscontains the css file for the web app which is referenced in the Razor page
package.jsonat the root folder which has dev dependencies to
esbuildand defines 2 scripts as below:
- minify -
esbuild --minify ./src/WebApp/static/css/site.css --outdir=./src/WebApp/wwwroot/css
- watch -
esbuild --watch ./src/WebApp/static/css/site.css --outdir=./src/WebApp/wwwroot/css
- minify -
- ESBuild is used to minify the static assets i.e. css files from
src/WebApp/static/cssand adds it under
Now let us look at adding a build script using Bullseye and SimpleExec:
- Add a
buildfolder at the top level
- Add a new console project under the
buildfolder as below
dotnet new console
SimpleExecNuGet packages to
dotnet add package Bullseye dotnet add package SimpleExec
Program.csto add the build targets in your favourite editor and add the following code (uses .NET 6 minimal API). The inline comments should be self explanatory.
using static Bullseye.Targets; using static SimpleExec.Command; // Create a build target for npm install to make esbuild available for use Target("npm:install", () => Run("npm","install")); // Create build target esbuild:dist target which will minify // CSS under static/css and add it to wwwroot/css folder // This build target has a depedency on `npm:install` build target Target("esbuild:dist", DependsOn("npm:install"), () => Run("npm","run minify")); // Create build target esbuild:dist target which will allow to // watch for changes in dev env // This build target has a depedency on `npm:install` build target Target("esbuild:watch", DependsOn("npm:install"), () => Run("npm","run watch")); // Create a build target Clean the web project output from previous build Target("clean", () => Run("dotnet","clean src/WebApp/WebApp.csproj")); // Create a build target to compile the web project // This build target depends on `clean` build target Target("compile", DependsOn("clean"), () => Run("dotnet","build ./src/WebApp/WebApp.csproj -c Release")); // Create a build target to watch for changes in dev Target("watch", () => Run("dotnet", "watch --project ./src/WebApp/WebApp.csproj")); // Create a default target // This target depends on `esbuild:dist` and `compile` build targets Target("default", DependsOn("esbuild:dist", "compile")); // run the build target requested and exit await RunTargetsAndExitAsync(args);
- At the top level folder, let us create
build.shwith executable permission as below to be used in *nix environments to run a the build
set -euo pipefail dotnet run --project build/build.csproj -c Release -- "$@"
build.cmdwith the following content to be used in Windows commandline
@echo Off dotnet run --project build/build.csproj -- %*
build.ps1with the following content to be used from PowerShell prompt
$ErrorActionPreference = "Stop"; dotnet run --project build/build.csproj -- $args
- To run the default build target, we use the below:
You will see an output as below:
... ... Time Elapsed 00:00:04.88 build: compile: Succeeded (5.66 s) build: default: Succeeded build: ────────────────────────────────────── build: Target Outcome Duration build: ──────────── ───────── ───────────── build: npm:install Succeeded 868 ms 10.1% build: esbuild:dist Succeeded 206 ms 2.4% build: clean Succeeded 1.82 s 21.3% build: compile Succeeded 5.66 s 66.1% build: default Succeeded build: ────────────────────────────────────── build: Succeeded (default) (8.55 s)
- To run any other build target, pass the target name as the parameter to the build file
You will see an output as below:
# In this example, `esbuild:dist` is the build target ./build.sh esbuild:dist
... ... ⚡ Done in 4ms build: esbuild:dist: Succeeded (184 ms) build: ────────────────────────────────────── build: Target Outcome Duration build: ──────────── ───────── ───────────── build: npm:install Succeeded 1.58 s 89.6% build: esbuild:dist Succeeded 184 ms 10.4% build: ────────────────────────────────────── build: Succeeded (esbuild:dist) (1.76 s)
If you want to make changes with hot reload in dev, you would run the .NET project with watch using build script
./build.sh watch and also the ESBuild task for CSS
./build.sh esbuild:watch on two terminal windows and work with it. Build tool shortens the typing required to run things.
Bullseye generates a nice coloured output on the terminal and also adapts the colour profile according to the CI environments such as GitHub, GitLab etc.
If you are looking at a more full fledged build tool in action, you can take a look at the build tool setup for Marten here.
Happy coding :-)