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
11 changes: 11 additions & 0 deletions .cargo/mutants.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,14 @@
# scratch-data noise. Scope, baseline, and time limits stay explicit per lane.
cap_lints = true
gitignore = true

# `run_config_editor` is the `kache config` TUI entry point: it enables raw
# mode, takes over the alternate screen, and runs an event loop until the user
# quits. Nothing can drive that from a test — CI has no TTY at all, and a test
# that did get one would seize the developer's terminal and then block — so a
# replaced body is unobservable and the mutant survives by construction rather
# than by a coverage gap. Kept as narrow as possible: the parts of the editor
# that hold real logic (`initial_editor_state`, `build_fields`,
# `fields_to_file_config`, `do_save_to`) stay in scope and are mutation-tested
# normally.
exclude_re = ["replace run_config_editor"]
55 changes: 54 additions & 1 deletion docs/getting-started/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ The config file is TOML. You can edit it directly or use the TUI editor:
kache config
```

The TUI editor surfaces the common fields with their current values, marks which ones are coming from env vars (those are read-only and the cursor skips them), and lets you toggle or edit the rest interactively. Navigate with the arrows or `j`/`k`, jump between sections with Tab/Shift-Tab, Enter edits a field, Space toggles a boolean, `s` (or Ctrl-S) saves, and `q`/Esc quits with an unsaved-changes prompt. Some advanced sections — `[cache.planner]`, `[cc]`, `[paths]`, and `cache.path_only_env_vars` — have no form fields and are preserved verbatim on save.
The TUI editor surfaces the common fields with their current values, marks which ones are coming from env vars (those are read-only and the cursor skips them), and lets you toggle or edit the rest interactively. Navigate with the arrows or `j`/`k`, jump between sections with Tab/Shift-Tab, Enter edits a field, Space toggles a boolean, `s` (or Ctrl-S) saves, and `q`/Esc quits with an unsaved-changes prompt. Some advanced sections — `[cache.planner]`, `[cc]`, `[paths]`, `cache.path_only_env_vars`, and `cache.key_env_vars` — have no form fields and are preserved verbatim on save.

Config file priority:

Expand Down Expand Up @@ -103,6 +103,7 @@ for AWS SDK-specific cases that need attention.
| — (file-only) | `cache.ignore_env` | `false` | Make the config file authoritative by ignoring `KACHE_*` env overrides for file-backed settings (see [Pinning config against env](#pinning-config-against-env)) |
| `KACHE_FALLBACK` | `cache.fallback` | — | Secondary compiler-wrapper to hand passed-through compiles to; kache runs `<fallback> <compiler> <args>` when it declines to cache. `off`/`none`/empty disables |
| `KACHE_PATH_ONLY_ENV_VARS` | `cache.path_only_env_vars` | `[]` | Extra env vars (besides `OUT_DIR`) whose values only locate an `include!`'d file, so kache normalizes their absolute path in the cache key. Env value is comma/whitespace-separated and replaces the file list (see [Path-only env vars](#path-only-env-vars)) |
| `KACHE_KEY_ENV_VARS` | `cache.key_env_vars` | `[]` | Env vars to fold into every cache key, for values a proc macro reads at expansion time that the compiler never reports. Exact names or a trailing-`*` prefix glob. Env value is comma/whitespace-separated and replaces the file list (see [Env vars that steer expansion](#env-vars-that-steer-expansion)) |
| `KACHE_PLANNER_ENDPOINT` | `cache.planner.endpoint` | — | Prefetch-planner service URL; setting it enables the planner client. Empty/whitespace is treated as unset |
| `KACHE_PLANNER_TIMEOUT_MS` | `cache.planner.timeout_ms` | `750` | Planner request timeout in milliseconds |
| `KACHE_PLANNER_TOKEN` | `cache.planner.token` | — | Bearer credential sent with planner requests |
Expand Down Expand Up @@ -266,6 +267,58 @@ export KACHE_PATH_ONLY_ENV_VARS="BUILDCONFIG_RS MOZ_TOPOBJDIR"

This is an advanced opt-in: only list vars whose value is purely a path locator. An empty list means only `OUT_DIR` is normalized.

## Env vars that steer expansion

kache keys the compile-time environment a build *bakes in* — the `env!()` and `option_env!()` values rustc reports in dep-info. A **proc macro** that calls `std::env::var` while expanding is a different story: rustc has no way to report it, so the command line, the source hashes, and the `--extern` set are byte-identical whether or not the var is set, while the emitted artifact is not.

That is a real failure mode, not a hypothetical one. In [#635](https://github.com/kunobi-ninja/kache/issues/635) a two-phase build ran the same crate graph twice — once with `BOLTFFI_BINDING_EXPANSION=1`, where the `#[export]` macro strips itself from dependency crates and emits no trait impls, and once without. The dependency crate's rustc invocation was identical in both phases, so both compiles keyed the same and the normal build restored the stripped 264 KiB artifact instead of building the 620 KiB one. It surfaced as a missing trait impl at compile time; the same mechanism can just as easily produce a wrong binary that builds cleanly.

`cache.key_env_vars` declares the vars that steer expansion so the two modes get distinct entries:

```toml title=".kache.toml"
[cache]
key_env_vars = ["BOLTFFI_*"]
```

Or via the environment (comma- or whitespace-separated; replaces the file list entirely):

```sh
export KACHE_KEY_ENV_VARS="BOLTFFI_BINDING_EXPANSION,BOLTFFI_SURFACE"
```

Semantics:

- **Exact names, or a trailing `*` prefix glob.** `BOLTFFI_*` selects every var starting with `BOLTFFI_`; `APP_MODE` selects only that name. Matching is ASCII case-insensitive, so a Windows environment behaves like a Unix one. A `*` anywhere but the end is a literal character — `A*B` matches a variable actually named `A*B` — and kache warns when it sees one, since that is almost never what someone meant.
- **Turning it on re-keys the crate.** The declared patterns are folded, not just the matched values — otherwise the build that leaves the vars unset would land back on the poisoned entry that made you reach for this setting. Expect one cold rebuild after adding or editing the list.
- **Only vars actually set are folded**, as `NAME=VALUE` pairs. Set-to-empty and unset are distinct, matching what `std::env::var` reports.
- **Declaration spelling doesn't matter.** The list is upper-cased, sorted and deduplicated before it is folded, so two teammates writing `BOLTFFI_*` and `boltffi_*` in either order still share a cache.
- **Values are folded exactly**, as their raw OS bytes. See the warning below.
- **Off by default.** An empty list has no effect; keys are byte-identical to not setting it.
- **Union-only.** A misdeclared pattern can cost a cache miss, never restore a wrong artifact.

<Callout type="warn">
Declared values are **not** path-normalized, unlike most paths kache keys. A
macro is free to paste a variable's value straight into the code it emits, so
two checkout paths that would collapse to the same `<BASE_DIR>` sentinel can
still produce different artifacts — normalizing them would reintroduce the
wrong-hit this setting exists to prevent. The consequence is that a declared
variable holding a machine-local path (`BOLTFFI_ROOT=/home/alice/proj`) makes
that crate's key machine-specific and stops it sharing across hosts. Prefer
declaring the switch a macro actually branches on
(`BOLTFFI_BINDING_EXPANSION`) over a broad `BOLTFFI_*` glob that sweeps in
path variables too.
</Callout>

Keep the list to vars that genuinely change what a macro emits. Naming something that varies per build — a timestamp, a job id, `PWD` — gives every compile its own key and disables caching for the whole workspace. `"*"` is a valid pattern and folds the entire environment, but for the same reason it is close to a cache-off switch, and it makes every credential in the process contribute to your cache keys.

Values are never written to logs or events — only variable names appear at `KACHE_LOG=trace` — but they do influence the resulting cache key, which is stored and (with a remote) transmitted. Treat a declared secret as hashed, not as hidden.

<Callout type="info">
`proc_macro::tracked_env` would let a macro register these reads with the compiler and make this configuration unnecessary. It is still unstable, so for now the vars have to be declared. Other compiler caches share the limitation.
</Callout>

If you would rather not cache the affected crate at all, [`cache.exclude`](#excluding-sources) takes a source-path glob and bypasses kache entirely for those compiles.

## Extra cc allowlist flags

kache caches a C/C++ compile only when it recognizes every flag on the command line. Its cc flag classifier is an **allow-list**: each flag is one kache has reasoned about and knows how to key. Anything unrecognized is refused and the compile passes through uncached — the safe default, since a flag kache doesn't model could change the object file without changing the key.
Expand Down
3 changes: 2 additions & 1 deletion docs/how-it-works/cache-key.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ What goes in (change any of these and the crate re-keys):
- **What you're compiling** — crate name, types, and edition; and every source file the crate reads. kache finds them with a `--emit=dep-info` pre-pass, so modules, `include!()` targets, and build-script output are all covered, and hashes each by content.
- **How you're compiling it** — target triple, feature/`cfg` flags, codegen (`-C`) options, `RUSTFLAGS`, and the active `--emit` set (so a `cargo check` entry is never served to a `cargo build`).
- **What it depends on** — each `--extern` rlib/rmeta, hashed by content, so a dependency change propagates to everything downstream.
- **Compile-time environment** — the `env!()` / `option_env!()` values a build bakes in.
- **Compile-time environment** — the `env!()` / `option_env!()` values a build bakes in. These are the reads rustc reports; an env var a **proc macro** reads with `std::env::var` while expanding is invisible to the compiler and has to be declared (see [`key_env_vars`](/getting-started/configuration#env-vars-that-steer-expansion)).
- **Native Linux libc** — for OS-loaded outputs (`bin`, `dylib`, `cdylib`, and proc-macros), the GNU libc or musl version. Portable rlibs and cross-target outputs deliberately exclude this host-only signal.

What's deliberately left out (machine-local, doesn't affect output): the incremental-compilation directory, the linker path (its *identity* is captured via `--version` instead), and absolute build/checkout paths — those are normalized to stable sentinels so the key is the same regardless of where you built. See [Cross-machine portability](#cross-machine-portability).
Expand All @@ -37,6 +37,7 @@ A few opt-in knobs, all no-ops by default:
- **`KACHE_KEY_SALT` / `cache.key_salt`** — fold an opaque string into every key to force a cold cache when the toolchain changes in a way kache cannot observe (a custom/cross-target libc or sysroot change, a same-version distro libc patch, a Nix closure rebuild, or a hidden linker change). See [Configuration](/getting-started/configuration#cache-key-salt).
- **`kache.toml` extra inputs** — declare files the compiler reads but never reports (sqlx's `.sqlx/`, `migrations/`, `include!`'d data) so editing them re-keys the crate. See [Configuration](/getting-started/configuration#extra-cache-key-inputs).
- **`path_only_env_vars`** — extra env vars whose value is only a path locator, so kache normalizes their path out of the key for cross-machine hits. See [Configuration](/getting-started/configuration#path-only-env-vars).
- **`key_env_vars`** — env vars a proc macro reads at expansion time. rustc reports only `env!`/`option_env!` reads, so a macro branching on `std::env::var` produces different code from an identical command line; declaring the var separates the two. See [Configuration](/getting-started/configuration#env-vars-that-steer-expansion).

## Cross-machine portability

Expand Down
Loading
Loading