Typed build targets for .NET: your targets are the cases of a C# union or an F# DU, dependencies run first, and every target gets real --help.
$ ./build.cs release --graph
release Verify gates, pack, generate notes, diff APIs
├─ requires
│ ├─ pristine-check
│ └─ test
└─ composes
├─ pkg generate
├─ pkg validate
├─ release-notes
└─ api-changes
Runs: pristine-check → test → pkg generate → pkg validate → release-notes → api-changes
dotnet add package Runna # C#
dotnet add package Runna.Fs # F#The same three targets in either language: test needs build, which needs clean. It requires .NET 11.
A file-based app next to your solution is enough.
#!/usr/bin/env -S dotnet --
#:package Runna@0.2.0
#:property GenerateDocumentationFile=true
#:property NoWarn=CS1591
using Nullean.Runna;
using static Nullean.Runna.Shell;
var app = new RunnaApp<Target>("build");
app.Bind(t => t.Value switch
{
Clean => app.Target(() => Exec("dotnet clean")),
Build => app.Target([new Clean()], () => Exec("dotnet build")),
Test => app.Target([new Build()], () => Exec("dotnet test")),
_ => throw new InvalidOperationException($"unmapped target {t.Value}")
});
return await app.RunAsync(args);
/// <summary>Delete build output</summary>
record Clean;
/// <summary>Build the solution</summary>
record Build;
/// <summary>Build, then run the tests</summary>
record Test;
union Target(Clean, Build, Test);./build.cs --help
./build.cs test --graphTargets are the cases of a DU, so a case you forget in Bind is a compiler warning (FS0025), and each target body is a
Proc.Fs shell { } block. Put it in a small project, build/build.fsproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net11.0</TargetFramework>
<!-- the /// comments become each target's description in the help -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<Compile Include="Program.fs" />
<PackageReference Include="Runna.Fs" Version="0.2.0" />
</ItemGroup>
</Project>open Nullean.Runna.Fs
open Proc.Fs
type Target =
/// Delete build output
| Clean
/// Build the solution
| Build
/// Build, then run the tests
| Test
[<EntryPoint>]
let main argv =
let app = RunnaApp<Target>("build", None)
app.Bind <| function
| Clean -> target [] { sh $"dotnet clean" }
| Build -> target [Clean] { sh $"dotnet build" }
| Test -> target [Build] { sh $"dotnet test" }
app.RunAsync(argv).GetAwaiter().GetResult()dotnet run --project build -- --help
dotnet run --project build -- test --graphBoth print the same help and plan. The package READMEs go further: typed per-target flags, namespaces, pipeline commands, parallel stages and the Runna / Runna.Fs specifics.
| Package | What it is | |
|---|---|---|
Runna |
C# build targets as the cases of a union |
|
Runna.Fs |
F# build targets as DU cases |
- Documentation: https://runna.nullean.net
- Built on argh, the source-generated CLI framework
- Releases
- Contributing
MIT licensed. Copyright Nullean and contributors.
