Skip to content

Generated-build storage model

Incan compiles generated Rust through Cargo. Cargo dependency and intermediate artifacts are usually the largest part of that pipeline, so Incan shares those rebuildable files across compatible projects while keeping source and durable outputs project-local.

Storage ownership

Category Default owner and location Lifecycle
Generated Rust source and manifests Project target/incan/, target/incan_tests/, and target/incan_lock/ by default; a caller-selected generated output directory when supplied Recreated from Incan source; remains caller/project-owned
Generated Cargo dependency and intermediate output $INCAN_HOME/cache/generated-cargo/v1/<identity>/target/ Shared by compatible commands; idle domains are LRU-pruned toward a 20 GiB soft limit, and an idle domain whose measured output exceeds its 20 GiB default safety bound has only its rebuildable Cargo target discarded
Published executable The generated project below project target/incan/ by default, or below a caller-selected generated output directory Copied atomically from Cargo output after a successful build; never pruned by incan cache
Test harness and rust-inspect workspace source/metadata Project target/incan_tests/ and target/incan_lock/rust_inspect/ Project-local generated inputs; their Cargo output uses the applicable shared or explicit target
Compiled SDK providers The selected SDK provider store below INCAN_HOME, or an explicit provider-store override Content-addressed SDK lifecycle; not removed by generated-cache pruning
Vocab companion metadata <generated-target>/incan-vocab-cache, unless INCAN_VOCAB_COMPANION_CACHE_DIR overrides it Pruned with a managed generated target; an explicit override is caller-owned and not pruned by incan cache
Durable Incan library Project target/lib/*.incnlib User-facing build artifact; never removed by generated-cache pruning
Incan compiler repository build Repository target/ Ordinary Cargo development state, outside the installed compiler's generated-cache manager
Legacy Rust CompilationPlan executor output Caller-selected plan output directory Caller-owned compatibility API; contained below that output and not used by CLI build, run, test, lock, or library paths

The cache reports recursive logical file lengths. These are useful for deterministic category comparisons but are not the same as allocated or uniquely reclaimable filesystem blocks on APFS, sparse, compressed, cloned, or hardlinked storage.

Compatibility and bounded growth

A compatibility identity includes the Incan version, selected Rust backend command and verbose host/version output, Rust/Cargo target, profile, and flag environment selectors, the Cargo executable and verbose version, Cargo profile, normalized dependency lock, Cargo feature selection, and Cargo arguments that can affect compiled artifacts. Execution-only offline, lock-enforcement, timing, verbosity, and color policy does not split otherwise compatible domains. Cargo fingerprints its remaining compiler and configuration inputs inside the domain. Cargo passthrough cannot set --target-dir or load a file-valued --config, because either could move artifacts outside the directory protected and reported by Incan; use the explicit generated-target override instead. Generated binary target names use a path-independent digest of generated root source plus package, dependency, provider, lock, edition, and feature inputs, so an unchanged logical fixture does not leave a new top-level binary and incremental tree merely because a temporary project directory changed. A per-root exclusive lock spans Cargo execution and atomic project-local publication; the executable path itself comes from Cargo's JSON artifact message, including target-triple layouts.

Incan takes a shared activity lease before Cargo can use a domain. Cleanup takes an exclusive lease and skips active domains. Automatic cleanup runs before acquiring the requested domain and again after a completed lease makes new usage idle. The total limit is therefore soft only while domains are active; each completion brings the measurable idle set back toward the limit. When the final Cargo lease ends, Incan measures the domain; if it exceeds the per-domain safety bound, Incan discards that domain's rebuildable target/ tree while preserving its identity metadata. The same recovery runs before reusing an interrupted idle domain, so repeated crashes cannot retain unmeasured growth indefinitely. Set INCAN_GENERATED_CACHE_MAX_ENTRY_BYTES to change the bound, or use an explicit generated-target override when an external system owns a larger target lifecycle. The lease ends after Cargo has published the project-local executable and before incan run starts user code.

If a build process exits before releasing its lease, the next acquisition of that idle identity treats its unmeasured size as unknown, measures the partial domain before taking the new shared lease, and discards an oversized rebuildable target. Acquiring a different identity also includes the interrupted idle domain in ordinary LRU pruning.

Repository CI ownership

Incan's repository CI deliberately supplies INCAN_GENERATED_CARGO_TARGET_DIR, so the workflow—not the managed INCAN_HOME cache—owns generated Cargo cleanup and persistence there. SDK provider publication preserves that caller-owned target for rebuildable Cargo intermediates while keeping immutable provider output transaction-local; ordinary callers without an override retain the private staging target. One archive job per platform and toolchain compiles the test suite with inner CARGO_BUILD_JOBS=2, then transfers Nextest's portable test archive instead of relying on a runner's installed Rust inventory to reproduce a Cargo target-cache key. The source cache retains only Cargo registry and Git inputs; every consumer runs cargo fetch --locked before entering offline nested builds. The hosted test jobs disable test-profile debug information to avoid storing rebuildable symbols on constrained runners, and they do not create a separate per-runner sccache: the measured local caches consumed 332–543 MiB while producing only 0–3% hits. A separate job uses the exact archived compiler binary to restore or prepare the immutable SDK provider store, then uploads that store for the ordinary sliced test shards. The intentionally cold provider-publication proofs consume the same test archive but not the warmed provider store, run together under one-job Cargo children, and remain part of the stable Linux, stable macOS, and Rust 1.93 lanes. Shards report their archive and provider footprint, retain nextest's bounded nested-cargo group, and never rebuild the repository merely to list their partition. Local and installed compiler use continues to use the managed cache unless the caller makes the same explicit target choice.

Reproducing the audit

Use an isolated INCAN_HOME and the same compiler binary for every sample. For offline warm-cache evidence, seed Cargo's registry first, then set CARGO_NET_OFFLINE=true for both runs.

  1. Remove only the task-local project targets and task-local INCAN_HOME.
  2. Run incan build, incan run, incan test, and incan build --lib once and record /usr/bin/time -p.
  3. Record incan cache inspect --format json and du -sk for each project-local category in the table above.
  4. Repeat the unchanged command, then repeat it from a compatible clean project or worktree.
  5. Compare compatibility-identity count and category growth. Do not use one platform-specific absolute byte ceiling as a regression assertion.

v0.5 cache evidence

On 2026-07-20, a macOS APFS canary used two separate projects with the same serde_json dependency graph, an isolated INCAN_HOME, and CARGO_NET_OFFLINE=true:

Scenario First project Compatible second project Result after both
incan build (release) 67.74 s 0.43 s One 58,829,076-logical-byte release domain; both final binaries project-local
incan run (debug) 6.94 s 0.44 s One additional 113,103,114-logical-byte debug domain, separated by profile

After both scenarios, project-local generated source plus published output was 436 KiB per project; the managed cache reported 171,932,190 logical bytes across the two intentional profile domains. The second runs compiled only their root crate, demonstrating offline dependency reuse without an explicit target override.

The CI-focused audit also found that path-derived binary names made an unchanged 11-fixture rerun grow one shared target from 241,280 KiB to 285,924 KiB (+44,644 KiB) while the provider store stayed at 6,140 KiB. The path-independent generated source identity above fixes that demonstrated root-artifact growth mechanism. Regression checks assert stable identities rather than those machine-specific byte totals.

The completed audit recorded one observational matrix on 2026-07-22 under the uncontended /private/tmp/incan-v05-heavy-gate.lease. The small fixture used serde_json; the downstream-shaped fixture retained InQL's DataFusion 53, DataFusion-Substrait 53, Substrait 0.63, and Prost 0.14 dependency graph while using minimal current Incan source, because the copied older InQL source itself no longer typechecked on current main. Cargo sources were seeded first, and every measured command ran with CARGO_NET_OFFLINE=true and CARGO_BUILD_JOBS=2.

Package and command Cold Unchanged warm Managed logical bytes after the command pair
Small build 295.41 s 8.14 s 166,001,563 across canonical-lock preheat, release, and rust-inspect domains
Small run 12.78 s 8.09 s 114,226,194 across debug and rust-inspect domains
Small test 15.19 s 10.65 s 114,908,627 across test and rust-inspect domains
Small build --lib 300.59 s 9.07 s 51,703,901 across release and rust-inspect domains
DataFusion-shaped build 564.69 s 11.30 s 1,161,099,759 in the release domain; shared-home totals are below
DataFusion-shaped run 151.15 s 10.21 s 4,097,273,097 in the debug domain
DataFusion-shaped test 141.05 s 13.95 s 3,794,476,320 in the test domain
DataFusion-shaped build --lib 658.15 s 77.87 s 1,187,893,210 across release and rust-inspect domains

The first small build and library samples each crossed a compiler-binary identity change during the audit and therefore include a fresh SDK-provider preparation; that is why their cold time is much larger than small run or test. One provider identity occupied about 6 MiB. The task-local store contained two 6 MiB identities after the compiler fix, which is expected content-addressed invalidation rather than generated-Cargo duplication.

After build, run, and test shared one heavy-package home, incan cache inspect --format json reported 9,052,850,583 logical bytes: 4,097,273,097 debug, 3,794,476,320 test, 1,161,099,759 release, and a metadata-only rust-inspect domain. The project-owned target was about 394 MiB, dominated by the final generated executable; generated test source was 88 KiB, lock/preheat source and metadata 604 KiB, and the durable .incnlib output 136 KiB. The fixture exercised no vocab surface, so it created no vocab companion cache. These numbers show that Cargo intermediates are the dominant cost and that the default 20 GiB managed soft limit bounds them independently of project source and durable artifacts.

The release-test topology was also measured with the same warmed binaries, shared generated target, provider store, inner CARGO_BUILD_JOBS=2, and 12 representative nested-Cargo CLI regressions. These machine-specific timings are evidence for the selected topology, not a portable benchmark claim. Outer nextest limits of 12, 6, 4, and 2 completed in 18.45 s, 25.10 s, 39.03 s, and 62.21 s respectively. The provisional limit of 2 was therefore rejected as a 3.4x warmed-throughput regression. The limit of 6 halves the previous outer fan-out while remaining 2.5x faster than 2 on the cache-identical sample. Repository CI transfers the compiled test inventory explicitly, supplies ordinary shards with a fully prepared immutable SDK provider store, and schedules the three tests that must prove an empty provider store in their own acceptance job rather than hiding that unavoidable construction time inside otherwise ordinary partitions.

Remaining Cargo-owned cost

The shared domain still contains Cargo's dependency objects, build-script output, fingerprints, and incremental state. Different profiles, compiler/toolchain contracts, target selectors, features, or dependency locks intentionally create separate compatibility domains. incan cache inspect reports logical file lengths without creating a cache-management lock; incan cache prune --dry-run previews the resulting logical usage, and exact idle identities can be removed with incan cache prune --identity <SHA256>.