Skip to main content

sui_eval/
drv_cache.rs

1//! Content-addressed derivation path cache.
2//!
3//! Maps `(lock_hash, source_hash, attr_path) → (drvPath, outPath)` using redb.
4//! Survives process restarts. Designed to avoid full nixpkgs evaluation when
5//! the result is already known for a given `flake.lock` + `flake.nix` + attribute.
6//!
7//! The cache file lives at `~/.cache/sui/drv-cache.redb` by default.
8
9use std::path::{Path, PathBuf};
10
11use redb::{Database, ReadableTable, ReadableTableMetadata, TableDefinition};
12use sha2::{Digest, Sha256};
13use tracing::{debug, info};
14
15/// redb table: key = `"{lock_hash}:{source_hash}:{attr_path}"`, value = `"{drv_path}\n{out_path}"`.
16const DRV_TABLE: TableDefinition<&str, &str> = TableDefinition::new("drv_paths");
17
18/// Errors from the derivation cache.
19#[derive(Debug, thiserror::Error)]
20pub enum DrvCacheError {
21    #[error("redb error: {0}")]
22    Db(String),
23    #[error("I/O error: {0}")]
24    Io(#[from] std::io::Error),
25}
26
27/// A cached derivation path entry.
28#[derive(Debug, Clone)]
29pub struct DrvCacheEntry {
30    pub drv_path: String,
31    pub out_path: String,
32}
33
34/// redb-backed derivation path cache.
35pub struct DrvCache {
36    db: Database,
37}
38
39impl DrvCache {
40    /// Open or create the cache at the given path.
41    pub fn open(path: &Path) -> Result<Self, DrvCacheError> {
42        if let Some(parent) = path.parent() {
43            std::fs::create_dir_all(parent)?;
44        }
45        let db = Database::create(path)
46            .map_err(|e| DrvCacheError::Db(format!("open: {e}")))?;
47
48        // Ensure the table exists.
49        let txn = db
50            .begin_write()
51            .map_err(|e| DrvCacheError::Db(format!("txn: {e}")))?;
52        { let _ = txn.open_table(DRV_TABLE); }
53        txn.commit()
54            .map_err(|e| DrvCacheError::Db(format!("commit: {e}")))?;
55
56        info!(path = %path.display(), "Opened derivation cache");
57        Ok(Self { db })
58    }
59
60    /// Default cache path: `~/.cache/sui/drv-cache.redb`.
61    pub fn default_path() -> PathBuf {
62        // .filter(!is_empty) was already here; absoluteness was the missing
63        // half — the partial-guard shape, which reads as guarded.
64        let base = std::env::var_os("XDG_CACHE_HOME")
65            .map(PathBuf::from)
66            .filter(|p| p.is_absolute())
67            .or_else(|| {
68                std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".cache"))
69            })
70            .unwrap_or_else(|| PathBuf::from("/tmp"));
71        base.join("sui").join("drv-cache.redb")
72    }
73
74    /// Look up a cached derivation.
75    pub fn get(
76        &self,
77        lock_hash: &str,
78        source_hash: &str,
79        attr_path: &str,
80    ) -> Option<DrvCacheEntry> {
81        let key = format!("{lock_hash}:{source_hash}:{attr_path}");
82        let txn = self.db.begin_read().ok()?;
83        let table = txn.open_table(DRV_TABLE).ok()?;
84        let value = table.get(key.as_str()).ok()??;
85        let s = value.value();
86        let (drv, out) = s.split_once('\n')?;
87        debug!(attr_path, "drv cache hit");
88        Some(DrvCacheEntry {
89            drv_path: drv.to_string(),
90            out_path: out.to_string(),
91        })
92    }
93
94    /// Store a derivation mapping.
95    pub fn put(
96        &self,
97        lock_hash: &str,
98        source_hash: &str,
99        attr_path: &str,
100        entry: &DrvCacheEntry,
101    ) -> Result<(), DrvCacheError> {
102        let key = format!("{lock_hash}:{source_hash}:{attr_path}");
103        let value = format!("{}\n{}", entry.drv_path, entry.out_path);
104        let txn = self.db.begin_write()
105            .map_err(|e| DrvCacheError::Db(format!("txn: {e}")))?;
106        {
107            let mut table = txn.open_table(DRV_TABLE)
108                .map_err(|e| DrvCacheError::Db(format!("table: {e}")))?;
109            table.insert(key.as_str(), value.as_str())
110                .map_err(|e| DrvCacheError::Db(format!("insert: {e}")))?;
111        }
112        txn.commit()
113            .map_err(|e| DrvCacheError::Db(format!("commit: {e}")))?;
114        debug!(attr_path, "drv cache put");
115        Ok(())
116    }
117
118    /// Number of cached entries.
119    pub fn len(&self) -> usize {
120        let Ok(txn) = self.db.begin_read() else { return 0 };
121        let Ok(table) = txn.open_table(DRV_TABLE) else { return 0 };
122        table.len().unwrap_or(0) as usize
123    }
124
125    /// Whether the cache is empty.
126    pub fn is_empty(&self) -> bool {
127        self.len() == 0
128    }
129
130    /// SHA-256 hex digest of file content — used for cache keys.
131    pub fn hash_bytes(content: &[u8]) -> String {
132        let mut hasher = Sha256::new();
133        hasher.update(content);
134        format!("{:x}", hasher.finalize())
135    }
136}
137
138// ── Thread-local singleton ────────────────────────────────────
139
140thread_local! {
141    static GLOBAL_CACHE: std::cell::RefCell<Option<DrvCache>> = const { std::cell::RefCell::new(None) };
142}
143
144/// Initialize the global derivation cache (call once at startup).
145pub fn init_global_cache() {
146    GLOBAL_CACHE.with(|cell| {
147        let mut cache = cell.borrow_mut();
148        if cache.is_none() {
149            let path = DrvCache::default_path();
150            match DrvCache::open(&path) {
151                Ok(c) => {
152                    info!(entries = c.len(), "Derivation cache initialized");
153                    *cache = Some(c);
154                }
155                Err(e) => {
156                    tracing::warn!(error = %e, "Failed to open derivation cache (continuing without)");
157                }
158            }
159        }
160    });
161}
162
163/// Access the global cache for a lookup.
164pub fn with_cache<F, R>(f: F) -> Option<R>
165where
166    F: FnOnce(&DrvCache) -> Option<R>,
167{
168    GLOBAL_CACHE.with(|cell| {
169        let borrow = cell.borrow();
170        borrow.as_ref().and_then(f)
171    })
172}
173
174/// Access the global cache for a write.
175pub fn with_cache_mut<F>(f: F)
176where
177    F: FnOnce(&DrvCache),
178{
179    GLOBAL_CACHE.with(|cell| {
180        let borrow = cell.borrow();
181        if let Some(cache) = borrow.as_ref() {
182            f(cache);
183        }
184    });
185}
186
187// ── Tests ─────────────────────────────────────────────────────
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn roundtrip() {
195        let tmp = tempfile::tempdir().unwrap();
196        let cache = DrvCache::open(&tmp.path().join("test.redb")).unwrap();
197
198        assert!(cache.get("lock1", "src1", "packages.x86_64-linux.default").is_none());
199
200        cache
201            .put("lock1", "src1", "packages.x86_64-linux.default", &DrvCacheEntry {
202                drv_path: "/nix/store/abc-hello.drv".to_string(),
203                out_path: "/nix/store/xyz-hello-2.10".to_string(),
204            })
205            .unwrap();
206
207        let entry = cache.get("lock1", "src1", "packages.x86_64-linux.default").unwrap();
208        assert_eq!(entry.drv_path, "/nix/store/abc-hello.drv");
209        assert_eq!(entry.out_path, "/nix/store/xyz-hello-2.10");
210        assert_eq!(cache.len(), 1);
211    }
212
213    #[test]
214    fn different_keys_no_collision() {
215        let tmp = tempfile::tempdir().unwrap();
216        let cache = DrvCache::open(&tmp.path().join("test.redb")).unwrap();
217
218        cache.put("lock1", "src1", "attr.a", &DrvCacheEntry {
219            drv_path: "/nix/store/a.drv".into(),
220            out_path: "/nix/store/a".into(),
221        }).unwrap();
222
223        cache.put("lock1", "src1", "attr.b", &DrvCacheEntry {
224            drv_path: "/nix/store/b.drv".into(),
225            out_path: "/nix/store/b".into(),
226        }).unwrap();
227
228        cache.put("lock2", "src1", "attr.a", &DrvCacheEntry {
229            drv_path: "/nix/store/c.drv".into(),
230            out_path: "/nix/store/c".into(),
231        }).unwrap();
232
233        assert_eq!(cache.get("lock1", "src1", "attr.a").unwrap().out_path, "/nix/store/a");
234        assert_eq!(cache.get("lock1", "src1", "attr.b").unwrap().out_path, "/nix/store/b");
235        assert_eq!(cache.get("lock2", "src1", "attr.a").unwrap().out_path, "/nix/store/c");
236        assert_eq!(cache.len(), 3);
237    }
238
239    #[test]
240    fn hash_bytes_deterministic() {
241        let h1 = DrvCache::hash_bytes(b"hello world");
242        let h2 = DrvCache::hash_bytes(b"hello world");
243        assert_eq!(h1, h2);
244        assert_eq!(h1.len(), 64); // SHA-256 hex = 64 chars
245    }
246}