.NET build tool using Bullseye and SimpleExec
Tags: dotnet, build
Published: 2022-02-23 06:28:08

_This post was originally published at https://mysticmind.dev/dotnet-build-tool-using-bullseye-and-simpleexec_

[NAnt](http://nant.sourceforge.net/) 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](https://cakebuild.net/) and [FAKE](https://fake.build/) gained a lot of adoption providing a C# and F# based DSL for build tasks accordingly. 

I personally advocate [Bullseye](https://github.com/adamralph/bullseye) in combination with [SimpleExec](https://github.com/adamralph/simple-exec) 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](https://twitter.com/adamralph). 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](https://github.com/JasperFx/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](https://github.com/mysticmind/bullseye-sample) available in GitHub which has the following setup and then attempt to add a build script for it:

-  `src/WebApp` folder contains a .NET 6 web app with a Razor page to serve the index. The project uses minimal API. 
- `src/WebApp/static/css/site.css` contains the css file for the web app which is referenced in the Razor page
- `package.json` at the root folder which has dev dependencies to `esbuild` and 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`
- ESBuild is used to minify the static assets i.e. css files from `src/WebApp/static/css`  and adds it under `wwwroot/css`

Now let us look at adding a build script using Bullseye and SimpleExec:
- Add a `build` folder at the top level
- Add a new console project under the `build` folder as below
```bash
dotnet new console
```
- Add `Bullseye`  and `SimpleExec` NuGet packages to `build.csproj`
```bash
dotnet add package Bullseye
dotnet add package SimpleExec
```
- Open `Program.cs` to 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.

```csharp
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.sh` with executable permission as below to be used in *nix environments to run a the build

```bash
#!/usr/bin/env bash

set -euo pipefail

dotnet run --project build/build.csproj -c Release -- "$@"
```
- Create `build.cmd` with the following content to be used in Windows commandline

```cmd
@echo Off
dotnet run --project build/build.csproj -- %*
```
- Create `build.ps1` with the following content to be used from PowerShell prompt

```powershell
$ErrorActionPreference = "Stop";
dotnet run --project build/build.csproj -- $args
```

- To run the default build target, we use the below:

```bash
./build.sh
```
You will see an output as below:
```shell
...
...
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
```bash
# In this example, `esbuild:dist` is the build target
./build.sh esbuild:dist
```
You will see an output as below:
```bash
...
...
⚡ 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](https://github.com/JasperFx/marten/tree/master/build).

Happy coding :-)