Skip to content

Paths And Filesystem

Scott Singleton edited this page May 13, 2026 · 1 revision

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 on AbsolutePath itself.
  • 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.

Construction

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).

Path components

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

Existence checks

Method Returns true when
FileExists() path resolves to an existing file
DirectoryExists() path resolves to an existing directory
Exists() either of the above

Creating and deleting

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 created

All deletes are idempotent (no exception when the target doesn't exist).

Reading and writing

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

Copy and move

// 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.

Hashing and sizing

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 file

Sha256 is useful for declaring input-hash gates on idempotent targets, and for SBOM provenance (paired with Tamp.Syft/Tamp.Grype).

Children and globbing

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.

OS temp-path factories

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.

Build-scoped scratch

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

Method reference

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)

Things AbsolutePath deliberately does NOT do

  • No streams. Need Stream? Drop to File.OpenRead(path.Value). Streams are lifecycle-coupled to using/Dispose and 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 IFileSystem abstraction 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.

See also

Clone this wiki locally