cache-manager
Directory-based cache and artifact path management with discovered .cache roots, grouped cache paths, and optional eviction on directory initialization.
This crate was built to solve a recurring workspace problem we had before adopting it.
Previously, several crates wrote artifacts to different locations with inconsistent eviction policy management.
cache-managerprovides a single, consistent cache/artifact path layer across the workspace (and also works outside ofcargoenvironments).
Quick start
Add cache-manager to your project
Create a cache entry
use CacheRoot;
// Discover the workspace or crate root, anchor .cache there
let root = from_discovery.expect;
// Create (or resume) a group for artifacts
let group = root.group;
group.ensure_dir.expect;
// Create or refresh a cache entry (creates parent dirs automatically)
let entry = group.touch.expect;
println!;
Pass a path to another tool
Composing explicit paths without touch (hand the path to another tool):
use CacheRoot;
use fs;
let root = from_discovery.expect;
let group = root.group;
group.ensure_dir.expect;
let entry = group.entry_path;
create_dir_all
.expect;
write.expect;
println!;
Core capabilities
- Tool-agnostic: any tool or library that can write to the filesystem can use
cache-manageras a managed cache/artifact path layout layer. - Zero default runtime dependencies: the standard install uses only the Rust standard library (optional features do add additional dependencies).
- Built-in eviction policies: enforce cache limits by file age, file count, and total bytes, with deterministic oldest-first trimming.
- Predictable discovery + root control: discover
<workspace-or-crate-root>/.cacheautomatically or pin an explicit root withCacheRoot::from_root(...). - Composable cache layout API: create groups/subgroups and entry paths consistently across tools without custom path-joining logic.
- Artifact-friendly: suitable for build outputs, generated files, and intermediate data.
- Workspace-friendly: suitable for monorepos or multi-crate workspaces that need centralized cache/artifact management via a shared root (for example with
CacheRoot::from_root(...)).
Optional features
-
process-scoped-cache: addstempfileand enables process/thread scoped caches. -
os-cache-dir: addsdirectoriesand enables OS-native per-user cache roots. -
Open-source + commercial-friendly: dual-licensed under MIT or Apache-2.0.
Tested on macOS, Linux, and Windows.
Reference
Mental model: root -> groups -> entries
CacheRoot: project/workspace anchor path.CacheGroup: subdirectory under a root where a class of cache files lives.- Entries: files under a group (for example
v1/index.bin).
CacheRoot and CacheGroup are lightweight path objects. Constructing them does not create directories.
Filesystem effects
-
Core APIs (always available):
CacheRoot::from_root,CacheRoot::from_discovery,CacheRoot::cache_path,CacheRoot::groupCacheGroup::subgroup,CacheGroup::entry_pathCacheRoot::ensure_group,CacheGroup::ensure_dirCacheRoot::ensure_group_with_policy,CacheGroup::ensure_dir_with_policyCacheGroup::touch
-
Feature
os-cache-dir:CacheRoot::from_project_dirs
-
Feature
process-scoped-cache:CacheRoot::from_tempdirProcessScopedCacheGroup::new,ProcessScopedCacheGroup::from_groupProcessScopedCacheGroup::thread_group,ProcessScopedCacheGroup::ensure_thread_groupProcessScopedCacheGroup::thread_entry_path,ProcessScopedCacheGroup::touch_thread_entry
Note: eviction only runs when you pass a policy to the
*_with_policymethods.
Cache root discovery
Discover a cache root by searching parent directories for a Cargo workspace or crate root.
Note:
CacheRoot::from_discovery()?.cache_path(...)only computes a filesystem path — it does not create directories or files.
Behavior:
- Searches upward from the current working directory for a
Cargo.toml. - If a
Cargo.tomlcontaining[workspace]is found, uses<workspace-root>/.cache. - Otherwise uses the nearest
<crate-root>/.cache. - Falls back to
<cwd>/.cachewhen noCargo.tomlexists. - The discovered anchor is canonicalized when possible.
- If the
relative_pathargument is absolute, it is returned unchanged.
use CacheRoot;
use Path;
// Compute a path like <workspace-root>/.cache/tool/data.bin
let cache_path = from_discovery
.expect
.cache_path;
println!;
// Relative location under the discovered root:
assert!;
// The call only computes the path; it does not create files or directories
assert!;
// Absolute paths are returned unchanged:
let absolute = new;
let kept = from_discovery
.expect
.cache_path;
assert_eq!;
Notes on discovery behavior
CacheRoot::from_discovery() deterministically anchors discovered cache
paths under the configured CACHE_DIR_NAME (default: .cache). It does
not scan for arbitrary directory names — creating a directory named
.cache-v2 at the crate root will not cause from_discovery() to use it.
If you want to use a custom cache root, construct it explicitly with
CacheRoot::from_root(...).
OS-native user cache root (optional)
Enable feature flag:
Then construct a CacheRoot from platform-native user cache directories:
use CacheRoot;
let root = from_project_dirs
.expect;
let group = root.group;
group.ensure_dir.expect;
from_project_dirs uses directories::ProjectDirs and typically resolves to:
- macOS:
~/Library/Caches/<app> - Linux:
$XDG_CACHE_HOME/<app>or~/.cache/<app> - Windows:
%LOCALAPPDATA%\\<org>\\<app>\\cache
from_project_dirs(qualifier, organization, application) parameters:
qualifier: a DNS-like namespace component (commonly"com"or"org")organization: vendor/team name (for example"ExampleOrg")application: app/tool identifier (for example"ExampleApp")
Example identity tuple:
use CacheRoot;
use ProjectDirs;
use fs;
let root: CacheRoot = from_project_dirs
.expect;
let got: PathBuf = root.path.to_path_buf;
let expected: PathBuf = from
.expect
.cache_dir
.to_path_buf;
assert_eq!;
// If the example writes anything, keep it scoped and remove it explicitly.
let example_group = root.group;
let probe = example_group.touch.expect;
assert!;
remove_dir_all.expect;
Eviction Policy
Use EvictPolicy with:
CacheGroup::ensure_dir_with_policy(...)CacheRoot::ensure_group_with_policy(...)CacheGroup::eviction_report(...)to preview which files would be evicted.
Apply policy directly to a CacheGroup:
use ;
let root: CacheRoot = from_root;
let group: CacheGroup = root.group;
let policy: EvictPolicy = EvictPolicy ;
group
.ensure_dir_with_policy
.expect;
Apply policy through CacheRoot convenience API:
use ;
use Duration;
let root: CacheRoot = from_root;
let policy: EvictPolicy = EvictPolicy ;
root
.ensure_group_with_policy
.expect;
Preview evictions without deleting files:
use ;
let root: CacheRoot = from_root;
let group: CacheGroup = root.group;
let policy: EvictPolicy = EvictPolicy ;
let report: EvictionReport = group.eviction_report.expect;
for path in report.marked_for_eviction
Policy fields:
max_age: remove files older than or equal to the age threshold.max_files: keep at most N files.max_bytes: keep total file bytes at or below the threshold.
Policies can be combined by setting multiple fields in one EvictPolicy.
When combined, all configured limits are enforced in order.
use EvictPolicy;
use Duration;
let combined: EvictPolicy = EvictPolicy ;
Eviction order is always:
max_agemax_filesmax_bytes
For max_files and max_bytes, files are evicted oldest-first by modified time (ascending), then by path for deterministic tie-breaking.
eviction_report(...) and ensure_*_with_policy(...) use the same selection logic.
How max_bytes works
- Scans regular files recursively under the managed directory.
- Sums
metadata.len()across those files. - If total exceeds
max_bytes, removes files oldest-first until total is<= max_bytes. - Directories are not counted as bytes.
- Enforcement happens only during policy-aware
ensure_*_with_policycalls (not continuously in the background).
Optional process/thread scoped caches
Enable feature flag:
Or, if editing Cargo.toml manually:
[]
= { = "<latest>", = ["process-scoped-cache"] }
CacheRoot from tempdir
Create a temporary cache root backed by a persisted temp directory:
ProcessScopedCacheGroup from root and group path
Use this constructor when you have a CacheRoot plus a relative group path.
It creates a process-scoped directory under root.group(...).
ProcessScopedCacheGroup from existing group
Use this constructor when you already have a CacheGroup (for example,
shared or precomputed by higher-level setup) and want process scoping from
that existing group.
Behavior notes:
- Respects all configured roots/groups because process-scoped paths are always created under your provided
CacheRoot/CacheGroup. - The process subdirectory is deleted when the handle is dropped during normal process shutdown.
- Cleanup is best-effort; abnormal termination (for example
SIGKILLor crash) can leave stale directories.
Per-subdirectory policies
Get the root path
To obtain the underlying filesystem path for a CacheRoot, use path():
use CacheRoot;
let root: CacheRoot = from_root;
let root_path: &Path = root.path;
println!;
Also obtain a CacheGroup path and resolve an entry path under that group:
use ;
let root: CacheRoot = from_root;
let group: CacheGroup = root.group;
let group_path: &Path = group.path;
println!;
let entry_path: PathBuf = group.entry_path;
println!;
License
cache-manager is primarily distributed under the terms of both the MIT license and the Apache License (Version 2.0).
See LICENSE-APACHE and LICENSE-MIT for details.