Skip to main content

mesh_llm_native_runtime/
cache.rs

1use crate::{NativeRuntimeManifest, manifest::NATIVE_RUNTIME_MANIFEST_FILE};
2use anyhow::{Context, Result};
3use serde::{Deserialize, Serialize};
4use std::{
5    fs,
6    path::{Path, PathBuf},
7};
8
9/// Relative path of the hardware benchmark executable shipped by GPU runtimes.
10///
11/// The manifest checksum must include this path before the executable is
12/// selected or run. CPU and Vulkan runtimes intentionally do not provide it.
13#[cfg(target_os = "windows")]
14pub const GPU_BENCHMARK_TOOL_PATH: &str = "tools/mesh-llm-gpu-benchmark.exe";
15#[cfg(not(target_os = "windows"))]
16pub const GPU_BENCHMARK_TOOL_PATH: &str = "tools/mesh-llm-gpu-benchmark";
17
18#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
19pub struct NativeRuntimeCacheRoot {
20    pub path: PathBuf,
21}
22
23#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
24pub struct InstalledNativeRuntime {
25    pub mesh_version: String,
26    pub native_runtime_id: String,
27    pub flavor: String,
28    pub path: PathBuf,
29    pub manifest: NativeRuntimeManifest,
30}
31
32impl InstalledNativeRuntime {
33    /// Resolve the manifest-verified GPU benchmark helper inside this runtime.
34    pub fn gpu_benchmark_tool(&self) -> Result<PathBuf> {
35        if !self
36            .manifest
37            .runtime
38            .tools
39            .contains_key(GPU_BENCHMARK_TOOL_PATH)
40        {
41            anyhow::bail!(
42                "native runtime {} does not provide the GPU benchmark tool",
43                self.native_runtime_id
44            );
45        }
46        let path = self.path.join(GPU_BENCHMARK_TOOL_PATH);
47        if !path.is_file() {
48            anyhow::bail!(
49                "native runtime GPU benchmark tool is missing: {}",
50                path.display()
51            );
52        }
53        #[cfg(unix)]
54        {
55            use std::os::unix::fs::PermissionsExt;
56
57            if fs::metadata(&path)?.permissions().mode() & 0o111 == 0 {
58                anyhow::bail!(
59                    "native runtime GPU benchmark tool is not executable: {}",
60                    path.display()
61                );
62            }
63        }
64        Ok(path)
65    }
66}
67
68#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
69#[serde(rename_all = "snake_case")]
70pub enum NativeRuntimePruneMode {
71    KeepActiveAndPrevious,
72    ActiveOnly,
73}
74
75#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
76pub struct CachePrunePlan {
77    #[serde(default)]
78    pub remove_dirs: Vec<PathBuf>,
79}
80
81#[derive(Clone, Debug, Eq, PartialEq)]
82pub struct NativeRuntimeCache {
83    root: PathBuf,
84}
85
86impl NativeRuntimeCache {
87    pub fn new(root: impl Into<PathBuf>) -> Self {
88        Self { root: root.into() }
89    }
90
91    pub fn root(&self) -> &Path {
92        &self.root
93    }
94
95    pub fn runtime_dir(&self, mesh_version: &str, native_runtime_id: &str) -> PathBuf {
96        self.root.join(mesh_version).join(native_runtime_id)
97    }
98
99    pub fn installed(&self) -> Result<Vec<InstalledNativeRuntime>> {
100        let mut installed = Vec::new();
101        if !self.root.exists() {
102            return Ok(installed);
103        }
104        for version_entry in fs::read_dir(&self.root)
105            .with_context(|| format!("read native runtime cache {}", self.root.display()))?
106        {
107            let version_entry = version_entry?;
108            if !version_entry.file_type()?.is_dir() {
109                continue;
110            }
111            installed.extend(installed_in_version_dir(&version_entry.path())?);
112        }
113        installed.sort_by(|left, right| {
114            (&left.mesh_version, &left.native_runtime_id)
115                .cmp(&(&right.mesh_version, &right.native_runtime_id))
116        });
117        Ok(installed)
118    }
119
120    /// Returns strictly validated runtimes for one MeshLLM version.
121    ///
122    /// TODO(issue #1162): Remove this resolver-specific compatibility boundary
123    /// once the oldest supported upgrade path no longer contains pre-checksum
124    /// runtime caches, if full-cache resolver enumeration is safe again.
125    pub(crate) fn installed_for_version(
126        &self,
127        mesh_version: &str,
128    ) -> Result<Vec<InstalledNativeRuntime>> {
129        installed_in_version_dir(&self.root.join(mesh_version))
130    }
131
132    pub fn find_installed(
133        &self,
134        mesh_version: &str,
135        native_runtime_id: &str,
136    ) -> Result<Option<InstalledNativeRuntime>> {
137        let dir = self.runtime_dir(mesh_version, native_runtime_id);
138        if !dir.join(NATIVE_RUNTIME_MANIFEST_FILE).exists() {
139            return Ok(None);
140        }
141        installed_runtime_from_dir(&dir)
142    }
143
144    pub fn install_from_dir(&self, source_dir: &Path) -> Result<InstalledNativeRuntime> {
145        let manifest = NativeRuntimeManifest::read_from_dir(source_dir)?;
146        manifest.validate()?;
147        let mesh_version = manifest
148            .runtime
149            .mesh_version
150            .as_deref()
151            .unwrap_or("unknown");
152        let target = self.runtime_dir(mesh_version, manifest.runtime.native_runtime_id());
153        if target.exists() {
154            fs::remove_dir_all(&target)
155                .with_context(|| format!("replace native runtime {}", target.display()))?;
156        }
157        copy_dir_recursive(source_dir, &target)?;
158        installed_runtime_from_dir(&target)?.context("installed native runtime manifest missing")
159    }
160
161    pub fn remove(&self, mesh_version: &str, native_runtime_id: &str) -> Result<bool> {
162        let dir = self.runtime_dir(mesh_version, native_runtime_id);
163        if !dir.exists() {
164            return Ok(false);
165        }
166        fs::remove_dir_all(&dir)
167            .with_context(|| format!("remove native runtime {}", dir.display()))?;
168        Ok(true)
169    }
170
171    pub fn prune_plan(
172        &self,
173        active_mesh_version: &str,
174        mode: NativeRuntimePruneMode,
175    ) -> Result<CachePrunePlan> {
176        let mut versions = self.installed_versions()?;
177        versions.sort();
178        let previous = match mode {
179            NativeRuntimePruneMode::ActiveOnly => None,
180            NativeRuntimePruneMode::KeepActiveAndPrevious => versions
181                .iter()
182                .rfind(|version| version.as_str() != active_mesh_version)
183                .cloned(),
184        };
185        let remove_dirs = versions
186            .into_iter()
187            .filter(|version| version != active_mesh_version)
188            .filter(|version| Some(version) != previous.as_ref())
189            .map(|version| self.root.join(version))
190            .collect();
191        Ok(CachePrunePlan { remove_dirs })
192    }
193
194    pub fn prune(
195        &self,
196        active_mesh_version: &str,
197        mode: NativeRuntimePruneMode,
198    ) -> Result<CachePrunePlan> {
199        let plan = self.prune_plan(active_mesh_version, mode)?;
200        for dir in &plan.remove_dirs {
201            if dir.exists() {
202                fs::remove_dir_all(dir)
203                    .with_context(|| format!("remove native runtime cache {}", dir.display()))?;
204            }
205        }
206        Ok(plan)
207    }
208
209    fn installed_versions(&self) -> Result<Vec<String>> {
210        if !self.root.exists() {
211            return Ok(Vec::new());
212        }
213        let mut versions = Vec::new();
214        for entry in fs::read_dir(&self.root)
215            .with_context(|| format!("read native runtime cache {}", self.root.display()))?
216        {
217            let entry = entry?;
218            if entry.file_type()?.is_dir() {
219                versions.push(entry.file_name().to_string_lossy().to_string());
220            }
221        }
222        Ok(versions)
223    }
224}
225
226pub fn native_runtime_cache_root(base_cache_dir: &Path) -> PathBuf {
227    base_cache_dir.join("mesh-llm").join("native-runtimes")
228}
229
230fn installed_runtime_from_dir(dir: &Path) -> Result<Option<InstalledNativeRuntime>> {
231    if !dir.join(NATIVE_RUNTIME_MANIFEST_FILE).exists() {
232        return Ok(None);
233    }
234    let manifest = NativeRuntimeManifest::read_from_dir(dir)?;
235    let mesh_version = manifest
236        .runtime
237        .mesh_version
238        .clone()
239        .unwrap_or_else(|| "unknown".to_string());
240    Ok(Some(InstalledNativeRuntime {
241        mesh_version,
242        native_runtime_id: manifest.runtime.id.clone(),
243        flavor: manifest.runtime.backend.kind.to_string(),
244        path: dir.to_path_buf(),
245        manifest,
246    }))
247}
248
249fn installed_in_version_dir(version_dir: &Path) -> Result<Vec<InstalledNativeRuntime>> {
250    let mut installed = Vec::new();
251    if !version_dir.is_dir() {
252        return Ok(installed);
253    }
254    for runtime_entry in fs::read_dir(version_dir)
255        .with_context(|| format!("read native runtime cache {}", version_dir.display()))?
256    {
257        let runtime_entry = runtime_entry?;
258        if !runtime_entry.file_type()?.is_dir() {
259            continue;
260        }
261        if let Some(runtime) = installed_runtime_from_dir(&runtime_entry.path())? {
262            installed.push(runtime);
263        }
264    }
265    installed.sort_by(|left, right| left.native_runtime_id.cmp(&right.native_runtime_id));
266    Ok(installed)
267}
268
269fn copy_dir_recursive(source: &Path, target: &Path) -> Result<()> {
270    fs::create_dir_all(target).with_context(|| format!("create {}", target.display()))?;
271    for entry in fs::read_dir(source).with_context(|| format!("read {}", source.display()))? {
272        let entry = entry?;
273        let source_path = entry.path();
274        let target_path = target.join(entry.file_name());
275        if entry.file_type()?.is_dir() {
276            copy_dir_recursive(&source_path, &target_path)?;
277        } else {
278            fs::copy(&source_path, &target_path).with_context(|| {
279                format!(
280                    "copy {} to {}",
281                    source_path.display(),
282                    target_path.display()
283                )
284            })?;
285        }
286    }
287    Ok(())
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293    use crate::{
294        NativeRuntimeArtifact, NativeRuntimeBackend, NativeRuntimeManifest, NativeRuntimePlatform,
295    };
296
297    fn write_runtime(dir: &Path, version: &str, id: &str) {
298        fs::create_dir_all(dir.join("lib")).unwrap();
299        fs::write(dir.join("lib/libmeshllm_ffi.so"), b"native runtime").unwrap();
300        let manifest = NativeRuntimeManifest {
301            runtime: NativeRuntimeArtifact {
302                id: id.to_string(),
303                mesh_version: Some(version.to_string()),
304                skippy_abi: "0.1.25".to_string(),
305                platform: NativeRuntimePlatform {
306                    os: "linux".to_string(),
307                    arch: "x86_64".to_string(),
308                    target: None,
309                },
310                backend: NativeRuntimeBackend::cpu(),
311                rank: 0,
312                libraries: vec!["lib/libmeshllm_ffi.so".to_string()],
313                files: Default::default(),
314                tools: Default::default(),
315                url: None,
316                sha256: None,
317                signature: None,
318            },
319        };
320        manifest.write_to_dir(dir).unwrap();
321    }
322
323    #[test]
324    fn installs_bundle_runtime_into_versioned_cache() {
325        let temp = tempfile::tempdir().unwrap();
326        let source = temp.path().join("source");
327        write_runtime(&source, "0.68.0", "meshllm-native-linux-x86_64-cpu");
328
329        let cache = NativeRuntimeCache::new(temp.path().join("cache"));
330        let installed = cache.install_from_dir(&source).unwrap();
331
332        assert_eq!(installed.mesh_version, "0.68.0");
333        assert!(installed.path.ends_with("meshllm-native-linux-x86_64-cpu"));
334    }
335
336    #[test]
337    fn installed_for_version_ignores_legacy_cache_versions() {
338        let temp = tempfile::tempdir().unwrap();
339        let cache = NativeRuntimeCache::new(temp.path().join("cache"));
340        write_runtime(
341            &cache.runtime_dir("0.75.0", "meshllm-native-linux-x86_64-cpu"),
342            "0.75.0",
343            "meshllm-native-linux-x86_64-cpu",
344        );
345
346        let legacy = cache.runtime_dir("0.74.0", "meshllm-native-linux-x86_64-cpu");
347        fs::create_dir_all(legacy.join("lib")).unwrap();
348        fs::write(legacy.join("lib/libmeshllm_ffi.so"), b"legacy runtime").unwrap();
349        fs::write(
350            legacy.join(NATIVE_RUNTIME_MANIFEST_FILE),
351            r#"{
352  "runtime": {
353    "id": "meshllm-native-linux-x86_64-cpu",
354    "mesh_version": "0.74.0",
355    "skippy_abi": "0.1.25",
356    "platform": {"os": "linux", "arch": "x86_64"},
357    "backend": {"kind": "cpu"},
358    "libraries": ["lib/libmeshllm_ffi.so"]
359  }
360}"#,
361        )
362        .unwrap();
363
364        let installed = cache.installed_for_version("0.75.0").unwrap();
365
366        assert_eq!(installed.len(), 1);
367        assert_eq!(installed[0].mesh_version, "0.75.0");
368        assert!(cache.installed().is_err());
369    }
370
371    #[test]
372    fn installed_for_version_ignores_file_at_version_path() {
373        let temp = tempfile::tempdir().unwrap();
374        let cache = NativeRuntimeCache::new(temp.path().join("cache"));
375        fs::create_dir_all(cache.root()).unwrap();
376        fs::write(cache.root().join("0.75.0"), b"partial cache artifact").unwrap();
377
378        let installed = cache.installed_for_version("0.75.0").unwrap();
379
380        assert!(installed.is_empty());
381    }
382
383    #[test]
384    fn prune_keeps_active_and_previous_by_default() {
385        let temp = tempfile::tempdir().unwrap();
386        let cache = NativeRuntimeCache::new(temp.path().join("cache"));
387        for version in ["0.67.0", "0.68.0", "0.69.0"] {
388            write_runtime(
389                &cache.runtime_dir(version, "meshllm-native-linux-x86_64-cpu"),
390                version,
391                "meshllm-native-linux-x86_64-cpu",
392            );
393        }
394
395        let plan = cache
396            .prune_plan("0.69.0", NativeRuntimePruneMode::KeepActiveAndPrevious)
397            .unwrap();
398
399        assert_eq!(plan.remove_dirs, vec![cache.root().join("0.67.0")]);
400    }
401
402    #[test]
403    fn installed_runtime_exposes_load_plan() {
404        let temp = tempfile::tempdir().unwrap();
405        let source = temp.path().join("source");
406        write_runtime(&source, "0.68.0", "meshllm-native-linux-x86_64-cpu");
407
408        let cache = NativeRuntimeCache::new(temp.path().join("cache"));
409        let installed = cache.install_from_dir(&source).unwrap();
410        let plan = installed.load_plan().unwrap();
411
412        assert_eq!(plan.native_runtime_id, "meshllm-native-linux-x86_64-cpu");
413        assert_eq!(
414            plan.libraries,
415            vec![
416                cache
417                    .runtime_dir("0.68.0", "meshllm-native-linux-x86_64-cpu")
418                    .join("lib/libmeshllm_ffi.so")
419            ]
420        );
421    }
422
423    #[cfg(unix)]
424    #[test]
425    fn gpu_benchmark_tool_must_be_declared_and_executable() {
426        use std::os::unix::fs::PermissionsExt;
427
428        let temp = tempfile::tempdir().unwrap();
429        let source = temp.path().join("source");
430        write_runtime(&source, "0.68.0", "meshllm-native-linux-x86_64-cuda12");
431
432        let cache = NativeRuntimeCache::new(temp.path().join("cache"));
433        let mut installed = cache.install_from_dir(&source).unwrap();
434        let tool = installed.path.join(GPU_BENCHMARK_TOOL_PATH);
435        fs::create_dir_all(tool.parent().unwrap()).unwrap();
436        fs::write(&tool, b"benchmark tool").unwrap();
437        installed
438            .manifest
439            .runtime
440            .tools
441            .insert(GPU_BENCHMARK_TOOL_PATH.to_string(), "0".repeat(64));
442
443        let error = installed.gpu_benchmark_tool().unwrap_err();
444        assert!(error.to_string().contains("not executable"));
445
446        let mut permissions = fs::metadata(&tool).unwrap().permissions();
447        permissions.set_mode(0o755);
448        fs::set_permissions(&tool, permissions).unwrap();
449        assert_eq!(installed.gpu_benchmark_tool().unwrap(), tool);
450    }
451}