Skip to content

Repository files navigation

Tamp.Sccache

Wrapper for mozilla/sccache — a shared compilation cache that drops in transparently via RUSTC_WRAPPER (and cc/c++ wrappers). Turns the cold-CI Rust-compile bottleneck into a warm-cache lookup. Pairs with Tamp.Cargo for the Rust toolchain side.

Package Status
Tamp.Sccache 0.1.0 (initial)

Why this exists

Cargo's incremental-compile only helps within a single working tree. On a fresh CI runner, every PR pays the cold-rustc tax — compile every transitive dep from scratch. For a non-trivial Rust app like DasBook (Tauri shell + axum service + sqlite-bundled), that's the dominant CI wall-clock cost.

sccache solves this by caching rustc outputs keyed by source hash + flags + dep-tree fingerprint. With an S3 / Azure Blob / Redis backend, every runner shares the same cache: PR #501's compilation feeds PR #502's. Cold-cache compile drops from minutes to seconds for the parts that didn't actually change.

Tamp.Sccache makes this a typed step in the Tamp build graph:

  • Storage backends are typedSetLocal(...), SetS3(...), SetAzureBlob(...), SetGcs(...), SetRedis(...), SetMemcached(...), SetGitHubActionsCache(...). Mutually exclusive (validated at ToCommandPlan time, not after the slow tool launches).
  • Secrets are maskedSecret-typed Azure connection strings and Redis passwords flow through CommandPlan.Secrets so they don't appear in Tamp's printed process trace.
  • Daemon lifecycle is typedSccache.Start(...), Sccache.Stop(...), Sccache.Stats(...), Sccache.ZeroStats(...) for the daemon-control verbs.
  • Tamp.Cargo integration is one lineSccache.RustcWrapperEnv() returns the env-var dictionary that wires sccache as Cargo's rustc wrapper.

Install

dotnet add package Tamp.Sccache

Multi-targets net8 / net9 / net10. Requires Tamp.Core1.6.0 (which made Secret.Reveal() public + ships the TAMP004 analyzer — see Tamp.Core 1.6.0 changelog). No per-satellite InternalsVisibleTo entry is needed; the analyzer recognizes the *Settings / *SettingsBase class shape that Tamp.Sccache uses for the call sites that reveal Azure / Redis credentials.

Tool installation

sccache is a single Go-style binary distributed via:

Pin the version in CI for reproducibility.

Quick start — wiring sccache into a Cargo build

using Tamp;
using Tamp.Cargo;
using Tamp.Sccache;

class Build : TampBuild
{
    public static int Main(string[] args) => Execute<Build>(args);

    [FromPath("cargo")] readonly Tool CargoBin = null!;
    [FromPath("sccache")] readonly Tool SccacheTool = null!;

    [Secret] readonly Secret AzureConn = null!;

    AbsolutePath ServiceCrate => RootDirectory / "dasbook-service";

    // ── Start the daemon with an Azure Blob backend (CI runners share the cache) ──
    Target StartCache => _ => _
        .Executes(() => Sccache.Start(SccacheTool, s => s
            .WithStorage(b => b.SetAzureBlob("sccache", AzureConn, "ci-rust/"))
            .SetIdleTimeoutSeconds(0)));   // never auto-stop in CI

    // Reset counters so Stats reflects only this build's hits/misses
    Target ResetCacheStats => _ => _
        .DependsOn(nameof(StartCache))
        .Executes(() => Sccache.ZeroStats(SccacheTool));

    // ── Build the Rust service with sccache wired as RUSTC_WRAPPER ──
    Target BuildService => _ => _
        .DependsOn(nameof(ResetCacheStats))
        .Executes(() =>
        {
            var env = Sccache.RustcWrapperEnv();    // { "RUSTC_WRAPPER": "sccache" }
            return Cargo.Build(CargoBin, s =>
            {
                s.SetWorkingDirectory(ServiceCrate)
                 .SetRelease()
                 .SetTarget("x86_64-pc-windows-msvc")
                 .SetLocked();
                foreach (var (k, v) in env) s.SetEnvironmentVariable(k, v);
            });
        });

    // Print stats at the end so CI logs show cache hit rate
    Target ShowCacheStats => _ => _
        .DependsOn(nameof(BuildService))
        .Executes(() => Sccache.Stats(SccacheTool, s => s.SetAdvanced()));
}

Run dotnet tamp BuildService once → cold cache, all crates compiled. Run again → 95%+ hit rate on the same machine. Run on a different runner with the same Azure Blob backend → still 95%+. The cache crosses runners, machines, and CI workflows.

Backend matrix

Backend Setter Env vars set Best for
Local disk SetLocal(dir, cacheSize?) SCCACHE_DIR, SCCACHE_CACHE_SIZE Single-developer machine, single-runner CI
Amazon S3 SetS3(bucket, region, keyPrefix?) + optional SetS3Endpoint(...) / SetS3NoCredentials() SCCACHE_BUCKET, SCCACHE_REGION, SCCACHE_S3_KEY_PREFIX, SCCACHE_ENDPOINT, SCCACHE_S3_NO_CREDENTIALS Multi-runner CI on AWS; also works with MinIO / R2 via endpoint override
Azure Blob SetAzureBlob(container, Secret connString, keyPrefix?) SCCACHE_AZURE_BLOB_CONTAINER, SCCACHE_AZURE_CONNECTION_STRING, SCCACHE_AZURE_KEY_PREFIX Azure-hosted CI (App Service / ADO pipelines)
Google Cloud Storage SetGcs(bucket, keyPath?, rwMode?, keyPrefix?) SCCACHE_GCS_BUCKET, SCCACHE_GCS_KEY_PATH, SCCACHE_GCS_RW_MODE, SCCACHE_GCS_KEY_PREFIX GCP-hosted CI
Redis SetRedis(url, Secret? password, db?, ttlSeconds?) SCCACHE_REDIS, SCCACHE_REDIS_PASSWORD, SCCACHE_REDIS_DB, SCCACHE_REDIS_EXPIRATION Self-hosted CI with a Redis pool
Memcached SetMemcached(endpoint, ttlSeconds?) SCCACHE_MEMCACHED, SCCACHE_MEMCACHED_EXPIRATION Self-hosted CI with memcached
GitHub Actions cache SetGitHubActionsCache(version?) SCCACHE_GHA_ENABLED=true, SCCACHE_GHA_VERSION GitHub Actions — uses the actions cache as backend, no separate infra

Backends are mutually exclusive — set one, validation enforces this at ToCommandPlan time:

// throws InvalidOperationException — two backends set
Sccache.Start(tool, s => s.WithStorage(b => b
    .SetLocal("/tmp/sccache")
    .SetS3("bucket", "us-east-1")));

Verb surface

Tamp method sccache cli Notes
Sccache.Start(...) sccache --start-server Backend storage knobs flow through env. SetLogLevel("debug"), SetServerPort(int), SetIdleTimeoutSeconds(int).
Sccache.Stop(...) sccache --stop-server Stop daemon.
Sccache.Stats(...) sccache --show-stats / --show-adv-stats SetAdvanced() for per-cacheable-op breakdown; SetJson() for machine-readable.
Sccache.ZeroStats(...) sccache --zero-stats Reset counters. Useful at build start so end-of-build stats reflect just this run.
Sccache.Version(...) sccache --version Diagnostic.
Sccache.Raw(...) sccache <anything> Escape hatch — including sccache <compiler> <args> for direct compiler-wrapper invocation.

Sccache.RustcWrapperEnv() — the load-bearing helper

public static IReadOnlyDictionary<string, string> RustcWrapperEnv(string? sccacheExecutable = null);

Returns { "RUSTC_WRAPPER": "sccache" }. Merge into any Tamp.Cargo settings:

foreach (var (k, v) in Sccache.RustcWrapperEnv())
    cargoSettings.SetEnvironmentVariable(k, v);

Pass an absolute path to pin to a specific sccache binary (avoids PATH lookup at exec time):

var env = Sccache.RustcWrapperEnv("/usr/local/bin/sccache");

DasBook wishlist #3 — how this fits

DasBook flagged: "Reproducibility / cache hits across Rust + Node (currently the npm ci and Rust deps caches are the slowest parts of the CI run)."

Two distinct caches map to different solutions:

Cache Tool Tamp coverage
Rust compile output (rustc artifacts) sccache Tamp.Sccache (this package)
Rust deps (~/.cargo/registry, ~/.cargo/git) actions/cache + Cargo.lock Standard CI YAML — Tamp doesn't replace
npm deps (~/.npm, node_modules/) actions/cache + package-lock.json Standard CI YAML — Tamp doesn't replace
Vite build output (.vite/, dist/) actions/cache or Turbo Tamp.Turbo.V2 handles the Turbo path

The sccache satellite covers the highest-value gap: the rustc-compile bottleneck. Dep restoration and npm cache stay at the CI YAML layer (one actions/cache block per ecosystem).

Secrets handling

Backend credentials that flow through env vars use Tamp.Core.Secret:

  • SetAzureBlob(container, Secret connectionString, ...)
  • SetRedis(url, Secret? password, ...)

These are Reveal()'d into the env dictionary at exec time, and the Secret instance is listed in CommandPlan.Secrets so Tamp's process trace masks the value in printed output.

Tamp.Sccache requires Tamp.Core ≥ 1.6.0 (which made Reveal() public; no InternalsVisibleTo dance needed anymore).

Sibling packages

  • Tamp.Cargo — Rust toolchain. The package sccache wraps for.
  • Tamp.Turbo.V2 — for the JS-side build-output cache (Turbo / Turborepo).

Releasing

Releases follow the Tamp dogfood pattern.

Settings authoring style

Examples above use the fluent Set*-chain shape. Every wrapper verb also accepts a new XxxSettings { ... } object-init form — both produce identical CommandPlans. The fluent shape stays canonical in docs and the tamp init template; opt into object-init scaffolding via tamp init --settings-style=init.

See Build Script Authoring → Two authoring styles on the wiki for the side-by-side comparison.

License

MIT. See LICENSE.

About

Wrapper for mozilla/sccache — shared compile cache for Rust (RUSTC_WRAPPER) with S3/Azure/GCS/Redis/GHA-cache backends. Pairs with Tamp.Cargo.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages