Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
973 changes: 312 additions & 661 deletions Cargo.lock

Large diffs are not rendered by default.

51 changes: 17 additions & 34 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "kache"
version = "0.11.0"
edition = "2024"
description = "Zero-copy, content-addressed build cache for Rust, C/C++ and more. No copies, no wasted disk — just hardlinks locally and S3 for sharing."
description = "Zero-copy, content-addressed build cache for Rust, C/C++ and more, with S3 and shared-filesystem remotes."
license = "Apache-2.0"
repository = "https://github.com/kunobi-ninja/kache"
homepage = "https://kunobi.ninja/docs/kache"
Expand All @@ -20,35 +20,22 @@ resolver = "3"

[dependencies]
blake3 = "1"
# Already transitive via aws-sdk-s3; declared for RemoteBackend's body type.
bytes = "1"
# TLS provider: `ring`, NOT aws-lc-rs. We avoid `aws-lc-sys` entirely
# because its cmake/NASM C build hangs when cross-compiling to
# `aarch64-pc-windows-msvc` under cargo-xwin; `ring` ships prebuilt asm
# for that target and cross-compiles cleanly.
#
# `aws-lc-sys` enters ONLY as the rustls crypto provider — `sigv4a` uses
# pure-Rust `p256`/`crypto-bigint`, not aws-lc. To force `ring`, every
# rustls consumer must opt out of aws-lc AND avoid re-enabling it via
# additive features:
# - aws-sdk-s3: drop `default-https-client` (it hard-wires
# `aws-smithy-http-client/rustls-aws-lc` on aws-smithy-runtime).
# We inject our own ring-backed Smithy client in `src/remote.rs`.
# - aws-config: `default-features = false` for the same reason — its
# default pulls `default-https-client`. We inject the client there too.
# - aws-smithy-http-client: `rustls-ring` (modern rustls 0.23 + ring).
# NOT `legacy-rustls-ring`, which pins vulnerable rustls 0.21
# (RUSTSEC-2026-0098/0099/0104).
# - reqwest: `rustls-no-provider` + a process-default ring CryptoProvider
# installed at startup (see `src/main.rs`).
aws-sdk-s3 = { version = "1", default-features = false, features = ["sigv4a", "http-1x", "rt-tokio"] }
aws-config = { version = "1", default-features = false, features = ["rt-tokio", "credentials-process", "sso"] }
aws-smithy-http-client = { version = "1", features = ["rustls-ring"] }
# Already in-tree via aws-sdk-s3; direct dep only to name the concrete HTTP
# response type in SdkError so a raw GET 404 is detectable (#485 Phase 0).
aws-smithy-runtime-api = "1"
# Direct dep so we can install ring as the process-wide rustls crypto
# provider (reqwest uses `rustls-no-provider`, so one must be installed).
# OpenDAL 0.58 publishes its modules as separate crates. Depending on only the
# core, S3, filesystem, retry, and reqwest transport modules keeps Kache's
# supported backend set explicit and avoids compiling unused services.
opendal = { package = "opendal-core", version = "=0.58.0", default-features = false, features = ["executors-tokio"] }
opendal-http-transport-reqwest = { version = "=0.58.0", default-features = false, features = ["rustls-no-provider"] }
opendal-layer-retry = "=0.58.0"
opendal-service-fs = "=0.58.0"
opendal-service-s3 = "=0.58.0"
# OpenDAL exposes a custom credential chain but not a profile-name shortcut.
# These published reqsign APIs preserve Kache's explicit profile selection
# (including SSO and credential_process) without a git-only patch.
reqsign-aws-v4 = { version = "=3.0.2", default-features = false }
reqsign-command-execute-tokio = { version = "=3.0.2", default-features = false }
reqsign-core = { version = "=3.1.0", default-features = false }
# Direct dep so OpenDAL/reqwest can use ring without reintroducing aws-lc.
rustls = { version = "0.23", default-features = false, features = ["ring"] }
kache-core = { version = "0.11.0", path = "crates/kache-core", default-features = false, features = ["planning"] }
async-trait = "0.1"
Expand Down Expand Up @@ -107,10 +94,6 @@ windows-sys = { version = "0.61", features = [
assert_cmd = "2"
predicates = "3"
serde_json = "1"
# Wire-level mock S3 server (WireMockServer) for testing the remote/S3 code
# paths without network — see src/remote.rs and src/remote_layout.rs tests.
# `test-util` is dev-only; the release build never enables it.
aws-smithy-http-client = { version = "1", features = ["test-util", "wire-mock"] }
bytes = "1"

# `{ archive-suffix }` resolves from each target's pkg-fmt (".tar.gz" for
Expand Down Expand Up @@ -140,7 +123,7 @@ extended-description = """\
kache is a zero-copy, content-addressed build cache for Rust, C/C++ and more. \
It avoids file \
copies and wasted disk by hardlinking artifacts locally and sharing them \
via S3-compatible object storage."""
via S3-compatible object storage or a shared filesystem."""
section = "utils"
priority = "optional"
# CRITICAL: empty depends. kache is built as a static musl binary, so it has
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
[![MSRV](https://img.shields.io/badge/MSRV-1.95-blue.svg)](Cargo.toml)

Zero-copy, content-addressed build cache for Rust and C/C++ object compiles. No copies, no wasted disk — reflinks where the filesystem supports them, hardlinks or copies otherwise, plus S3 for Rust artifact sharing.
Zero-copy, content-addressed build cache for Rust and C/C++ object compiles. No copies, no wasted disk — reflinks where the filesystem supports them, hardlinks or copies otherwise, plus S3 or shared-filesystem remotes for Rust artifact sharing.

A drop-in `RUSTC_WRAPPER` for Rust and a `cc` / `c++` compiler wrapper for C/C++ object compiles. Cache keys are blake3 hashes of normalized compiler inputs; cache hits restore zero-copy — a reflink (copy-on-write clone) where the filesystem supports it (APFS, btrfs, XFS-with-reflink), and a hardlink or copy otherwise — and identical blobs are stored once and shared. Optional S3 sync (AWS, Ceph, MinIO, R2) shares Rust artifacts across machines.
A drop-in `RUSTC_WRAPPER` for Rust and a `cc` / `c++` compiler wrapper for C/C++ object compiles. Cache keys are blake3 hashes of normalized compiler inputs; cache hits restore zero-copy — a reflink (copy-on-write clone) where the filesystem supports it (APFS, btrfs, XFS-with-reflink), and a hardlink or copy otherwise — and identical blobs are stored once and shared. Optional S3 (AWS, Ceph, MinIO, R2) or shared-filesystem sync shares Rust artifacts across machines.

Local Rust caching, local C/C++ object caching, and direct S3 sync are working today. C/C++ artifacts are local-only for now; unsupported compiler shapes pass through to the real compiler.
Local Rust caching, local C/C++ object caching, and direct S3/shared-filesystem sync are working today. C/C++ artifacts are local-only for now; unsupported compiler shapes pass through to the real compiler.

**PREVIEW:** a remote planner that prefetches from workspace manifests, dependency history, and build intent — warming the right artifacts before rustc asks for them. The daemon already calls a planner when `KACHE_PLANNER_ENDPOINT` is set; the hosted service is still preview.

Expand Down Expand Up @@ -167,7 +167,7 @@ set "CC=kache clang-cl"

For **gcc/clang** the key is portable across machines and worktrees (paths are normalized via `-ffile-prefix-map`). Set `KACHE_BASE_DIR` to a user-declared root that gets stripped from the key, covering paths the derived source/build roots miss — e.g. objdir-built units whose `__FILE__` points above the derived root — for cross-checkout hits the automatic roots can't reach. If path normalization ever miscaches, `KACHE_CC_PATH_NORMALIZE=0` is the escape hatch: keys become path-literal (no cross-machine sharing, zero normalization risk). For **clang-cl** the key is already **machine-local**: clang-cl ignores `-ffile-prefix-map`, so kache keeps literal paths in the key — correct local hits, but no cross-machine sharing yet. clang-cl **debug** compiles (`/Z7`, `/Zi`, `-Z7`, or a `-g` form) are cached too: clang-cl embeds debug info straight into the `.obj` (there's no separate compile-time PDB), so kache folds the CodeView path inputs that object embeds — source path, output name, and compilation dir — into the same machine-local key.

**Not cached yet** (these pass through): link / whole-program steps, multi-source and multi-arch invocations, response-file (`@file`) invocations, precompiled headers, modules, coverage, and split-DWARF; for clang-cl also `-bigobj` and `-showIncludes`. C/C++ caching is also **local-only** for now — the Rust path's S3 sharing doesn't extend to `cc` artifacts yet.
**Not cached yet** (these pass through): link / whole-program steps, multi-source and multi-arch invocations, response-file (`@file`) invocations, precompiled headers, modules, coverage, and split-DWARF; for clang-cl also `-bigobj` and `-showIncludes`. C/C++ caching is also **local-only** for now — the Rust path's remote sharing doesn't extend to `cc` artifacts yet.

C/C++ caching is live but still conservative by design: when in doubt, kache misses rather than serve a wrong artifact. See [C/C++ caching](docs/getting-started/c-cpp.mdx) for setup, status, and limitations. Scope and remaining work are tracked in [#49](https://github.com/kunobi-ninja/kache/issues/49).

Expand Down Expand Up @@ -270,7 +270,7 @@ See [`scenarios/README.md`](scenarios/README.md) for the scenario format.
| `kache list [<crate>] [--sort name\|size\|hits\|age]` | List cached entries, or show details for a specific crate |
| `kache why-miss <crate>` | Explain why a specific crate missed the cache |
| `kache report [--format text\|json\|markdown\|github\|perfetto\|chrome-trace] [--since <dur>] [--top <n>] [--output <path>]` | Generate a detailed hit/dup/miss build report or Perfetto/Chrome trace (`--top` defaults to 10) |
| `kache sync [--manifest-path <path>] [--pull] [--push] [--all] [--workspace] [--dry-run]` | Synchronize local cache with S3 remote (pull + push); `--manifest-path` points the Cargo.lock pull filter at a non-cwd manifest; `--workspace` scopes the pull to workspace members only (one LIST per member) |
| `kache sync [--manifest-path <path>] [--pull] [--push] [--all] [--workspace] [--dry-run]` | Synchronize the local cache with its configured remote (pull + push); `--manifest-path` controls the push-side workspace filter; `--workspace` scopes the pull to workspace members only (one LIST per member) |
| `kache save-manifest [--manifest-key <key>] [--namespace <ns>]` | Save a build manifest for future prefetch warming; `--manifest-key` overrides the default host-target-triple key |
| `kache gc [--max-age <dur>]` | Garbage collect — LRU eviction or age-based cleanup |
| `kache purge [--crate-name <name>]` | Wipe entire cache or entries for a specific crate |
Expand All @@ -291,14 +291,14 @@ Durations use days, hours, or bare hours: `7d`, `24h`, `1h`, `48`.

## Remote cache and configuration

`kache sync` can pull from and push to S3-compatible storage directly, without the daemon. Pulls are filtered by the current workspace's `Cargo.lock` by default. See [Sync](docs/remote-cache/sync.mdx) for the full command behavior and S3 layout.
`kache sync` can pull from and push to S3-compatible storage or a shared filesystem directly, without the daemon. Pulls are filtered by the current workspace's `Cargo.lock` by default. See [Sync](docs/remote-cache/sync.mdx) for the full command behavior and remote layout.

Configuration is available through `kache config`, environment variables, or config files. Environment variables win over config files, and project-local `.kache.toml` files are supported. See [Configuration](docs/getting-started/configuration.mdx) for the full reference.

## Architecture

- **Wrapper**: `RUSTC_WRAPPER` intercepts rustc calls, computes blake3 cache keys, restores hits zero-copy (reflink where supported, else hardlink or copy)
- **Daemon**: Background process handles async S3 uploads, remote checks, and prefetch. Auto-restarts when binary is updated
- **Daemon**: Background process handles async remote uploads, checks, and prefetch. Auto-restarts when binary is updated
- **Store**: content-addressed blobs under `{cache_dir}/store/blobs/<prefix>/<hash>`, indexed by a SQLite DB; cache hits reflink or hardlink those blobs into `target/`
- **Cache keys**: Deterministic blake3 hash of rustc version, crate name, source, dependencies, and normalized flags — portable across machines

Expand Down
2 changes: 1 addition & 1 deletion deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ license-files = [{ path = "LICENSE", hash = 0xbb1191d7 }]
unknown-registry = "deny"
unknown-git = "deny"
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
# First-party git dependencies (kache-service HA + auth building blocks).
# First-party dependencies.
allow-git = [
"https://github.com/kunobi-ninja/kunobi-auth.git",
"https://github.com/kunobi-ninja/kunobi-ha.git",
Expand Down
9 changes: 5 additions & 4 deletions docs/commands/reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ When invoked without arguments, kache prints `--help` and exits — it does not
| `kache gc [--max-age <dur>]` | Evict entries by LRU or age |
| `kache purge [--crate-name <name>]` | Wipe entire cache or entries for one crate |
| `kache clean [-n \| --dry-run] [-y \| --yes]` | Find and remove `target/` directories (interactive; `-n` previews, `-y` removes all non-interactively) |
| `kache sync [flags]` | Sync local cache with S3 remote |
| `kache sync [flags]` | Sync local cache with a configured remote |
| `kache save-manifest [--manifest-key <key>] [--namespace <ns>]` | Save a build manifest for future prefetch warming |
| `kache doctor [--fix [--purge-sccache]] [--verify] [--checksums] [--repair]` | Diagnose and fix setup issues; verify cache integrity |
| `kache config` | Open the TUI configuration editor |
Expand Down Expand Up @@ -256,15 +256,16 @@ On macOS, the scan skips TCC-protected locations (`/System`, `/Library`, `/priva
## `kache sync`

```sh
kache sync [--pull] [--push] [--all] [--dry-run] [--manifest-path <path>]
kache sync [--pull] [--push] [--all] [--workspace] [--dry-run] [--manifest-path <path>]
```

Synchronizes the local cache with the configured S3 remote. See [Sync](/docs/remote-cache/sync) for a full walkthrough.
Synchronizes the local cache with the configured S3 or filesystem remote. See
[Sync](/docs/remote-cache/sync) for a full walkthrough.

```sh
kache sync # pull missing + push new artifacts
kache sync --pull # download only, filtered to current workspace
kache sync --pull --all # download everything in the bucket
kache sync --pull --all # download everything in the remote
kache sync --push # upload only
kache sync --dry-run # preview transfers, make no changes
```
Expand Down
2 changes: 1 addition & 1 deletion docs/daemon/lifecycle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ A few behaviors worth knowing about:

- **Single instance**: a second `kache daemon run` exits cleanly (code 0) if another process holds the run lock or the socket is already live, so launchd/systemd `KeepAlive` won't loop. Stale socket files are removed on startup when nothing is listening.
- **Background log file**: auto-started daemons write stderr to `<socket>.log` next to the daemon socket (Linux `~/.cache/kache/daemon.log`, macOS `~/Library/Caches/kache/daemon.log`). It is reset to a `--- log rotated ---` marker once it grows past 2 MiB. `kache daemon log` tails this file.
- **Graceful drain**: on shutdown the daemon closes its upload queue and waits up to 30 s for in-flight S3 uploads to finish before aborting workers.
- **Graceful drain**: on shutdown the daemon closes its upload queue and waits up to 30 s for in-flight remote uploads to finish before aborting workers.

## Offline behavior

Expand Down
8 changes: 4 additions & 4 deletions docs/daemon/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ The daemon is a background process that handles everything that shouldn't block

## What the daemon handles

**Upload queue.** After a cache miss and successful compilation, the wrapper hands the daemon a reference to the freshly stored entry and returns immediately. The daemon reads it from the local store and uploads to S3 in the background. No build time is spent waiting for uploads.
**Upload queue.** After a cache miss and successful compilation, the wrapper hands the daemon a reference to the freshly stored entry and returns immediately. The daemon reads it from the local store and uploads to the configured remote in the background. No build time is spent waiting for uploads.

**Remote checks.** Before compiling a crate that missed locally, the wrapper asks the daemon to check S3 for the artifact. The daemon downloads it if found, then signals the wrapper to restore from the local store.
**Remote checks.** Before compiling a crate that missed locally, the wrapper asks the daemon to check the remote for the artifact. The daemon downloads it if found, then signals the wrapper to restore from the local store.

**Prefetch.** At the start of a new build session, the wrapper detects that a build is beginning (via a timestamped marker file), runs `cargo metadata` to get the full dependency graph, and sends the daemon a prefetch hint with all crate names in topological order. The daemon prefetches them from S3 before cargo even asks for them, turning remote hits into local hits for the rest of the build.
**Prefetch.** At the start of a new build session, the wrapper detects that a build is beginning (via a timestamped marker file), runs `cargo metadata` to get the full dependency graph, and sends the daemon a prefetch hint with all crate names in topological order. The daemon prefetches them from the remote before cargo even asks for them, turning remote hits into local hits for the rest of the build.

The prefetch hint fires once per build session (with a 5-minute cooldown) to avoid redundant work across sequential cargo commands in CI — `cargo check`, `cargo clippy`, `cargo test` may all run within the same session window.

Expand All @@ -29,7 +29,7 @@ If you're using kache locally with no remote configured, you don't need the daem

The daemon becomes necessary as soon as you configure a remote cache. Without it:

- New artifacts won't be uploaded to S3
- New artifacts won't be uploaded to the remote
- The cache won't be checked before a local miss triggers compilation
- Prefetch won't run

Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/c-cpp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ These shapes compile normally but are not cached yet:
- clang-cl `-bigobj` and `-showIncludes`
- flags kache has not classified

C/C++ artifacts are local-only today. S3 sync and remote sharing apply to Rust artifacts, not `cc` objects.
C/C++ artifacts are local-only today. Remote sync and sharing apply to Rust artifacts, not `cc` objects.

## Diagnosing Passthroughs

Expand Down
Loading