-
Notifications
You must be signed in to change notification settings - Fork 0
Tooling Primitives
The cross-cutting types every build script reaches for: paths, well-known directories, runtime preconditions.
Replaces stringly-typed path manipulation. Always normalised, always absolute.
var p = AbsolutePath.Create("./relative/or/absolute"); // resolved against CWD
AbsolutePath.Create(null); // throws ArgumentNullException
AbsolutePath.Create(""); // throws ArgumentExceptionThere's an implicit conversion to string (so any string-taking API works without ceremony). There's deliberately no implicit conversion from string — AbsolutePath.Create(...) is required at the boundary, because relative paths can change meaning silently when normalised.
The / operator combines path segments:
var artifacts = RootDirectory / "artifacts";
var nupkg = artifacts / $"MyApp.{Version}.nupkg";
var deepFile = RootDirectory / "src" / "Foo" / "bin" / "Debug" / "net10.0" / "Foo.dll";Right-hand side can be a relative segment (combined) or an absolute path (replaces, matching Path.Combine semantics).
var p = AbsolutePath.Create("/repo/src/foo.bar.txt");
p.Parent // /repo/src
p.Name // "foo.bar.txt"
p.NameWithoutExtension // "foo.bar"
p.Extension // ".txt"p.Exists() // file or directory
p.FileExists()
p.DirectoryExists()p.EnsureDirectoryExists() // mkdir -p
p.DeleteFile()
p.DeleteDirectory(recursive: true)
p.Delete() // polymorphic: deletes file or directory; no-op if missing
p.CopyTo(destination, overwrite: false) // recursive for dirs
p.MoveTo(destination, overwrite: false)p.ReadAllText()
p.ReadAllLines()
p.ReadAllBytes()
p.WriteAllText("hello") // creates parent dirs implicitly
p.WriteAllLines(new[] {"a","b"})
p.WriteAllBytes(bytes)p.Sha256() // hex of file contents
AbsolutePath.Sha256Of("inline") // hex of arbitrary string (UTF-8)The hash format is lowercase hex, 64 chars. Useful for input-hash declarations on idempotent targets:
Target Compile => _ => _
.Idempotent()
.InputHash(() => SourceDirectory.Sha256()) // pseudo: realistically use globbing + per-file hash
.Executes(...);p.EnumerateFiles() // top-level files
p.EnumerateDirectories() // top-level subdirs
// Recursive globbing — supports **, *, ?
p.GlobFiles("**/*.cs")
p.GlobFiles("src/**/*.cs", "tests/**/*.cs") // multiple patterns unioned + deduped
p.GlobDirectories("**/bin", "**/obj")Powered by Microsoft.Extensions.FileSystemGlobbing under the hood.
// Clean every bin/obj in the tree
RootDirectory
.GlobDirectories("**/bin", "**/obj")
.ForEach(d => d.Delete());
// Hash every csproj for cache invalidation
var hashes = RootDirectory
.GlobFiles("**/*.csproj")
.Select(f => f.Sha256());
var combined = AbsolutePath.Sha256Of(string.Join('|', hashes));
// Copy artifacts into a flat publish dir
var publish = RootDirectory / "publish";
publish.EnsureDirectoryExists();
SourceDirectory
.GlobFiles("**/bin/Release/net10.0/*.dll")
.ForEach(dll => dll.CopyTo(publish / dll.Name));Static properties on TampBuild (so any subclass and any target's lambda can use them).
The root of the consumer repository. Resolved by walking up from the build assembly's location, stopping at the first directory containing any of:
.git/-
.tamp/(a Tamp-specific marker; more on this below) *.slnx*.sln
Cached after first access. Throws if no marker is found above the assembly.
public class Build : TampBuild
{
AbsolutePath SourceDirectory => RootDirectory / "src";
AbsolutePath ArtifactsDirectory => RootDirectory / "artifacts";
}Tamp's per-build scratch space. Lives at RootDirectory / ".tamp" / "temp", created on first access. Used by [NuGetPackage] for tool caches, and available for any target that needs scratch:
Target ExtractDownload => _ => _.Executes(() =>
{
var tmp = TemporaryDirectory / "downloads";
tmp.EnsureDirectoryExists();
// … extract into tmp …
});The .tamp/ directory is also a marker for RootDirectory resolution — useful in repos that don't have a solution at the root. Just mkdir .tamp and Tamp will treat that as the root.
Runtime preconditions for build scripts. Throws InvalidOperationException with a clear message when a check fails. Named Verify rather than Assert to dodge the namespace collision with xUnit's Assert in your own test files.
using Tamp;
Verify.NotNull(Solution); // throws if null
Verify.NotNullOrEmpty(Configuration.ToString());
Verify.NotNullOrWhiteSpace(Environment);
Verify.True(Version.Major >= 1); // captured expression in failure message
Verify.False(string.IsNullOrEmpty(token));
Verify.NotEmpty(testProjects);
Verify.Single(testProjects); // throws unless exactly one element
Verify.Fail("unreachable"); // throw with custom message[CallerArgumentExpression] captures the source expression for default failure messages, so Verify.True(x > 10) fails with Expected true: x > 10, not just Expected true.
Inside target action bodies, when you've assumed something is true and want a clear message rather than a NullReferenceException if it isn't:
Target Deploy => _ => _
.Requires(() => Solution != null) // declarative precondition
.Executes(() =>
{
var manifest = Solution!.Path.Parent!.Parent;
Verify.NotNull(manifest); // belt-and-braces; defensive
Verify.True(manifest.DirectoryExists());
// …
});Requires is the better declarative form; Verify is for the cases that come up imperatively inside action bodies.
- Build Script Authoring — DSL that consumes these primitives
-
Parameter & Secret Injection —
[Solution],[GitRepository]populate the most common starting AbsolutePath values
Start here
Modules
- Module Catalog (canonical list, 50+ satellites)
- .NET toolchain
- Containers
- JS toolchain
- Supply-chain security
Analyzers
Tooling
Execution
CI integration
Editor integration
Migration
Reference
Project