-
Notifications
You must be signed in to change notification settings - Fork 0
Paths And Filesystem
AbsolutePath is Tamp's typed path replacement for stringly-typed string paths inside build scripts. It carries enough filesystem surface to express the common operations (existence, read/write, copy/move/delete, globbing, hashing) without dropping to System.IO. The shape mirrors NUKE's idiom so adopters migrating from NUKE find muscle memory works.
This page is the canonical reference for the type. New surface lands in Tamp.Core release notes; this page tracks the current state.
At a glance
- Combine paths with
/:RootDirectory / "src" / "Foo.csproj".using Tamp;is all you need — every method below is onAbsolutePathitself.- Mutating methods return
this(or the destination) for fluent chaining.- Temp paths — use
TampBuild.Scratch(...)for auto-cleanup at end of build;AbsolutePath.CreateTempDirectory()for manual lifecycle.
var p = AbsolutePath.Create(rawPath); // Relative resolved vs CWD; normalised.
var q = AbsolutePath.Create("/abs/path"); // Already absolute.
var r = TampBuild.RootDirectory / "src"; // Combine operator; right-hand may be relative or absolute.
string s = p; // Implicit AbsolutePath → string for interop.
// (No implicit string → AbsolutePath: relative-vs-absolute disambiguation must be explicit.)AbsolutePath is a sealed record; equality is value-based on the underlying string. The constructor is private — go through Create (or the / operator on another AbsolutePath).
| Member | Returns | Example for /repo/src/Foo.csproj
|
|---|---|---|
Value |
string |
/repo/src/Foo.csproj |
Parent |
AbsolutePath? |
/repo/src |
Name |
string |
Foo.csproj |
NameWithoutExtension |
string |
Foo |
Extension |
string |
.csproj |
| Method | Returns true when |
|---|---|
FileExists() |
path resolves to an existing file |
DirectoryExists() |
path resolves to an existing directory |
Exists() |
either of the above |
path.EnsureDirectoryExists(); // mkdir -p; returns this for chaining
path.CreateDirectory(); // NUKE-style alias for EnsureDirectoryExists()
path.EnsureParentDirectoryExists(); // mkdir -p on Parent; returns this
path.DeleteFile(); // No-op if missing
path.DeleteDirectory(recursive: true);
path.Delete(); // Polymorphic — file OR directory, no-op if neither
path.Touch(); // Create empty file or update mtime; parent dirs createdAll deletes are idempotent (no exception when the target doesn't exist).
string text = path.ReadAllText();
string[] lines = path.ReadAllLines();
byte[] bytes = path.ReadAllBytes();
path.WriteAllText("content"); // Parent dirs auto-created
path.WriteAllLines(new[] { "a", "b" });
path.WriteAllBytes(new byte[] { 1, 2 });
path.AppendAllText("more content\n"); // Create-if-missing// CopyTo treats the argument as the FULL destination path.
src.CopyTo(dst, overwrite: true); // file → file, or dir → dir (recursive)
// CopyToDirectory treats the argument as a DIRECTORY; filename is preserved.
src.CopyToDirectory(stagingDir); // dst becomes stagingDir / src.Name
src.CopyToDirectory(stagingDir, overwrite: false); // Throws IOException if collision
// MoveTo: same shape as CopyTo, but moves (file or dir).
oldLoc.MoveTo(newLoc, overwrite: true);CopyTo is recursive for directories and ensures the destination's parent exists. CopyToDirectory is the helper you want when looping over files into a staging folder — it preserves names so you don't have to compose dst / src.Name by hand.
string hex = path.Sha256(); // SHA-256 of file contents (lowercase hex)
string hex2 = AbsolutePath.Sha256Of("text"); // SHA-256 of a string (static)
long bytes = path.SizeBytes(); // Throws FileNotFoundException if not a fileSha256 is useful for declaring input-hash gates on idempotent targets, and for SBOM provenance (paired with Tamp.Syft/Tamp.Grype).
foreach (var f in path.EnumerateFiles()) // Top-level files
foreach (var d in path.EnumerateDirectories()) // Top-level subdirs
// Glob — uses Microsoft.Extensions.FileSystemGlobbing under the hood.
var sources = RootDirectory.GlobFiles("src/**/*.cs", "src/**/*.fs");
var binDirs = RootDirectory.GlobDirectories("**/bin", "**/obj");Both glob methods dedup overlapping pattern hits and return empty collections (not null) when the directory doesn't exist.
For ad-hoc temp paths whose lifetime you manage yourself:
var tempRoot = AbsolutePath.GetTempDirectoryRoot(); // /tmp or %TEMP% as AbsolutePath; non-creating
var tmpDir = AbsolutePath.CreateTempDirectory(); // /tmp/tamp-<guid>/ (created on disk)
var tmpDir2 = AbsolutePath.CreateTempDirectory("msix-staging"); // /tmp/msix-staging-<guid>/
var tmpFile = AbsolutePath.CreateTempFile(".pfx"); // /tmp/tamp-<guid>.pfx (empty file, on disk)These do NOT auto-clean — caller is responsible. Use them for short-lived scratch in helpers where build-scoped lifetime doesn't fit. For build-script use, prefer TampBuild.Scratch(...) (below) which deletes its allocations at end of build automatically.
TampBuild exposes a protected Scratch(...) helper for temp directories with managed lifetime:
class Build : TampBuild
{
public static int Main(string[] args) => Execute<Build>(args);
Target StageMsix => _ => _
.Executes(() =>
{
var staging = Scratch("msix-staging"); // /tmp/msix-staging-<guid>/
Msix.SetAppxManifestVersion(staging / "AppxManifest.xml", Version);
// ... build the MSIX layout under `staging` ...
// Auto-deleted at end of Execute<Build>, success OR failure.
});
}Cleanup runs on every exit path, including InvalidOperationException from the framework itself. If a flaky build is filling /tmp you have a different problem; the framework will clean up after itself.
Preserving scratch for post-mortem: set TAMP_KEEP_SCRATCH=1 (or true, or any non-empty non-0/false value) in the environment. The dir tracking list is retained, but the cleanup pass becomes a no-op so adopters can cd /tmp && ls to inspect intermediate artifacts.
TAMP_KEEP_SCRATCH=1 dotnet tamp StageMsix
ls /tmp/msix-staging-*When to pick which:
| Use | Recommended |
|---|---|
| Per-target temp dir, auto-cleaned at end of build | Scratch(...) |
| Long-lived scratch shared across multiple builds |
TampBuild.TemporaryDirectory (<repo>/.tamp/temp) |
| Helper / utility outside a TampBuild instance | AbsolutePath.CreateTempDirectory(...) |
| Single temp file in a helper | AbsolutePath.CreateTempFile(...) |
Quick index of every method on AbsolutePath:
Static factories
AbsolutePath.Create(string)AbsolutePath.GetTempDirectoryRoot()AbsolutePath.CreateTempDirectory(string? namePrefix = null)AbsolutePath.CreateTempFile(string? extension = null)AbsolutePath.Sha256Of(string)
Components
-
Value,Parent,Name,NameWithoutExtension,Extension
Existence
-
Exists(),FileExists(),DirectoryExists()
Creation / deletion
-
EnsureDirectoryExists(),CreateDirectory()(alias),EnsureParentDirectoryExists(),Touch() -
DeleteFile(),DeleteDirectory(bool recursive = true),Delete()
Read
-
ReadAllText(),ReadAllLines(),ReadAllBytes()
Write
-
WriteAllText(string),WriteAllLines(IEnumerable<string>),WriteAllBytes(byte[]),AppendAllText(string)
Copy / move
CopyTo(AbsolutePath dst, bool overwrite = false)CopyToDirectory(AbsolutePath dstDir, bool overwrite = true)MoveTo(AbsolutePath dst, bool overwrite = false)
Hashing / size
-
Sha256(),SizeBytes()
Enumeration / globbing
-
EnumerateFiles(),EnumerateDirectories() -
GlobFiles(params string[] patterns),GlobDirectories(params string[] patterns)
-
No streams. Need
Stream? Drop toFile.OpenRead(path.Value). Streams are lifecycle-coupled tousing/Disposeand don't compose with fluent chaining without losing clarity. - No async overloads. Build scripts are mostly sync; if a target genuinely needs async IO it should call System.IO directly.
- No file watchers, ACLs, junctions, symlinks, hard links. Not in a build framework's hot path.
-
No
IFileSystemabstraction for testability. Adopters mock at the target level (test that the target's plan/side-effect is correct), not at the per-file-call level. - No CommandPlan integration for FS mutation. Tamp's CommandPlan represents child-process invocations. Filesystem mutation doesn't naturally fit that model; forcing it would break the abstraction for theoretical dry-run-visibility benefits. If you need a paper trail of FS mutations, emit them via the build's logger.
- Build Script Authoring — the broader build-script idiom.
- Migrating From NUKE — most of this surface is a 1:1 port.
-
Pitfalls — the
[FromPath("cargo")] readonly Tool Cargoshadowing trap and similar adopter snags.
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