Jig 0.2.1
dotnet add package Jig --version 0.2.1
NuGet\Install-Package Jig -Version 0.2.1
<PackageReference Include="Jig" Version="0.2.1" />
<PackageVersion Include="Jig" Version="0.2.1" />
<PackageReference Include="Jig" />
paket add Jig --version 0.2.1
#r "nuget: Jig, 0.2.1"
#:package Jig@0.2.1
#addin nuget:?package=Jig&version=0.2.1
#tool nuget:?package=Jig&version=0.2.1
An extensibility-first, low ceremony, C# task-based build automation system. Originally inspired by NUKE Build.
Getting Started
Ensure your project is within a git repository:
git init
Create a new console application:
dotnet new console -n build
Add the package:
dotnet add package Jig
dotnet add package Jig.Shell
dotnet add package Jig.Serilog
<PackageReference Include="Jig" Version="X.Y.Z"/>
<PackageReference Include="Jig.Shell" Version="X.Y.Z"/>
<PackageReference Include="Jig.Serilog" Version="X.Y.Z"/>
Add Targets:
using System.CommandLine;
using Jig.Targets;
namespace _build.Targets;
public class Targets : IBuildTargets
{
public const string SolutionPath = "Jig.sln";
BuildOption<string> Verbosity { get; } = new("minimal", description: "Verbosity for dotnet tasks" );
private ITarget Build => field ??= new Target(description: "Builds the solution")
.Executes($"dotnet build {SolutionPath} --verbosity {Verbosity}");
private ITarget Test => field ??= new Target(description: "Tests the solution")
.After(() => Build)
.Executes($"dotnet test {SolutionPath} --verbosity {Verbosity}");
private ITarget MergeCheck => field ??= new Target(description: "Runs required merge checks")
.DependsOn(Build, Test);
}
Program.cs:
await new Build(workingDirectory: "src")
.AddShell()
.AddSerilog()
.AddTargetsFromEntryAssembly()
.ExecuteAsync(args);
Then run! No further setup required:
dotnet run --project build/build.csproj -- {args}
Features Summary
- Fluent, flexible and low ceremony target definition and execution engine
- First class dependency injection support
- Straightforward calling of CLI tools with
Shellextension - Dry run capability
- Fully inspectable target DAG, powered by QuikGraph
- Result passing between targets
- High extensibility
- Maintainable low footprint (< 3000 cloc)
Core Concepts
Jig core contains the bare minimum code required to construct and execute a target graph. The core is then extended to provide additional functionality.
Build Construction
Jig builds are created through the Build class, which is a thin wrapper around a
Microsoft.Extensions.DependencyInjection DI container which is used for initialization of services
required by the build:
await new Build(workingDirectory: "src")
.AddShell() // Adds depdendencies required for shell execution
.AddSerilog() // Adds a serilog backed logging implemenation
.AddTargetsFromEntryAssembly() // Add target classes implemented
... // Add other serivices and extensions
.ExecuteAsync(args); // Run the build
A single ServiceProvider is initialized and used during the build. A scope is created for each target when
executed.
Build Initialization
When the build is executed, the following initialization actions are taken:
- Environment variables are loaded from any .env file found in the directory path to the build executable
- The process working directory is changed to the parent directory containing .git
- An instance of
BuildOptionsimplementingIOptionProvideris registered as a singleton. Build options specifies the following options which are supplied to the build execution:- Targets: Targets to run
- Exclude: Targets to exclude
- Skipped: Targets to skip
- BuildConcurrency: The concurrency to use for target execution
- An instance of
IBuildContextis registered as a singleton, which holds information about the build, including theTargetGraph.
Targets
Build targets are executable units of functionality that can be invoked, and can express dependencies and execution
ordering relative to each other. Targets are collected by the build from any service implementing ITargetProvider and
used to build a TargetGraph instance, which contain Directed Acyclic Graphs modeling the targets dependencies and
execution ordering. The build will check for cycles in execution ordering and throw if they exist. Targets are executed
in the order that they are invoked, and then according to the resulting graph topology.
Excluded targets are not included as options in the build when constructing the final build graph. As such their relationships with other targets are also ignored.
Skipped targets are included in the build as normal if invoked, but their executions are not run.
If the BuildConcurrency is set to Sequential, targets are run one at a time. In the absence of
clear target ordering, order of execution is arbitrary. If it is Parallel, targets are run concurrently except when
there is an explicit execution order specified.
An example of the options available when declaring targets:
// Target classes support dependency injection
public class MyTargets(IMySingleton singleton) : ITargetProvider
{
// Targets are best specified as lazy loaded properties
public ITarget One => field ??= new Target(description: "Some target")
.Executes((IMyScoped scoped) =>
{
// This action will be run when the target is run, supports dependency injection
});
public ITarget Two => field ??= new Target(description: "Some other action")
.DependsOn(() => One) // causes the specified target to be triggered by this target is, and run before
.Executes(async () =>
{
// Executions can be asynchronous
});
public ITarget Three => field ??= new Target()
.After(() => One) // ensures this target is run after the specified target
.Before(() => Two) // ensures this target is run before the specified target
.Executes(async (IMyScoped scoped) =>
{
// Do something asynchronously
});
.Executes(() =>
{
// A target can have multiple executions that are always run consecutively
});
public ITarget Four => field ??= new Target()
.DependentFor(() => Three) // this target is triggered by and run before the specified target
.Executes(() =>
{
// Tasks can return data, which becomes available to subsequent target executions
return new [] {"Task Result Data"};
});
public ITarget Five => field ??= new Target()
.DependsOn(() => Four)
.ProceedAfterFailure() // The build should continue to run if this target fails
.Executes((ITargetLogger logger, string[] results) =>
{
logger.LogInformationFormat(results.First());
throw new Exception(); // Targets only fail if they throw an exception
});
}
Target providers can be added to the build like so:
await new Build(workingDirectory: "src")
.AddTargets<MyTargets>()
.ExecuteAsync(args);
When target providers are registered, they can be resolved via dependency injection, and referred to by other targets providers like so:
public class OtherTargets(MyTargets targets) : ITargetProvider
{
public ITarget Six => field ??= new Target()
.DependsOn(() => targets.One);
}
Options
Jig provides expanded support for custom CLI and Environment variable options using System.CommandLine.
BuildOption represents a variable that is usable in the pipeline that can be set through the command line or
through environment variables, with a default value in case neither is specified. BuildOption names are inferred
from the their property name, but can also be constructed manually. BuildOptions can be marked a sensitive, and it's
expected that extensions never leak those values.
BuildOption instances can be specified as properties of classes implementing IOptionsProvider
(or ITargetProvider) like so:
public class MyOptions : IOptionsProvider
{
// Settable as --verbosity on the CLI or VERBOSITY as an environment variable
public BuildOption<string> SolutionPath => field ??= new("solution.sln", description: "Path to the solution");
}
// Also supported in ITargetProvider implementations
public class MyTargets(MyOptions options) : ITargetProvider
{
BuildOption<string> Verbosity => field ??= new("minimal", description: "Some build option");
private ITarget Build => field ??= new Target(description: "Builds the solution")
.Executes($"dotnet build {options.SolutionPath} --verbosity {Verbosity}");
}
Lifetime Event Handlers
Services registered as the following interfaces are resolved by the build and executed at different stages in the build execution:
IBuildInitializedHandler: When the build has been initialised, before targets have been executed
ITargetStartedHandler: Before a target is executed by the build
ITargetCompletedHandler: After a target is executed by the build
IBuildCompletedHandler: When the build has completed, after all targets have run
Build Execution
Once the build has been constructed and initialised and all targets, options and handlers have been collected, each invoked target and other triggered targets (not including excluded targets) are executed in the order expressed, and according to the specified BuildConcurrency. If target ordering is not expressed, execution ordering is not guaranteed, but will be deterministic.
If a target throws an exception, the build Status is set to Failed. By default, downstream targets are aborted
when a target fails. This behaviour can be controlled using the target'sDownstreamFailureMode and
UpstreamFailureMode options.
Target execution builder methods also add string representations about what they run where feasible. The dry run
extension allows for these to be logged instead of targets being run when the --dry-run option is specified.
Extensions
| Extension | Description |
|---|---|
| Jig.Shell | Extensions executing cli tools and shell commands |
| Jig.Serilog | Serilog logging implemenetation |
| Jig.Polly | Extensions wrapping target executions in polly execution policies |
| Jig.DesktopNotifications | Extensions enabling desktop notifications on build events |
| Jig.UserInput | Extensions allowing user input during builds |
| Jig.Apt | Extensions for interacting with apt packages |
Miscellaneous
Where's the CLI wrappers?
CLIs are broad and varied in their form and function, and are prone to change. While there is some advantage to
wrapping these in strongly defined code as some other build systems attempt to do, the amount of code required to
properly capture this can quickly get out of hand. Jig was built to be flexible and low ceremony, and Jig.Shell
reflects this philosophy by providing first class support for CLI command construction and execution that can be
copied straight to/from a script or YAML file, while refraining from trying to cater to any specific CLI tool.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- DotNetEnv (>= 3.1.1)
- Humanizer.Core (>= 3.0.1)
- JetBrains.Annotations (>= 2025.2.4)
- Microsoft.Extensions.DependencyInjection (>= 10.0.1)
- Microsoft.Extensions.Hosting (>= 10.0.1)
- morelinq (>= 4.4.0)
- NeoSmart.AsyncLock (>= 3.2.1)
- QuikGraph (>= 2.5.0)
- System.CommandLine (>= 2.0.2)
NuGet packages (5)
Showing the top 5 NuGet packages that depend on Jig:
| Package | Downloads |
|---|---|
|
Jig.Shell
Jig extension adding shell execution capability for targets. |
|
|
Jig.UserInput
Jig extension adding user input capability. |
|
|
Jig.Polly
Jig extension implementating adding Polly resilience for targets. |
|
|
Jig.Apt
Jig extension adding apt package utilities for targets |
|
|
Jig.Serilog
Jig extension adding serilog logging implementations |
GitHub repositories
This package is not used by any popular GitHub repositories.