Skip to main content

outl_exec/wasm/
cache.rs

1//! Lazy generation cache for `.wasm` modules.
2//!
3//! Two kinds of artefacts land here:
4//!
5//! 1. **Per-source compilations** — e.g. a Rust snippet → WASM goes
6//!    into `runtimes/rust/<source-hash>.wasm`. Idempotent: same source
7//!    text never recompiles.
8//! 2. **Drop-in interpreters** — once Steel-WASM / QuickJS-wasi ship,
9//!    a user can place `runtimes/<lang>.wasm` here and the registry
10//!    discovers it (M2 follow-up).
11//!
12//! Path resolution:
13//! - Linux/BSD → `$XDG_CACHE_HOME/outl/runtimes/` or `~/.cache/outl/runtimes/`
14//! - macOS     → `~/Library/Caches/outl/runtimes/`
15//! - Windows   → `%LOCALAPPDATA%\outl\runtimes\`
16//!
17//! Delegated to the `dirs` crate so we follow OS conventions instead
18//! of hard-coding paths.
19
20use std::path::{Path, PathBuf};
21
22use sha2::{Digest, Sha256};
23
24/// Root cache directory: `<os cache>/outl/runtimes/`.
25///
26/// Creates the directory if missing. Returns `None` if the OS doesn't
27/// expose a user cache dir (extremely rare — sandboxed CI without a
28/// HOME, for example) so the caller can fall back to compiling on the
29/// fly without caching.
30pub fn cache_dir() -> Option<PathBuf> {
31    let mut p = dirs::cache_dir()?;
32    p.push("outl");
33    p.push("runtimes");
34    std::fs::create_dir_all(&p).ok()?;
35    Some(p)
36}
37
38/// Path where a compiled artefact for `(language, source)` should
39/// land. The filename is `<sha256-of-source>.wasm`; collisions are
40/// vanishingly unlikely and the hash also doubles as a cache key
41/// for "is this source the same as last time?".
42///
43/// Returns `None` only when `cache_dir()` is unavailable. The caller
44/// can fall back to a `tempfile` in that case.
45pub fn cache_path_for_source(language: &str, source: &str) -> Option<PathBuf> {
46    let mut dir = cache_dir()?;
47    dir.push(language);
48    std::fs::create_dir_all(&dir).ok()?;
49    let mut hasher = Sha256::new();
50    hasher.update(source.as_bytes());
51    let digest = hasher.finalize();
52    let mut hash = String::with_capacity(digest.len() * 2);
53    for b in digest {
54        use std::fmt::Write;
55        let _ = write!(hash, "{b:02x}");
56    }
57    dir.push(format!("{hash}.wasm"));
58    Some(dir)
59}
60
61/// Does the file at `path` exist *and* is non-empty? Used by lazy-gen
62/// runtimes to decide "skip the compile, just read the bytes".
63pub fn is_fresh(path: &Path) -> bool {
64    std::fs::metadata(path)
65        .map(|m| m.is_file() && m.len() > 0)
66        .unwrap_or(false)
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn cache_dir_exists_or_returns_none() {
75        // Smoke test — in CI/dev `dirs::cache_dir()` always works.
76        // The point is that we never panic.
77        let _ = cache_dir();
78    }
79
80    #[test]
81    fn same_source_hashes_to_same_path() {
82        let a = cache_path_for_source("rust", "fn main() {}").unwrap();
83        let b = cache_path_for_source("rust", "fn main() {}").unwrap();
84        assert_eq!(a, b);
85    }
86
87    #[test]
88    fn different_sources_hash_differently() {
89        let a = cache_path_for_source("rust", "fn main() {}").unwrap();
90        let b = cache_path_for_source("rust", "fn main() { 1; }").unwrap();
91        assert_ne!(a, b);
92    }
93}