Nullean.Make 0.28.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Nullean.Make --version 0.28.0
                    
NuGet\Install-Package Nullean.Make -Version 0.28.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" Version="0.28.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Nullean.Make" Version="0.28.0" />
                    
Directory.Packages.props
<PackageReference Include="Nullean.Make" />
                    
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 --version 0.28.0
                    
#r "nuget: Nullean.Make, 0.28.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@0.28.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&version=0.28.0
                    
Install as a Cake Addin
#tool nuget:?package=Nullean.Make&version=0.28.0
                    
Install as a Cake Tool

Nullean.Make

Nullean.Argh

Typed build targets for .NET, with Argh-quality help. Your targets are the cases of a C# union; each one runs commands, and dependencies run first.

NuGet

Install

dotnet add package Nullean.Make

Or, in a file-based app such as build.cs, #:package Nullean.Make@<version> as below. It requires .NET 11.

Quick start

#!/usr/bin/env -S dotnet run --
#:package Nullean.Make@0.24.0
#:property GenerateDocumentationFile=true
#:property NoWarn=CS1591
using Nullean.Make;
using static Nullean.Make.Shell;

var app = new MakeApp<Target>("build");
var config = app.Option<string>("--config", "Build configuration", defaultValue: "Release");

app.Bind(t => t.Value switch
{
    Clean     => app.Target(() => Exec("dotnet clean")),
    Build     => app.Target([new Clean()], () => Exec($"dotnet build -c {config.Value}")),
    Test test => app.Target([new Build()], () =>
    {
        if (test.Filter is { } filter) Exec($"dotnet test -c {config.Value} --filter:{filter}");
        else Exec($"dotnet test -c {config.Value}");
    }),
    Pack      => app.Target(() => Exec("dotnet pack -c Release -o build/output")),
    Release   => app.Command([new Test()], [new Pack()]),
    _ => throw new InvalidOperationException($"unmapped target {t.Value}")
});

return await app.RunAsync(args);

/// <summary>Clean known output locations</summary>
record Clean;
/// <summary>Run build</summary>
record Build;
/// <summary>Run build then tests</summary>
record Test(string? Filter = null);
/// <summary>Pack the NuGet packages</summary>
record Pack;
/// <summary>Test, then pack</summary>
record Release;

union Target(Clean, Build, Test, Pack, Release);
./build.cs --help
./build.cs test --filter Unit --config Debug
./build.cs release
./build.cs release -s        # skip the tests, run only pack
  Commands:  (pipeline entry points — compose and sequence targets)
  release    Test, then pack

  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)
  pack     Pack the NuGet packages

Targets

app.Target(body) and app.Target(dependsOn, body) take a plain lambda, so a body can branch, loop and read the case's fields. A Func<Task> body works too. Dependencies run first, once each, and -s / --single-target skips them.

A case record's primary-constructor parameters become the target's flags: Test(string? Filter = null) takes --filter.

Bind runs twice per case: once when the app reads the graph, with each parameter at its default (default when it has none, such as Deploy(string Environment)), and once when the target runs, with the parsed flags. Keep side effects inside the target body.

The package's source generator finds each new MakeApp<TUnion>(...) and records the union's cases at build time, so the app reads no union by reflection. A MakeApp<T> created only through a type parameter has no case table and exits 1 with an error.

Commands

app.Command(requires, composes) sequences other targets. requires are gates, such as tests, and -s skips them. composes always run, in order. An optional third argument is a body that runs last.

Running commands

using static Nullean.Make.Shell; brings in Exec:

  • Exec("dotnet build -c Release") splits on whitespace. Double quotes group an argument that contains spaces ("My App.csproj"), and \" is a literal quote.
  • Exec("dotnet", "build", "-c", config) passes each argument as is. Use it for arguments built in code.

A command shares the console. A non-zero exit code fails the target, and RunAsync returns 1.

Descriptions

A target's description is the summary of its case type's XML doc comment, such as Run build above. The comment is read from the XML documentation file, so set GenerateDocumentationFile=true: #:property GenerateDocumentationFile=true in a file-based app, or <GenerateDocumentationFile> in a project. .Description("...") overrides it.

Global options

app.Flag(...) and app.Option<T>(...) declare options that every target can read through .Value. A value that does not parse, or a missing value, exits 2 before any target runs.

Namespaces

A case that is itself a union becomes a namespace: union Db(Migrate, Seed) inside union Target(..., Db) gives db migrate and db seed, and db --help lists them. Case type names must be unique across the whole union, including nested unions.

mise, make and just

__wrappers sync writes one include file per wrapper from the targets, so mise run test, make test and just test call the script without one hand-written task per target:

./build.cs __wrappers sync
wrote .make/mise-tasks.toml (5 targets)
wrote .make/make.mk (5 targets)
wrote .make/make.just (5 targets)

Pull each one in once:

File Line to add Arguments
mise.toml [task_config] includes = [".make/mise-tasks.toml"] mise run test --filter Unit
Makefile -include .make/make.mk make test ARGS="--filter Unit"
justfile import? '.make/make.just' just test --filter Unit

A namespaced target is pkg:generate in mise, pkg/generate in make and pkg-generate in just. The wrappers only forward to the script, which still runs the dependencies, so a mise task has no depends. The mise tasks carry a usage spec, so mise completes each target's flags, and mise run test --help prints the script's own help.

  • --run "<command>" is what the wrappers call. A file-based app defaults to dotnet run build.cs --. A project needs it, e.g. --run "dotnet run --project build --". Later syncs reuse the one recorded in the files.
  • --only mise,just limits the files. A bare sync refreshes the files already in --out-dir (default .make).
  • --prefix make names the tasks make:test, make/test and make-test. A task in mise.toml silently wins over an included task of the same name, so use a prefix when mise.toml already has build or test tasks.
  • mise removes the first -- after the task name, so mise run test -- -- --x passes -- --x to the script.
  • __wrappers check exits 1 when a file is missing or out of date, for CI when the files are committed.
  • make.mk keeps your Makefile's default goal. In a justfile, the first recipe is the default, so put yours above the import?.

Or let mise regenerate them and gitignore .make/. sync prints this when it creates a file:

[hooks]
# On every `mise install`, so a fresh clone and CI get the files.
postinstall = "dotnet run build.cs -- __wrappers sync --quiet"

# In a shell with `mise activate`, on the next prompt after build.cs changes.
[[watch_files]]
patterns = ["build.cs"]
run = "dotnet run build.cs -- __wrappers sync --quiet"

The watch_files hook compiles the script, so the first prompt after an edit waits for that.

The docs have a page per wrapper: mise, make and just.

Class-based targets

MakeApp.Execute<TBuild>(args) discovers targets from a MakeBuild class instead: Target properties, [Flag] and [GlobalOption] properties, and nested Namespace classes. build.cs in this repository is a complete example.

For F#, see Nullean.Make.Fs.

Learn more

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 (1)

Showing the top 1 NuGet packages that depend on Nullean.Make:

Package Downloads
Nullean.Make.Fs

F# DU/match binding over Nullean.Make — exhaustive, typed, namespaced build targets.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.30.0 97 11/12/2026
0.29.0 148 11/12/2026
0.28.0 441 11/12/2026
0.27.2 164 11/11/2026
0.27.1 171 11/11/2026
0.25.0 664 11/11/2026
0.24.0 50 11/11/2026
0.23.0 46 11/11/2026
0.22.1 50 11/11/2026