From 0335e1719c4a69c443f0712597fb1bca134add02 Mon Sep 17 00:00:00 2001 From: Jeremy Harris Date: Mon, 16 Mar 2026 14:27:23 -0600 Subject: [PATCH 01/12] Add optional process-scoped cache --- Cargo.toml | 8 +++ README.md | 93 ++++++++++++++++++++++++++- src/lib.rs | 185 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 283 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c97d5cf..a40a825 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,15 @@ keywords = [ "workspace", ] +[features] +default = [] +process-scoped-cache = ["dep:tempfile"] + [dependencies] +tempfile = { workspace = true, optional = true } [dev-dependencies] +tempfile.workspace = true + +[workspace.dependencies] tempfile = "3.25.0" diff --git a/README.md b/README.md index bc161e2..a6fb40e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,10 @@ Directory-based cache and artifact path management with discovered `.cache` root **This crate is intentionally tool-agnostic** — it only manages cache/artifact directory layout and paths and does not assume or depend on any specific consumer tooling. Any tool or library that reads or writes files can use `cache-manager` to compute/manage project-scoped cache paths and apply eviction rules. -**It has zero runtime dependencies (standard library only for library consumers).** +**By default it has zero runtime dependencies (standard library only for library consumers).** + +An optional feature flag (`process-scoped-cache`) enables process/thread scoped +sub-caches backed by `tempfile` for automatic cleanup on normal shutdown. It is suitable for: @@ -35,10 +38,10 @@ use cache_manager::CacheRoot; let root = CacheRoot::from_root("/tmp/project"); let group = root.group("artifacts/json"); -// Create the group directory if needed. +// Create the group directory if needed group.ensure_dir().expect("ensure group"); -// `index.bin` is just an example artifact filename that another program might generate. +// `index.bin` is just an example artifact filename that another program might generate let entry: std::path::PathBuf = group.touch("v1/index.bin").expect("touch entry"); println!("{}", entry.display()); ``` @@ -69,6 +72,12 @@ println!("{}", entry_without_touch.display()); - **Create dirs + optional eviction:** `CacheRoot::ensure_group_with_policy`, `CacheGroup::ensure_dir_with_policy` - **Create file (creates parents):** `CacheGroup::touch` +With feature `process-scoped-cache` enabled: + +- **Process-scoped group:** `ProcessScopedCacheGroup::new`, `ProcessScopedCacheGroup::from_group` +- **Per-thread subgroup:** `ProcessScopedCacheGroup::thread_group`, `ProcessScopedCacheGroup::ensure_thread_group` +- **Per-thread entry helpers:** `ProcessScopedCacheGroup::thread_entry_path`, `ProcessScopedCacheGroup::touch_thread_entry` + > Note: eviction only runs when you pass a policy to the `*_with_policy` methods. ### Discovering cache paths @@ -93,8 +102,10 @@ let cache_path = CacheRoot::from_discovery() .expect("discover cache root") .cache_path("tool", "data.bin"); println!("cache path: {}", cache_path.display()); + // Expected relative location under the discovered crate root: assert!(cache_path.ends_with(Path::new(".cache").join("tool").join("data.bin"))); + // The call only computes the path; it does not create files or directories. assert!(!cache_path.exists()); @@ -215,6 +226,82 @@ For `max_files` and `max_bytes`, files are evicted oldest-first by modified time - Directories are not counted as bytes. - Enforcement happens only during policy-aware `ensure_*_with_policy` calls (not continuously in the background). +### Optional process/thread scoped caches + +Enable feature flag: + +```bash +cargo add cache-manager --features process-scoped-cache +``` + +Or, if editing `Cargo.toml` manually: + +```toml +[dependencies] +cache-manager = { version = "", features = ["process-scoped-cache"] } +``` + +Use `ProcessScopedCacheGroup` to create an auto-generated process subdirectory +under your assigned root/group, then derive a stable subgroup for each thread: + +```rust +#[cfg(feature = "process-scoped-cache")] +fn main() { + use cache_manager::{CacheRoot, ProcessScopedCacheGroup}; + use std::path::Path; + + // 1) Build the root and the base group where process directories will live + let root = CacheRoot::from_root("/tmp/project"); + let base_group = root.group("artifacts/session"); + + // 2) Create a process-scoped directory (name starts with `pid--...`) + let scoped = ProcessScopedCacheGroup::new(&root, "artifacts/session") + .expect("create process-scoped cache"); + + // 3) Resolve this thread's subgroup and touch an entry under it + let thread_group = scoped.ensure_thread_group().expect("ensure thread group"); + let entry = thread_group.touch("v1/index.bin").expect("touch thread entry"); + + // 4) Verify the static pieces of the structure + assert!(entry.starts_with(base_group.path())); + assert!(entry.ends_with(Path::new("v1/index.bin"))); + + // 5) Verify the dynamic thread segment (`thread-`) + let thread_dir = entry + .parent() + .and_then(|p| p.parent()) + .expect("thread dir"); + + assert!(thread_dir + .file_name() + .and_then(|s| s.to_str()) + .expect("thread dir name") + .starts_with("thread-")); + + // 6) Verify the dynamic process segment (`pid--`) + let process_dir = thread_dir.parent().expect("process dir"); + let expected_pid_prefix = format!("pid-{}-", std::process::id()); + + assert!(process_dir + .file_name() + .and_then(|s| s.to_str()) + .expect("process dir name") + .starts_with(&expected_pid_prefix)); + + // Example output path + println!("{}", entry.display()); +} + +#[cfg(not(feature = "process-scoped-cache"))] +fn main() {} +``` + +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 `SIGKILL` or crash) can leave stale directories. + ### Additional examples Create or update a cache entry (ensures parent directories exist): diff --git a/src/lib.rs b/src/lib.rs index 69aa1db..a25a3ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,14 +3,20 @@ mod constants; +#[cfg(feature = "process-scoped-cache")] +use std::cell::Cell; use std::env; use std::fs; use std::fs::OpenOptions; use std::io; use std::path::{Path, PathBuf}; +#[cfg(feature = "process-scoped-cache")] +use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use constants::{CACHE_DIR_NAME, CARGO_TOML_FILE_NAME}; +#[cfg(feature = "process-scoped-cache")] +use tempfile::{Builder, TempDir}; /// Optional eviction controls applied by `CacheGroup::ensure_dir_with_policy` /// and `CacheRoot::ensure_group_with_policy`. @@ -217,6 +223,105 @@ impl CacheGroup { } } +/// Process-scoped cache group handle with per-thread subgroup helpers. +/// +/// This type is available when the `process-scoped-cache` feature is enabled. +/// +/// It creates an auto-generated process subdirectory under a user-selected +/// base cache group. The backing directory is removed when this handle is +/// dropped during normal process shutdown. +/// +/// Notes: +/// - Cleanup is best-effort and is not guaranteed after abnormal termination +/// (for example `SIGKILL` or process crash). +/// - All paths still respect the caller-provided `CacheRoot` and base group. +#[cfg(feature = "process-scoped-cache")] +#[derive(Debug)] +pub struct ProcessScopedCacheGroup { + process_group: CacheGroup, + _temp_dir: TempDir, +} + +#[cfg(feature = "process-scoped-cache")] +impl ProcessScopedCacheGroup { + /// Create a process-scoped cache handle under `root.group(relative_group)`. + pub fn new>(root: &CacheRoot, relative_group: P) -> io::Result { + Self::from_group(root.group(relative_group)) + } + + /// Create a process-scoped cache handle under an existing base group. + pub fn from_group(base_group: CacheGroup) -> io::Result { + base_group.ensure_dir()?; + let pid = std::process::id(); + let temp_dir = Builder::new() + .prefix(&format!("pid-{pid}-")) + .tempdir_in(base_group.path())?; + let process_group = CacheGroup { + path: temp_dir.path().to_path_buf(), + }; + + Ok(Self { + process_group, + _temp_dir: temp_dir, + }) + } + + /// Return the process-scoped directory path. + pub fn path(&self) -> &Path { + self.process_group.path() + } + + /// Return the process-scoped cache group. + pub fn process_group(&self) -> CacheGroup { + self.process_group.clone() + } + + /// Return the subgroup for the current thread. + /// + /// Each thread gets a stable, process-local incremental id (`thread-`) + /// for the process lifetime. + pub fn thread_group(&self) -> CacheGroup { + self.process_group + .subgroup(format!("thread-{}", current_thread_cache_group_id())) + } + + /// Ensure and return the subgroup for the current thread. + pub fn ensure_thread_group(&self) -> io::Result { + let group = self.thread_group(); + group.ensure_dir()?; + Ok(group) + } + + /// Build an entry path inside the current thread subgroup. + pub fn thread_entry_path>(&self, relative_file: P) -> PathBuf { + self.thread_group().entry_path(relative_file) + } + + /// Touch an entry inside the current thread subgroup. + pub fn touch_thread_entry>(&self, relative_file: P) -> io::Result { + self.ensure_thread_group()?.touch(relative_file) + } +} + +#[cfg(feature = "process-scoped-cache")] +fn current_thread_cache_group_id() -> u64 { + thread_local! { + static THREAD_GROUP_ID: Cell> = const { Cell::new(None) }; + } + + static NEXT_THREAD_GROUP_ID: AtomicU64 = AtomicU64::new(1); + + THREAD_GROUP_ID.with(|slot| { + if let Some(id) = slot.get() { + id + } else { + let id = NEXT_THREAD_GROUP_ID.fetch_add(1, Ordering::Relaxed); + slot.set(Some(id)); + id + } + }) +} + fn find_crate_root(start: &Path) -> Option { let mut current = start.to_path_buf(); loop { @@ -998,4 +1103,84 @@ mod tests { assert_eq!(p1, p2); assert!(group.entry_path("keep_root.txt").exists()); } + + #[cfg(feature = "process-scoped-cache")] + #[test] + fn process_scoped_cache_respects_root_and_group_assignments() { + let tmp = TempDir::new().expect("tempdir"); + let root = CacheRoot::from_root(tmp.path().join("custom-root")); + + let scoped = ProcessScopedCacheGroup::new(&root, "artifacts/session").expect("create"); + let expected_prefix = root.group("artifacts/session").path().to_path_buf(); + + assert!(scoped.path().starts_with(&expected_prefix)); + assert!(scoped.path().exists()); + } + + #[cfg(feature = "process-scoped-cache")] + #[test] + fn process_scoped_cache_deletes_directory_on_drop() { + let tmp = TempDir::new().expect("tempdir"); + let root = CacheRoot::from_root(tmp.path()); + + let process_dir = { + let scoped = ProcessScopedCacheGroup::new(&root, "artifacts").expect("create"); + let p = scoped.path().to_path_buf(); + assert!(p.exists()); + p + }; + + assert!(!process_dir.exists()); + } + + #[cfg(feature = "process-scoped-cache")] + #[test] + fn process_scoped_cache_thread_group_is_stable_per_thread() { + let tmp = TempDir::new().expect("tempdir"); + let root = CacheRoot::from_root(tmp.path()); + let scoped = ProcessScopedCacheGroup::new(&root, "artifacts").expect("create"); + + let first = scoped.thread_group().path().to_path_buf(); + let second = scoped.thread_group().path().to_path_buf(); + + assert_eq!(first, second); + } + + #[cfg(feature = "process-scoped-cache")] + #[test] + fn process_scoped_cache_thread_group_differs_across_threads() { + let tmp = TempDir::new().expect("tempdir"); + let root = CacheRoot::from_root(tmp.path()); + let scoped = ProcessScopedCacheGroup::new(&root, "artifacts").expect("create"); + + let main_thread_group = scoped.thread_group().path().to_path_buf(); + let other_thread_group = std::thread::spawn(current_thread_cache_group_id) + .join() + .expect("join thread"); + + let expected_other = scoped + .process_group() + .subgroup(format!("thread-{other_thread_group}")) + .path() + .to_path_buf(); + + assert_ne!(main_thread_group, expected_other); + } + + #[cfg(feature = "process-scoped-cache")] + #[test] + fn touch_thread_entry_creates_entry_under_thread_group() { + let tmp = TempDir::new().expect("tempdir"); + let root = CacheRoot::from_root(tmp.path()); + let scoped = ProcessScopedCacheGroup::new(&root, "artifacts").expect("create"); + + let entry = scoped + .touch_thread_entry("nested/data.bin") + .expect("touch thread entry"); + + assert!(entry.exists()); + assert!(entry.starts_with(scoped.path())); + let thread_group = scoped.thread_group().path().to_path_buf(); + assert!(entry.starts_with(&thread_group)); + } } From 1e58147cfef07e09074337c7db8a16dc82d6e075 Mon Sep 17 00:00:00 2001 From: Jeremy Harris Date: Mon, 16 Mar 2026 14:29:15 -0600 Subject: [PATCH 02/12] Use consistent period endings (none) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a6fb40e..650f9bd 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ Behavior: use cache_manager::CacheRoot; use std::path::Path; -// Compute a path like /.cache/tool/data.bin without creating it. +// Compute a path like /.cache/tool/data.bin without creating it let cache_path = CacheRoot::from_discovery() .expect("discover cache root") .cache_path("tool", "data.bin"); @@ -106,7 +106,7 @@ println!("cache path: {}", cache_path.display()); // Expected relative location under the discovered crate root: assert!(cache_path.ends_with(Path::new(".cache").join("tool").join("data.bin"))); -// The call only computes the path; it does not create files or directories. +// The call only computes the path; it does not create files or directories assert!(!cache_path.exists()); // If you already have an absolute entry path, it's returned unchanged: From 7eeade37672169c5e2cb031331f0beb6ae95763d Mon Sep 17 00:00:00 2001 From: Jeremy Harris Date: Mon, 16 Mar 2026 14:29:47 -0600 Subject: [PATCH 03/12] Prepare for 0.3.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8df15f2..6eb281c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,7 +16,7 @@ checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "cache-manager" -version = "0.2.1" +version = "0.3.0" dependencies = [ "tempfile", ] diff --git a/Cargo.toml b/Cargo.toml index a40a825..c14273a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cache-manager" -version = "0.2.1" +version = "0.3.0" edition = "2024" description = "Simple managed directory system for project-scoped caches with optional eviction policies." license = "MIT OR Apache-2.0" From da6e373f84e6dc396f34db267f7fa7d3fb7ebcf4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:31:12 -0600 Subject: [PATCH 04/12] Bump tempfile from 3.26.0 to 3.27.0 (#7) Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.26.0 to 3.27.0. - [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md) - [Commits](https://github.com/Stebalien/tempfile/compare/v3.26.0...v3.27.0) --- updated-dependencies: - dependency-name: tempfile dependency-version: 3.27.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jeremy Harris --- Cargo.lock | 4 ++-- Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8df15f2..8efdcd7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -257,9 +257,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.26.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom", diff --git a/Cargo.toml b/Cargo.toml index a40a825..b34ac1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cache-manager" -version = "0.2.1" +version = "0.3.0" edition = "2024" description = "Simple managed directory system for project-scoped caches with optional eviction policies." license = "MIT OR Apache-2.0" @@ -26,4 +26,4 @@ tempfile = { workspace = true, optional = true } tempfile.workspace = true [workspace.dependencies] -tempfile = "3.25.0" +tempfile = "3.27.0" From 5a82aad31a37cbf0a3737bb7451850ec06a2fff7 Mon Sep 17 00:00:00 2001 From: Jeremy Harris Date: Mon, 16 Mar 2026 14:37:55 -0600 Subject: [PATCH 05/12] Fix incorrect author email --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b34ac1b..9ac5ab9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.3.0" edition = "2024" description = "Simple managed directory system for project-scoped caches with optional eviction policies." license = "MIT OR Apache-2.0" -authors = ["Jeremy "] +authors = ["Jeremy Harris "] repository = "https://github.com/jzombie/rust-cache-manager" categories = ["development-tools", "caching", "filesystem"] keywords = [ From 53a1d5900acaa91dae3352f510e21462ff20b204 Mon Sep 17 00:00:00 2001 From: Jeremy Harris Date: Mon, 16 Mar 2026 14:45:14 -0600 Subject: [PATCH 06/12] Add more tests --- src/lib.rs | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index a25a3ef..cc4c6b5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1028,6 +1028,26 @@ mod tests { assert_eq!(remaining.len(), 1); } + #[cfg(unix)] + #[test] + fn collect_files_recursive_ignores_non_file_non_directory_entries() { + use std::os::unix::net::UnixListener; + + let tmp = TempDir::new().expect("tempdir"); + let cache = CacheRoot::from_root(tmp.path()); + let group = cache.group("artifacts"); + group.ensure_dir().expect("ensure dir"); + + let socket_path = group.entry_path("live.sock"); + let _listener = UnixListener::bind(&socket_path).expect("bind unix socket"); + + fs::write(group.entry_path("a.bin"), vec![1u8; 1]).expect("write file"); + + let files = collect_files(group.path()).expect("collect files"); + assert_eq!(files.len(), 1); + assert_eq!(files[0].path, group.entry_path("a.bin")); + } + #[test] fn max_bytes_policy_under_threshold_does_not_evict() { let tmp = TempDir::new().expect("tempdir"); @@ -1167,6 +1187,35 @@ mod tests { assert_ne!(main_thread_group, expected_other); } + #[cfg(feature = "process-scoped-cache")] + #[test] + fn process_scoped_cache_from_group_uses_given_base_group() { + let tmp = TempDir::new().expect("tempdir"); + let root = CacheRoot::from_root(tmp.path()); + let base_group = root.group("artifacts/custom-base"); + + let scoped = ProcessScopedCacheGroup::from_group(base_group.clone()).expect("create"); + + assert!(scoped.path().starts_with(base_group.path())); + assert_eq!(scoped.process_group().path(), scoped.path()); + } + + #[cfg(feature = "process-scoped-cache")] + #[test] + fn process_scoped_cache_thread_entry_path_matches_touch_location() { + let tmp = TempDir::new().expect("tempdir"); + let root = CacheRoot::from_root(tmp.path()); + let scoped = ProcessScopedCacheGroup::new(&root, "artifacts").expect("create"); + + let planned = scoped.thread_entry_path("nested/data.bin"); + let touched = scoped + .touch_thread_entry("nested/data.bin") + .expect("touch thread entry"); + + assert_eq!(planned, touched); + assert!(touched.exists()); + } + #[cfg(feature = "process-scoped-cache")] #[test] fn touch_thread_entry_creates_entry_under_thread_group() { From 55c6738e29825d01973da260f7d0686d26d8234d Mon Sep 17 00:00:00 2001 From: Jeremy Harris Date: Mon, 16 Mar 2026 14:47:25 -0600 Subject: [PATCH 07/12] Sort cargo workspace --- Cargo.toml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9ac5ab9..0ec3c44 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,10 @@ keywords = [ "workspace", ] +# Optional/test dependencies +[workspace.dependencies] +tempfile = "3.27.0" + [features] default = [] process-scoped-cache = ["dep:tempfile"] @@ -24,6 +28,3 @@ tempfile = { workspace = true, optional = true } [dev-dependencies] tempfile.workspace = true - -[workspace.dependencies] -tempfile = "3.27.0" From ea10c9c9d4d6a795c8126371ae1da69baaabe7ca Mon Sep 17 00:00:00 2001 From: Jeremy Harris Date: Mon, 16 Mar 2026 14:54:35 -0600 Subject: [PATCH 08/12] Bump cargo sort (locally) --- Cargo.toml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0ec3c44..d6edd16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,13 +7,7 @@ license = "MIT OR Apache-2.0" authors = ["Jeremy Harris "] repository = "https://github.com/jzombie/rust-cache-manager" categories = ["development-tools", "caching", "filesystem"] -keywords = [ - "cache", - "artifact-cache", - "eviction-policy", - "filesystem", - "workspace", -] +keywords = ["cache", "artifact-cache", "eviction-policy", "filesystem", "workspace"] # Optional/test dependencies [workspace.dependencies] From 11774438ce84d0b116a22a4ec73646e4a64fe848 Mon Sep 17 00:00:00 2001 From: Jeremy Harris Date: Mon, 16 Mar 2026 14:58:55 -0600 Subject: [PATCH 09/12] Tighten README header --- README.md | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 650f9bd..5eb7ad5 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,11 @@ Directory-based cache and artifact path management with discovered `.cache` roots, grouped cache paths, and optional eviction on directory initialization. -**This crate is intentionally tool-agnostic** — it only manages cache/artifact directory layout and paths and does not assume or depend on any specific consumer tooling. Any tool or library that reads or writes files can use `cache-manager` to compute/manage project-scoped cache paths and apply eviction rules. - -**By default it has zero runtime dependencies (standard library only for library consumers).** - -An optional feature flag (`process-scoped-cache`) enables process/thread scoped -sub-caches backed by `tempfile` for automatic cleanup on normal shutdown. - -It is suitable for: - -- Artifact storage (build outputs, generated files, intermediate data, etc.). -- Monorepos or multi-crate workspaces that need centralized cache/artifact management via a shared root (for example with `CacheRoot::from_root(...)`). +- **Tool-agnostic:** manages only cache/artifact directory layout and paths, without assuming any specific consumer tooling. +- **Zero runtime dependencies** in the standard install (library consumers use only the Rust standard library). +- **Optional feature `process-scoped-cache`:** adds one runtime dependency, [`tempfile`](https://docs.rs/tempfile), to support process/thread scoped sub-caches with automatic cleanup on normal shutdown. +- **Suitable for artifact storage** (build outputs, generated files, intermediate data, etc.). +- **Suitable for monorepos or multi-crate workspaces** that need centralized cache/artifact management via a shared root (for example with `CacheRoot::from_root(...)`). > Tested on macOS, Linux, and Windows. From 9b70c776f63b1a3c6ef1b6556a33793e68162dbf Mon Sep 17 00:00:00 2001 From: Jeremy Harris Date: Mon, 16 Mar 2026 15:00:54 -0600 Subject: [PATCH 10/12] Tighten tool agnostic phrase --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5eb7ad5..dfcaa52 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Directory-based cache and artifact path management with discovered `.cache` roots, grouped cache paths, and optional eviction on directory initialization. -- **Tool-agnostic:** manages only cache/artifact directory layout and paths, without assuming any specific consumer tooling. +- **Tool-agnostic:** any tool or library that can write to the filesystem can use `cache-manager` as a managed cache/artifact path layout layer. - **Zero runtime dependencies** in the standard install (library consumers use only the Rust standard library). - **Optional feature `process-scoped-cache`:** adds one runtime dependency, [`tempfile`](https://docs.rs/tempfile), to support process/thread scoped sub-caches with automatic cleanup on normal shutdown. - **Suitable for artifact storage** (build outputs, generated files, intermediate data, etc.). From aee05a8b048a92b130f2ce88b30c83d4cd222047 Mon Sep 17 00:00:00 2001 From: Jeremy Harris Date: Mon, 16 Mar 2026 15:04:47 -0600 Subject: [PATCH 11/12] Add selling points --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dfcaa52..13d9606 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,11 @@ Directory-based cache and artifact path management with discovered `.cache` root - **Tool-agnostic:** any tool or library that can write to the filesystem can use `cache-manager` as a managed cache/artifact path layout layer. - **Zero runtime dependencies** in the standard install (library consumers use only the Rust standard library). - **Optional feature `process-scoped-cache`:** adds one runtime dependency, [`tempfile`](https://docs.rs/tempfile), to support process/thread scoped sub-caches with automatic cleanup on normal shutdown. +- **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 `/.cache` automatically or pin an explicit root with `CacheRoot::from_root(...)`. +- **Composable cache layout API:** create groups/subgroups and entry paths consistently across tools without custom path-joining logic. - **Suitable for artifact storage** (build outputs, generated files, intermediate data, etc.). -- **Suitable for monorepos or multi-crate workspaces** that need centralized cache/artifact management via a shared root (for example with `CacheRoot::from_root(...)`). +- **Suitable for monorepos or multi-crate workspaces** that need centralized cache/artifact management via a shared root (for example with `CacheRoot::from_root(...)`). _This tool was designed to facilitate common cache directory management in a multi-crate workspace._ > Tested on macOS, Linux, and Windows. From 0f2e90e49962ae5f178148af8a1d58f31593e2c4 Mon Sep 17 00:00:00 2001 From: Jeremy Harris Date: Mon, 16 Mar 2026 15:05:32 -0600 Subject: [PATCH 12/12] Add open source license to selling points --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 13d9606..34c2917 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Directory-based cache and artifact path management with discovered `.cache` root - **Tool-agnostic:** any tool or library that can write to the filesystem can use `cache-manager` as a managed cache/artifact path layout layer. - **Zero runtime dependencies** in the standard install (library consumers use only the Rust standard library). - **Optional feature `process-scoped-cache`:** adds one runtime dependency, [`tempfile`](https://docs.rs/tempfile), to support process/thread scoped sub-caches with automatic cleanup on normal shutdown. +- **Open-source + commercial-friendly licensing:** dual-licensed under MIT or Apache-2.0, so it can be used in open-source and commercial projects. - **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 `/.cache` automatically or pin an explicit root with `CacheRoot::from_root(...)`. - **Composable cache layout API:** create groups/subgroups and entry paths consistently across tools without custom path-joining logic.