From 78f3a65f1ec5116a890541c31ffb63310aaa3759 Mon Sep 17 00:00:00 2001 From: Jeremy Harris Date: Mon, 16 Mar 2026 18:30:18 -0600 Subject: [PATCH 1/9] Add support for os-cache-dir --- Cargo.lock | 87 ++++++++++++++++++++++++++++++++++++- Cargo.toml | 2 + README.md | 123 ++++++++++++++++++++++++++++++++++++++++++++--------- src/lib.rs | 90 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 280 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f3166b1..b076e64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,7 @@ checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" name = "cache-manager" version = "0.3.1" dependencies = [ + "directories", "tempfile", ] @@ -27,6 +28,27 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -55,6 +77,17 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.4.1" @@ -125,6 +158,15 @@ version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +[[package]] +name = "libredox" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +dependencies = [ + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -149,6 +191,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "prettyplease" version = "0.2.37" @@ -183,6 +231,17 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + [[package]] name = "rustix" version = "1.1.4" @@ -262,12 +321,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.4.1", "once_cell", "rustix", "windows-sys", ] +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "unicode-ident" version = "1.0.24" @@ -280,6 +359,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasip2" version = "1.0.2+wasi-0.2.9" diff --git a/Cargo.toml b/Cargo.toml index 883099f..1223378 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,9 +16,11 @@ tempfile = "3.27.0" [features] default = [] process-scoped-cache = ["dep:tempfile"] +os-cache-dir = ["dep:directories"] [dependencies] tempfile = { workspace = true, optional = true } +directories = { version = "6.0.0", optional = true } [dev-dependencies] tempfile.workspace = true diff --git a/README.md b/README.md index 5d89152..f8ed33a 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,21 @@ Directory-based cache and artifact path management with discovered `.cache` roots, grouped cache paths, and optional eviction on directory initialization. -- **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. -- **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(...)`). _This tool was designed to facilitate common cache directory management in a multi-crate workspace._ +- **Core capabilities** + - **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:** the standard install uses only the Rust standard library. + - **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. + - **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(...)`). _This tool was designed to facilitate common cache directory management in a multi-crate workspace._ + +- **Optional features** + - **`process-scoped-cache`:** adds [`tempfile`](https://docs.rs/tempfile) and enables process/thread scoped caches (`ProcessScopedCacheGroup`, `CacheRoot::from_tempdir(...)`). + - **`os-cache-dir`:** adds [`directories`](https://docs.rs/directories) and enables OS-native per-user cache roots (`CacheRoot::from_project_dirs(...)`). + +- **Licensing** + - **Open-source + commercial-friendly:** dual-licensed under [MIT][mit-license-page] or [Apache-2.0][apache-2.0-license-page]. > Tested on macOS, Linux, and Windows. @@ -83,17 +89,21 @@ println!("{}", entry_without_touch.display()); ### Filesystem effects -- **Pure path operations:** `CacheRoot::from_root`, `CacheRoot::cache_path`, `CacheRoot::group`, `CacheGroup::entry_path`, `CacheGroup::subgroup` -- **Discovery helper (cwd/crate-root based):** `CacheRoot::from_discovery` -- **Create dirs:** `CacheRoot::ensure_group`, `CacheGroup::ensure_dir` -- **Create dirs + optional eviction:** `CacheRoot::ensure_group_with_policy`, `CacheGroup::ensure_dir_with_policy` -- **Create file (creates parents):** `CacheGroup::touch` +- **Core APIs (always available):** + - `CacheRoot::from_root`, `CacheRoot::from_discovery`, `CacheRoot::cache_path`, `CacheRoot::group` + - `CacheGroup::subgroup`, `CacheGroup::entry_path` + - `CacheRoot::ensure_group`, `CacheGroup::ensure_dir` + - `CacheRoot::ensure_group_with_policy`, `CacheGroup::ensure_dir_with_policy` + - `CacheGroup::touch` -With feature `process-scoped-cache` enabled: +- **Feature `os-cache-dir`:** + - `CacheRoot::from_project_dirs` -- **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` +- **Feature `process-scoped-cache`:** + - `CacheRoot::from_tempdir` + - `ProcessScopedCacheGroup::new`, `ProcessScopedCacheGroup::from_group` + - `ProcessScopedCacheGroup::thread_group`, `ProcessScopedCacheGroup::ensure_thread_group` + - `ProcessScopedCacheGroup::thread_entry_path`, `ProcessScopedCacheGroup::touch_thread_entry` > Note: eviction only runs when you pass a policy to the `*_with_policy` methods. @@ -143,6 +153,63 @@ not scan for arbitrary directory names — creating a directory named 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: + +```bash +cargo add cache-manager --features os-cache-dir +``` + +Then construct a `CacheRoot` from platform-native user cache directories: + +```rust +use cache_manager::CacheRoot; + +let root = CacheRoot::from_project_dirs("com", "ExampleOrg", "ExampleApp") + .expect("discover OS cache dir"); + +let group = root.group("artifacts"); +group.ensure_dir().expect("ensure group"); +``` + +`from_project_dirs` uses `directories::ProjectDirs` and typically resolves to: + +- macOS: `~/Library/Caches/` +- Linux: `$XDG_CACHE_HOME/` or `~/.cache/` +- Windows: `%LOCALAPPDATA%\\\\\\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: + +```rust +use cache_manager::CacheRoot; +use directories::ProjectDirs; +use std::fs; + +let root: CacheRoot = CacheRoot::from_project_dirs("com", "Acme", "WidgetTool") + .expect("discover OS cache dir"); +let got: std::path::PathBuf = root.path().to_path_buf(); + +let expected: std::path::PathBuf = ProjectDirs::from("com", "Acme", "WidgetTool") + .expect("resolve project dirs") + .cache_dir() + .to_path_buf(); + +assert_eq!(got, expected); + +// If the example writes anything, keep it scoped and remove it explicitly. +let example_group = root.group("cache-manager-readme-example"); +let probe = example_group.touch("probe.txt").expect("write probe"); +assert!(probe.exists()); +fs::remove_dir_all(example_group.path()).expect("cleanup example group"); +``` + ### Eviction Policy @@ -258,6 +325,20 @@ Or, if editing `Cargo.toml` manually: cache-manager = { version = "", features = ["process-scoped-cache"] } ``` +Create a temporary cache root backed by a persisted temp directory: + +```rust +#[cfg(feature = "process-scoped-cache")] +fn example_temp_root() { + let root = cache_manager::CacheRoot::from_tempdir().expect("temp cache root"); + let group = root.group("artifacts"); + group.ensure_dir().expect("ensure group"); + + // `from_tempdir` intentionally persists the directory; clean up when done. + std::fs::remove_dir_all(root.path()).expect("cleanup temp root"); +} +``` + Use `ProcessScopedCacheGroup` to create an auto-generated process subdirectory under your assigned root/group, then derive a stable subgroup for each thread: @@ -379,7 +460,7 @@ println!("entry path: {}", entry_path.display()); `cache-manager` is primarily distributed under the terms of both the MIT license and the Apache License (Version 2.0). -See [LICENSE-APACHE](./LICENSE-APACHE) and [LICENSE-MIT](./LICENSE-MIT) for details. +See [LICENSE-APACHE][apache-2.0-license-page] and [LICENSE-MIT][mit-license-page] for details. [rust-src-page]: https://www.rust-lang.org/ [rust-logo]: https://img.shields.io/badge/Made%20with-Rust-black @@ -387,10 +468,10 @@ See [LICENSE-APACHE](./LICENSE-APACHE) and [LICENSE-MIT](./LICENSE-MIT) for deta [crates-page]: https://crates.io/crates/cache-manager [crates-badge]: https://img.shields.io/crates/v/cache-manager.svg -[mit-license-page]: ./LICENSE-MIT +[mit-license-page]: https://raw.githubusercontent.com/jzombie/rust-cache-manager/refs/heads/main/LICENSE-MIT [mit-license-badge]: https://img.shields.io/badge/license-MIT-blue.svg -[apache-2.0-license-page]: ./LICENSE-APACHE +[apache-2.0-license-page]: https://raw.githubusercontent.com/jzombie/rust-cache-manager/refs/heads/main/LICENSE-APACHE [apache-2.0-license-badge]: https://img.shields.io/badge/license-Apache%202.0-blue.svg [coveralls-page]: https://coveralls.io/github/jzombie/rust-cache-manager?branch=main diff --git a/src/lib.rs b/src/lib.rs index cc4c6b5..d85aaa5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,8 @@ use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use constants::{CACHE_DIR_NAME, CARGO_TOML_FILE_NAME}; +#[cfg(feature = "os-cache-dir")] +use directories::ProjectDirs; #[cfg(feature = "process-scoped-cache")] use tempfile::{Builder, TempDir}; @@ -103,6 +105,63 @@ impl CacheRoot { Self { root: root.into() } } + /// Create a `CacheRoot` from an OS-native per-user cache directory for + /// the given project identity. + /// + /// This API is available when the `os-cache-dir` feature is enabled and + /// uses [`directories::ProjectDirs`] internally. + /// + /// The returned path is OS-specific and typically resolves to: + /// - macOS: `~/Library/Caches/` + /// - Linux: `$XDG_CACHE_HOME/` or `~/.cache/` + /// - Windows: `%LOCALAPPDATA%\\\\\\cache` + /// + /// Parameters are passed directly to `ProjectDirs::from(qualifier, + /// organization, application)`. + /// + /// `qualifier` is a DNS-like namespace component used primarily on some + /// platforms (notably macOS) to form a unique app identity. Common values + /// include: + /// - `"com"` (for apps under a `com..` naming scheme) + /// - `"org"` (for apps under an `org..` naming scheme) + /// + /// Example: + /// `CacheRoot::from_project_dirs("com", "Acme", "WidgetTool")`. + #[cfg(feature = "os-cache-dir")] + pub fn from_project_dirs( + qualifier: &str, + organization: &str, + application: &str, + ) -> io::Result { + let project_dirs = + ProjectDirs::from(qualifier, organization, application).ok_or_else(|| { + io::Error::new( + io::ErrorKind::NotFound, + "could not resolve an OS cache directory for the provided project identity", + ) + })?; + + Ok(Self { + root: project_dirs.cache_dir().to_path_buf(), + }) + } + + /// Create a `CacheRoot` using a newly-created directory under the system + /// temporary directory. + /// + /// This API is available when the `process-scoped-cache` feature is + /// enabled (which provides the `tempfile` dependency). + /// + /// The directory is intentionally persisted and returned as the cache root, + /// so it is **not** automatically deleted when this function returns. + /// Callers who want cleanup should remove `root.path()` explicitly when + /// finished. + #[cfg(feature = "process-scoped-cache")] + pub fn from_tempdir() -> io::Result { + let root = TempDir::new()?.keep(); + Ok(Self { root }) + } + /// Return the underlying path for this `CacheRoot`. pub fn path(&self) -> &Path { &self.root @@ -652,6 +711,37 @@ mod tests { assert_eq!(resolved, absolute); } + #[cfg(feature = "os-cache-dir")] + #[test] + fn from_project_dirs_matches_directories_cache_dir() { + let qualifier = "com"; + let organization = "CacheManagerTests"; + let application = "CacheManagerOsCacheRoot"; + + let expected = ProjectDirs::from(qualifier, organization, application) + .expect("project dirs") + .cache_dir() + .to_path_buf(); + + let root = CacheRoot::from_project_dirs(qualifier, organization, application) + .expect("from project dirs"); + + assert_eq!(root.path(), expected.as_path()); + } + + #[cfg(feature = "process-scoped-cache")] + #[test] + fn from_tempdir_creates_existing_writable_root() { + let root = CacheRoot::from_tempdir().expect("from tempdir"); + assert!(root.path().is_dir()); + + let probe_group = root.group("probe"); + let probe_file = probe_group.touch("writable.txt").expect("touch probe"); + assert!(probe_file.is_file()); + + fs::remove_dir_all(root.path()).expect("cleanup temp root"); + } + #[test] fn ensure_dir_with_policy_max_files() { let tmp = TempDir::new().expect("tempdir"); From b328fce30a6b4ed13110e46adf0f651abf76be05 Mon Sep 17 00:00:00 2001 From: Jeremy Harris Date: Mon, 16 Mar 2026 18:30:37 -0600 Subject: [PATCH 2/9] Prepare for 0.4.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b076e64..6dd7bcc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,7 +16,7 @@ checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "cache-manager" -version = "0.3.1" +version = "0.4.0" dependencies = [ "directories", "tempfile", diff --git a/Cargo.toml b/Cargo.toml index 1223378..f98fcc5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cache-manager" -version = "0.3.1" +version = "0.4.0" edition = "2024" description = "Simple managed directory system for project-scoped caches with optional eviction policies." license = "MIT OR Apache-2.0" From cf7b8160d9c8e83429727067c71021528cccdfef Mon Sep 17 00:00:00 2001 From: Jeremy Harris Date: Mon, 16 Mar 2026 18:32:26 -0600 Subject: [PATCH 3/9] Sort cargo deps --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f98fcc5..75826a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,8 +19,8 @@ process-scoped-cache = ["dep:tempfile"] os-cache-dir = ["dep:directories"] [dependencies] -tempfile = { workspace = true, optional = true } directories = { version = "6.0.0", optional = true } +tempfile = { workspace = true, optional = true } [dev-dependencies] tempfile.workspace = true From 8f913d774c77f1ec7f3121dddb0ad44dac68394e Mon Sep 17 00:00:00 2001 From: Jeremy Harris Date: Mon, 16 Mar 2026 18:53:51 -0600 Subject: [PATCH 4/9] Add more tests --- src/lib.rs | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d85aaa5..96d7db0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,6 +20,16 @@ use directories::ProjectDirs; #[cfg(feature = "process-scoped-cache")] use tempfile::{Builder, TempDir}; +#[cfg(feature = "os-cache-dir")] +fn project_dirs_or_not_found(project_dirs: Option) -> io::Result { + project_dirs.ok_or_else(|| { + io::Error::new( + io::ErrorKind::NotFound, + "could not resolve an OS cache directory for the provided project identity", + ) + }) +} + /// Optional eviction controls applied by `CacheGroup::ensure_dir_with_policy` /// and `CacheRoot::ensure_group_with_policy`. /// @@ -134,12 +144,7 @@ impl CacheRoot { application: &str, ) -> io::Result { let project_dirs = - ProjectDirs::from(qualifier, organization, application).ok_or_else(|| { - io::Error::new( - io::ErrorKind::NotFound, - "could not resolve an OS cache directory for the provided project identity", - ) - })?; + project_dirs_or_not_found(ProjectDirs::from(qualifier, organization, application))?; Ok(Self { root: project_dirs.cache_dir().to_path_buf(), @@ -729,6 +734,14 @@ mod tests { assert_eq!(root.path(), expected.as_path()); } + #[cfg(feature = "os-cache-dir")] + #[test] + fn project_dirs_or_not_found_returns_not_found_for_none() { + let err = project_dirs_or_not_found(None) + .expect_err("none project dirs should map to not found"); + assert_eq!(err.kind(), io::ErrorKind::NotFound); + } + #[cfg(feature = "process-scoped-cache")] #[test] fn from_tempdir_creates_existing_writable_root() { From 8245d54d45c3eaa3cb8b78c792305f3e6da591b3 Mon Sep 17 00:00:00 2001 From: Jeremy Harris Date: Mon, 16 Mar 2026 18:54:06 -0600 Subject: [PATCH 5/9] Clean up README --- README.md | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f8ed33a..257cc4c 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,12 @@ Directory-based cache and artifact path management with discovered `.cache` root - **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(...)`). _This tool was designed to facilitate common cache directory management in a multi-crate workspace._ - **Optional features** - - **`process-scoped-cache`:** adds [`tempfile`](https://docs.rs/tempfile) and enables process/thread scoped caches (`ProcessScopedCacheGroup`, `CacheRoot::from_tempdir(...)`). - - **`os-cache-dir`:** adds [`directories`](https://docs.rs/directories) and enables OS-native per-user cache roots (`CacheRoot::from_project_dirs(...)`). + - **`process-scoped-cache`:** adds [`tempfile`](https://docs.rs/tempfile) and enables process/thread scoped caches. + - [`CacheRoot::from_tempdir(...)`](#cacheroot-from-tempdir) + - [`ProcessScopedCacheGroup::new(...)`](#processscopedcachegroup-from-root-and-group-path) + - [`ProcessScopedCacheGroup::from_group(...)`](#processscopedcachegroup-from-existing-group) + - **`os-cache-dir`:** adds [`directories`](https://docs.rs/directories) and enables OS-native per-user cache roots. + - [`CacheRoot::from_project_dirs(...)`](#os-native-user-cache-root-optional) - **Licensing** - **Open-source + commercial-friendly:** dual-licensed under [MIT][mit-license-page] or [Apache-2.0][apache-2.0-license-page]. @@ -325,6 +329,8 @@ Or, if editing `Cargo.toml` manually: cache-manager = { version = "", features = ["process-scoped-cache"] } ``` +#### CacheRoot from tempdir + Create a temporary cache root backed by a persisted temp directory: ```rust @@ -339,8 +345,10 @@ fn example_temp_root() { } ``` -Use `ProcessScopedCacheGroup` to create an auto-generated process subdirectory -under your assigned root/group, then derive a stable subgroup for each thread: +#### 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(...)`. ```rust #[cfg(feature = "process-scoped-cache")] @@ -394,6 +402,30 @@ fn main() { fn main() {} ``` +#### 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. + +```rust +#[cfg(feature = "process-scoped-cache")] +fn from_group_example() { + use cache_manager::{CacheGroup, CacheRoot, ProcessScopedCacheGroup}; + + let root: CacheRoot = CacheRoot::from_root("/tmp/project"); + let base_group: CacheGroup = root.group("artifacts/session"); + + let scoped: ProcessScopedCacheGroup = + ProcessScopedCacheGroup::from_group(base_group).expect("create process-scoped cache"); + let thread_entry = scoped + .touch_thread_entry("v1/index.bin") + .expect("touch thread entry"); + + assert!(thread_entry.starts_with(scoped.path())); +} +``` + Behavior notes: - Respects all configured roots/groups because process-scoped paths are always created under your provided `CacheRoot`/`CacheGroup`. From a17f37090ba38d4f6f2f08865a06125ee96a7db1 Mon Sep 17 00:00:00 2001 From: Jeremy Harris Date: Mon, 16 Mar 2026 18:54:23 -0600 Subject: [PATCH 6/9] cargo fmt --all --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 96d7db0..f6dd9c6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -737,8 +737,8 @@ mod tests { #[cfg(feature = "os-cache-dir")] #[test] fn project_dirs_or_not_found_returns_not_found_for_none() { - let err = project_dirs_or_not_found(None) - .expect_err("none project dirs should map to not found"); + let err = + project_dirs_or_not_found(None).expect_err("none project dirs should map to not found"); assert_eq!(err.kind(), io::ErrorKind::NotFound); } From c08af1be4f3e29290f8344ded36decddac19219c Mon Sep 17 00:00:00 2001 From: Jeremy Harris Date: Mon, 16 Mar 2026 18:57:14 -0600 Subject: [PATCH 7/9] Add comment --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 257cc4c..0e8c463 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ let expected: std::path::PathBuf = root .join("index.bin"); assert_eq!(entry, expected); +// Example output path println!("{}", entry.display()); ``` From 093b7bb97692eff69f190b10ab7fb32cd19f0eee Mon Sep 17 00:00:00 2001 From: Jeremy Harris Date: Mon, 16 Mar 2026 19:00:03 -0600 Subject: [PATCH 8/9] Clarify zero default runtime deps --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0e8c463..8e010d6 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Directory-based cache and artifact path management with discovered `.cache` root - **Core capabilities** - **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:** the standard install uses only the Rust standard library. + - **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 `/.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. From 383afb9ee71a66c9205853bcd0bfec2f27c36dcb Mon Sep 17 00:00:00 2001 From: Jeremy Harris Date: Mon, 16 Mar 2026 19:13:18 -0600 Subject: [PATCH 9/9] Fine tune README --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8e010d6..957e617 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ 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-manager` provides a single, consistent cache/artifact path layer across the workspace _(and also works outside of `cargo` environments)_. + - **Core capabilities** - **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 default runtime dependencies:** the standard install uses only the Rust standard library _(optional features do add additional dependencies)_. @@ -11,7 +15,7 @@ Directory-based cache and artifact path management with discovered `.cache` root - **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. - **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(...)`). _This tool was designed to facilitate common cache directory management in a multi-crate workspace._ + - **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`:** adds [`tempfile`](https://docs.rs/tempfile) and enables process/thread scoped caches.