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.

Shell
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:

C#
#!/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:

Shell
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:

Xml
<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:

Fsharp
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()
Shell
dotnet run --project build -- --help
dotnet run --project build -- test

Either one prints the same help, with each target's dependencies:

Text
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:

Text
test  Build, then run the tests
└─ build
   └─ clean

Runs: clean → build → test

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#.