Getting started
runna turns a set of build targets into a command line: each target is a type, dependencies run first, and every
target gets --help from its doc comment. It requires .NET 11.
dotnet add package Runna # C#
dotnet add package Runna.Fs # F#
Both examples below define the same three targets: test needs build, which needs clean.
A file-based app next to your solution is enough. Save
this as build.cs:
#!/usr/bin/env -S dotnet --
#:package [email protected]
#: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);
Targets are the cases of a C# union, one record each, and the /// <summary> becomes the target's description.
Make the file executable once, then run it like any CLI:
chmod +x build.cs
./build.cs --help
./build.cs test
dotnet run build.cs -- test works the same, on every platform.
F# targets are the cases of a DU, so a case you forget in Bind is a compiler warning (FS0025). Each target body is
a Proc.Fs shell { } block. Put the build 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>
and build/Program.fs:
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
Either one prints the same help, with each target's dependencies:
build
Usage:
build <command|target> [options] [<target> [options]...]
Targets: (atomic steps — can also be run directly)
clean Delete build output
build Build the solution (depends on: clean)
test Build, then run the tests (depends on: build)
and test --graph draws what test would run, without running it:
test Build, then run the tests
└─ build
└─ clean
Runs: clean → build → test
- Choosing what runs: several targets on one command line,
-sand--skip. - Drawing the plan:
--graphas a tree, Mermaid or JSON. - Running targets side by side: parallel stages,
-jand--keep-going. - Conditions, ordering and cleanup:
OnlyWhen,After,Cleanup,NeedsEnvandNeedsTool. - Wrappers: one task per target for mise, make and just.
The package READMEs cover the rest of the DSL, such as typed per-target flags, global options, namespaces and pipeline
commands: Runna for C# and
Runna.Fs for F#.