Skip to main content

osdk_core/cache/
mod.rs

1//! Manager-native downstream package caches (dedup layer 2).
2//!
3//! Each language package manager keeps its own global cache/store. By pointing
4//! each manager at a stable directory under an osdk-managed root, different
5//! projects and SDK versions can reuse that manager's downloaded dependencies.
6//! The directories are deliberately separate; osdk doesn't provide a universal
7//! cross-manager package CAS.
8//!
9//! These are emitted during shell activation, `osdk exec`, and direct shim
10//! execution (and can be inspected via `osdk cache env`). We only set a
11//! variable if the user hasn't already set it, so explicit choices win.
12
13use std::collections::BTreeMap;
14use std::path::{Path, PathBuf};
15
16/// The shared downstream-cache root, `<cache>/pkg`.
17pub fn downstream_root(cache_dir: &Path) -> PathBuf {
18    cache_dir.join("pkg")
19}
20
21/// Compute the env vars that redirect package-manager caches to the shared
22/// root. `getenv` lets callers avoid overriding user-set values.
23pub fn cache_env(
24    cache_dir: &Path,
25    getenv: impl Fn(&str) -> Option<String>,
26) -> BTreeMap<String, String> {
27    let root = downstream_root(cache_dir);
28    let mut env = BTreeMap::new();
29
30    let mut set_if_unset = |key: &str, path: PathBuf| {
31        if variable_is_available(key, &getenv) {
32            env.insert(key.to_string(), path.display().to_string());
33        }
34    };
35
36    // npm is also exposed by Node distributions, so keep its cache available
37    // even when a project selects only the Node backend.
38    set_if_unset("npm_config_cache", root.join("npm"));
39    // pip: download/wheel cache
40    set_if_unset("PIP_CACHE_DIR", root.join("pip"));
41    // Go: module cache
42    set_if_unset("GOMODCACHE", root.join("go-mod"));
43    set_if_unset("GOCACHE", root.join("go-build"));
44    // Cargo: registry + git caches (shared home; note this also holds bins)
45    set_if_unset("CARGO_HOME", root.join("cargo"));
46    // Maven / Gradle (java ecosystem)
47    set_if_unset("GRADLE_USER_HOME", root.join("gradle"));
48
49    env
50}
51
52/// Compute version-specific environment variables for one package manager.
53///
54/// `mappings` contains `(environment variable, directory below <cache>/pkg)`.
55/// A value already present in the process is preserved unless it was set by a
56/// previous osdk shell hook, in which case `OSDK_ORIG_<key>_SET` is present and
57/// the managed value may be refreshed safely.
58pub fn manager_env(
59    cache_dir: &Path,
60    mappings: &[(&str, &str)],
61    getenv: impl Fn(&str) -> Option<String>,
62) -> BTreeMap<String, String> {
63    let root = downstream_root(cache_dir);
64    mappings
65        .iter()
66        .filter(|(key, _)| variable_is_available(key, &getenv))
67        .map(|(key, directory)| {
68            (
69                (*key).to_string(),
70                root.join(directory).display().to_string(),
71            )
72        })
73        .collect()
74}
75
76/// Use the current process environment when computing manager cache settings.
77pub fn manager_exec_env(cache_dir: &Path, mappings: &[(&str, &str)]) -> BTreeMap<String, String> {
78    manager_env(cache_dir, mappings, |key| std::env::var(key).ok())
79}
80
81fn variable_is_available(key: &str, getenv: &impl Fn(&str) -> Option<String>) -> bool {
82    getenv(&format!("OSDK_ORIG_{key}_SET")).is_some() || getenv(key).is_none()
83}
84
85/// Human-readable listing of what the shared caches map to.
86pub fn describe(cache_dir: &Path) -> Vec<(String, String)> {
87    let mut env = cache_env(cache_dir, |_| None);
88    for (key, value) in manager_env(
89        cache_dir,
90        &[
91            ("npm_config_cache", "npm"),
92            ("PNPM_HOME", "pnpm"),
93            ("npm_config_store_dir", "pnpm-store"),
94            ("pnpm_config_store_dir", "pnpm-store"),
95            ("YARN_CACHE_FOLDER", "yarn-classic"),
96            ("YARN_GLOBAL_FOLDER", "yarn"),
97            ("BUN_INSTALL_CACHE_DIR", "bun"),
98            ("DENO_DIR", "deno"),
99        ],
100        |_| None,
101    ) {
102        env.insert(key, value);
103    }
104    env.into_iter().collect()
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use std::collections::HashMap;
111
112    #[test]
113    fn maps_known_managers() {
114        let cache = PathBuf::from("/x/cache");
115        let env = cache_env(&cache, |_| None);
116        assert_eq!(
117            PathBuf::from(env.get("PIP_CACHE_DIR").unwrap()),
118            cache.join("pkg/pip")
119        );
120        assert_eq!(
121            PathBuf::from(env.get("GOMODCACHE").unwrap()),
122            cache.join("pkg/go-mod")
123        );
124        assert_eq!(
125            PathBuf::from(env.get("npm_config_cache").unwrap()),
126            cache.join("pkg/npm")
127        );
128
129        let npm = manager_env(&cache, &[("npm_config_cache", "npm")], |_| None);
130        assert_eq!(
131            PathBuf::from(npm.get("npm_config_cache").unwrap()),
132            cache.join("pkg/npm")
133        );
134    }
135
136    #[test]
137    fn respects_user_set_vars() {
138        let cache = PathBuf::from("/x/cache");
139        let mut user = HashMap::new();
140        user.insert("PIP_CACHE_DIR".to_string(), "/custom/pip".to_string());
141        let env = cache_env(&cache, |k| user.get(k).cloned());
142        // user's PIP_CACHE_DIR is left untouched (not in our delta)
143        assert!(!env.contains_key("PIP_CACHE_DIR"));
144        // others are still set
145        assert!(env.contains_key("GOMODCACHE"));
146
147        let managed = cache_env(&cache, |key| match key {
148            "PIP_CACHE_DIR" => Some("/old/osdk/pkg/pip".into()),
149            "OSDK_ORIG_PIP_CACHE_DIR_SET" => Some("1".into()),
150            _ => None,
151        });
152        assert_eq!(
153            PathBuf::from(managed.get("PIP_CACHE_DIR").unwrap()),
154            cache.join("pkg/pip")
155        );
156    }
157
158    #[test]
159    fn manager_env_preserves_user_values_but_refreshes_hook_managed_values() {
160        let cache = PathBuf::from("/x/cache");
161        let mappings = &[("npm_config_cache", "npm")];
162        let user = manager_env(&cache, mappings, |key| {
163            (key == "npm_config_cache").then(|| "/custom/npm".into())
164        });
165        assert!(user.is_empty());
166
167        let managed = manager_env(&cache, mappings, |key| match key {
168            "npm_config_cache" => Some("/old/osdk/pkg/npm".into()),
169            "OSDK_ORIG_npm_config_cache_SET" => Some("1".into()),
170            _ => None,
171        });
172        assert_eq!(
173            PathBuf::from(managed.get("npm_config_cache").unwrap()),
174            cache.join("pkg/npm")
175        );
176    }
177
178    #[test]
179    fn describe_includes_bun_and_deno_manager_caches() {
180        let cache = PathBuf::from("/x/cache");
181        let described = describe(&cache).into_iter().collect::<BTreeMap<_, _>>();
182        assert_eq!(
183            PathBuf::from(described.get("BUN_INSTALL_CACHE_DIR").unwrap()),
184            cache.join("pkg/bun")
185        );
186        assert_eq!(
187            PathBuf::from(described.get("DENO_DIR").unwrap()),
188            cache.join("pkg/deno")
189        );
190    }
191}