Nullean.Make.Fs 0.23.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Nullean.Make.Fs --version 0.23.0
                    
NuGet\Install-Package Nullean.Make.Fs -Version 0.23.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Nullean.Make.Fs" Version="0.23.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Nullean.Make.Fs" Version="0.23.0" />
                    
Directory.Packages.props
<PackageReference Include="Nullean.Make.Fs" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Nullean.Make.Fs --version 0.23.0
                    
#r "nuget: Nullean.Make.Fs, 0.23.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Nullean.Make.Fs@0.23.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Nullean.Make.Fs&version=0.23.0
                    
Install as a Cake Addin
#tool nuget:?package=Nullean.Make.Fs&version=0.23.0
                    
Install as a Cake Tool

Nullean.Make.Fs

Typed, namespaced, exhaustive F# build targets. Your targets are DU cases, and a case you forget to handle in Bind is a compiler warning (FS0025).

open Nullean.Make.Fs
open Proc.Fs

type TestOptions = { Filter: string option }

type PkgTarget =
    /// Pack the NuGet packages
    | Generate
    /// Validate the packed NuGet packages
    | Validate

type Target =
    /// Clean known output locations
    | Clean
    /// Run build
    | Build
    /// Run build then tests
    | Test of TestOptions
    | Pkg of PkgTarget
    /// Test, then pack and validate
    | Release

[<EntryPoint>]
let main argv =
    let app    = MakeApp<Target>("build", None)
    let config = app.Option<string>("--config", desc = "Build configuration", defaultValue = "Release")

    app.Bind <| function
        | Clean        -> target [] { exec { run "dotnet" "clean" } }
        | Build        -> target [Clean] { exec { run "dotnet" ["build"; "-c"; config.Value] } }
        | Test o       -> target [Build] {
                              match o.Filter with
                              | Some f -> exec { run "dotnet" ["test"; "-c"; config.Value; $"--filter:{f}"] }
                              | None   -> exec { run "dotnet" ["test"; "-c"; config.Value] } }
        | Pkg Generate -> target [] { exec { run "dotnet" ["pack"; "-c"; "Release"; "-o"; "build/output"] } }
        | Pkg Validate -> target [] { exec { run "dotnet" ["nupkg-validator"; "build/output"] } }
        | Release      -> command {
                              requires [Test { Filter = None }]
                              composes [Pkg Generate; Pkg Validate] }

    app.RunAsync(argv).GetAwaiter().GetResult()
build --help
build test --filter Unit --config Debug
build pkg --help
build release
build release -s        # skip the tests, run only pkg generate, pkg validate
  Commands:  (pipeline entry points — compose and sequence targets)
  release    Test, then pack and validate

  Targets:  (atomic steps — can also be run directly)
  clean    Clean known output locations
  build    Run build  (depends on: clean)
  test     Run build then tests  (depends on: build)

  Namespaces:
  pkg

The package depends on Proc.Fs, so open Proc.Fs gives you exec { } without another reference.


Targets: target [deps] { ... }

target [Clean] { ... } declares a target that runs after Clean. The block is plain F#: exec { } calls, if, match, loops, use. It is deferred, so nothing in it runs until the target does. -s / --single-target skips the dependencies.

A failing exec { } fails the target, and RunAsync returns 1. To inspect a command's result instead, bind it:

| Doctor -> target [] {
      let helpOutput = exec {
          binary "dotnet"
          arguments "--help"
          output
      }
      if helpOutput.ConsoleOut |> Seq.exists (fun l -> l.Line.Contains "workload") then
          printfn "workloads are available" }

Commands: command { ... }

| Release -> command {
      requires [PristineCheck; Test { Filter = None }]   // gates, skipped under -s
      composes [Pkg Generate; Pkg Validate] }            // the work, always runs

executes (...) adds a body that runs after every composed target. Like a target block, it is deferred:

| Publish -> command {
      requires [Release]
      composes [CreateGithubRelease]
      executes (
          let head = exec {
              binary "git"
              arguments ["rev-parse"; "HEAD"]
              output
          }
          printfn "published %s" (head.ConsoleOut |> Seq.head).Line) }

Descriptions from doc comments

A target's description is the /// doc comment on its DU case. The comment is read from the XML documentation file, so the build project needs <GenerateDocumentationFile>true</GenerateDocumentationFile>. A .fsx script produces no documentation file. In a script, pass a description with Make.target (below), or the target shows its name.

Namespaces via nested DUs

A case whose payload is another DU becomes a CLI namespace: Pkg of PkgTarget gives pkg generate and pkg validate, and pkg --help lists them. Two sub-DUs may share case names, such as Validate; qualify the ambiguous one in Bind (| Schema SchemaTarget.Validate ->).

Per-target typed arguments

A case with a record payload binds the record's fields from CLI flags at execution time: Test of TestOptions with { Filter: string option } takes --filter. A dependency can name any value of the case: Test { Filter = Some "Unit" } resolves to the test target.

Global options

let verbose = app.Flag("--verbose", short = "-v", desc = "Enable verbose output")
let token   = app.Option<string option>("--token", desc = "API token", defaultValue = None)

Read them in target blocks through .Value. Help shows each option's type, such as --config <string>, and --token <string?> for an option type. A value that does not parse, or a missing value, exits 2 before any target runs. Enum options bind by name, ignoring case.

The explicit functions

target and command are shorthand for these, which also take a description and give the body an FsContext:

Make.target  (deps: 'T list) (desc: string) (body: FsContext -> unit)
Make.command (requires: 'T list) (composes: 'T list)
Make.composer (requires: 'T list) (composes: 'T list) (body: FsContext -> unit)   // like command { executes (...) }
failBuild "message"                                                               // abort with exit code 1

Installation

<PackageReference Include="Nullean.Make.Fs" Version="0.23.0" />

Requires .NET 11. The package pulls in Nullean.Make (the underlying C# engine) and Proc.Fs transitively.

Product Compatible and additional computed target framework versions.
.NET net11.0 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.30.0 347 11/12/2026
0.29.0 241 11/12/2026
0.28.0 461 11/12/2026
0.27.2 170 11/11/2026
0.27.1 168 11/11/2026
0.25.0 666 11/11/2026
0.24.0 50 11/11/2026
0.23.0 47 11/11/2026