Skip to main content

pyro_artifacts/
cache.rs

1use crate::artifacts::{Artifact, Artifacts, Module, ModuleBinary, ModuleSource, Playbook};
2
3use cargo_toml::Dependency;
4use std::path::{Path, PathBuf};
5use std::{collections::HashMap, io};
6use tokio::fs;
7
8#[derive(Debug, thiserror::Error)]
9#[error("{context}: {error}")]
10pub struct CacheError {
11    pub context: String,
12    #[source]
13    pub error: std::io::Error,
14}
15
16#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
17pub struct PyroductConfig {
18    pub author: Option<String>,
19    pub target: Option<PathBuf>,
20    pub pyroduct: Option<Dependency>,
21    pub build_slots: Option<usize>,
22}
23
24/// A loaded playbook where all the libraries are on disk and the binary is loaded
25#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
26pub struct LoadedPlaybook {
27    pub binary: ModuleBinary,
28    /// Per-class capability configuration. Keys are class names.
29    #[serde(default)]
30    pub configurations: HashMap<String, Option<serde_json::Value>>,
31    #[serde(default)]
32    pub paths: HashMap<String, PathBuf>,
33}
34
35// The lock file is automatically unlocked when `_lock_file` is dropped (fs2 behavior).
36
37pub struct CacheManager {
38    pub root: PathBuf,
39}
40
41impl CacheManager {
42    pub async fn new(root: &Path) -> Result<Self, CacheError> {
43        if !root.exists() {
44            fs::create_dir_all(&root).await.map_err(|e| CacheError {
45                context: "Failed to create cache root".to_string(),
46                error: e,
47            })?;
48        }
49        let manager = Self {
50            root: root.to_path_buf(),
51        };
52
53        Ok(manager)
54    }
55
56    pub async fn from_env() -> Result<Self, CacheError> {
57        let root = std::env::var("PYRODUCT")
58            .map(PathBuf::from)
59            .unwrap_or_else(|_| {
60                let home = std::env::var("HOME")
61                    .or_else(|_| std::env::var("USERPROFILE"))
62                    .map(PathBuf::from)
63                    .unwrap_or_else(|_| PathBuf::from("."));
64                home.join(".pyroduct")
65            });
66
67        Self::new(&root).await
68    }
69
70    pub async fn init(&self) -> Result<(), CacheError> {
71        fs::create_dir_all(self.capabilities_base_dir())
72            .await
73            .map_err(|error| CacheError {
74                context: format!(
75                    "Failed to create capabilities cache dir in {:?}",
76                    self.capabilities_base_dir()
77                ),
78                error,
79            })?;
80
81        fs::create_dir_all(self.interfaces_base_dir())
82            .await
83            .map_err(|error| CacheError {
84                context: "Failed to create interfaces cache dir".to_string(),
85                error,
86            })?;
87
88        let module_dir = self.root.join("modules");
89        fs::create_dir_all(&module_dir)
90            .await
91            .map_err(|error| CacheError {
92                context: "Failed to create modules cache dir".to_string(),
93                error,
94            })?;
95
96        let anon_dir = self.root.join("anon");
97        fs::create_dir_all(&anon_dir)
98            .await
99            .map_err(|error| CacheError {
100                context: "Failed to create anon cache dir".to_string(),
101                error,
102            })?;
103
104        Ok(())
105    }
106
107    pub async fn list_available_capabilities(
108        &self,
109    ) -> Result<Vec<(String, String, String)>, CacheError> {
110        let base = self.capabilities_base_dir();
111        if !base.exists() {
112            return Ok(Vec::new());
113        }
114
115        let mut results = Vec::new();
116        let mut authors = fs::read_dir(&base).await.map_err(|e| CacheError {
117            context: "Failed to read capabilities base dir".to_string(),
118            error: e,
119        })?;
120
121        while let Some(author_entry) = authors.next_entry().await.map_err(|e| CacheError {
122            context: "Failed to read author entry".to_string(),
123            error: e,
124        })? {
125            let author_path = author_entry.path();
126            if !author_path.is_dir() {
127                continue;
128            }
129            let author_name = author_entry.file_name().to_string_lossy().to_string();
130
131            let mut names = fs::read_dir(&author_path).await.map_err(|e| CacheError {
132                context: format!("Failed to read author dir: {}", author_path.display()),
133                error: e,
134            })?;
135
136            while let Some(name_entry) = names.next_entry().await.map_err(|e| CacheError {
137                context: "Failed to read name entry".to_string(),
138                error: e,
139            })? {
140                let name_path = name_entry.path();
141                if !name_path.is_dir() {
142                    continue;
143                }
144                let cap_name = name_entry.file_name().to_string_lossy().to_string();
145
146                let mut versions = fs::read_dir(&name_path).await.map_err(|e| CacheError {
147                    context: format!("Failed to read name dir: {}", name_path.display()),
148                    error: e,
149                })?;
150
151                while let Some(version_entry) =
152                    versions.next_entry().await.map_err(|e| CacheError {
153                        context: "Failed to read version entry".to_string(),
154                        error: e,
155                    })?
156                {
157                    let version_path = version_entry.path();
158                    if !version_path.is_dir() {
159                        continue;
160                    }
161                    let version = version_entry.file_name().to_string_lossy().to_string();
162
163                    if version_path.join("interface.json").exists() {
164                        results.push((author_name.clone(), cap_name.clone(), version));
165                    }
166                }
167            }
168        }
169
170        Ok(results)
171    }
172
173    pub fn capabilities_base_dir(&self) -> PathBuf {
174        self.root.join("capabilities")
175    }
176
177    pub fn capabilities_dir(&self, author: &str, name: &str, version: &str) -> PathBuf {
178        self.capabilities_base_dir()
179            .join(author)
180            .join(name)
181            .join(version)
182    }
183
184    pub fn interface_dir(&self, author: &str, name: &str, version: &str) -> PathBuf {
185        self.interfaces_base_dir()
186            .join(author)
187            .join(name)
188            .join(version)
189    }
190
191    pub fn interfaces_base_dir(&self) -> PathBuf {
192        self.root.join("interfaces")
193    }
194
195    pub async fn capability_interface_spec(
196        &self,
197        author: &str,
198        name: &str,
199        version: &str,
200    ) -> Result<String, CacheError> {
201        let path = self
202            .capabilities_dir(author, name, version)
203            .join("interface.json");
204        fs::read_to_string(&path).await.map_err(|error| CacheError {
205            context: format!("Failed to read interface.json from {}", path.display()),
206            error,
207        })
208    }
209
210    pub async fn capability_binary_path(
211        &self,
212        author: &str,
213        name: &str,
214        version: &str,
215    ) -> Result<PathBuf, CacheError> {
216        let base_dir = self.capabilities_dir(author, name, version);
217
218        #[cfg(target_os = "linux")]
219        let lib_file = "lib.so";
220        #[cfg(target_os = "macos")]
221        let lib_file = "lib.dylib";
222        #[cfg(target_os = "windows")]
223        let lib_file = "lib.dll";
224        #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
225        let lib_file = "lib.so";
226
227        let path = base_dir.join(lib_file);
228        if !path.exists() {
229            Err(CacheError {
230                context: format!("Missing {} binary for this system", path.display()),
231                error: io::Error::new(io::ErrorKind::NotFound, "Not Found"),
232            })
233        } else {
234            Ok(path)
235        }
236    }
237
238    pub async fn capability_config_spec(
239        &self,
240        author: &str,
241        name: &str,
242        version: &str,
243    ) -> Result<Option<String>, CacheError> {
244        let path = self
245            .capabilities_dir(author, name, version)
246            .join("config.json");
247        if path.exists() {
248            let content = fs::read_to_string(&path)
249                .await
250                .map_err(|error| CacheError {
251                    context: format!("Failed to read config.json from {}", path.display()),
252                    error,
253                })?;
254            Ok(Some(content))
255        } else {
256            Ok(None)
257        }
258    }
259
260    pub async fn get_binary(&self, hash: &str) -> Result<ModuleBinary, CacheError> {
261        let path = self.root.join("anon").join(hash);
262        if path.exists() {
263            let binary = ModuleBinary::from_dir(&path)
264                .await
265                .map_err(|error| CacheError {
266                    context: "Unable to load binary".to_string(),
267                    error,
268                })?;
269            Ok(binary)
270        } else {
271            Err(CacheError {
272                context: format!("Missing {} binary", path.display()),
273                error: io::Error::new(io::ErrorKind::NotFound, "Not Found"),
274            })
275        }
276    }
277
278    pub async fn get_source(&self, hash: &str) -> Result<ModuleSource, CacheError> {
279        let path = self.root.join("anon").join(hash);
280        if path.exists() {
281            let source = ModuleSource::from_dir(&path)
282                .await
283                .map_err(|error| CacheError {
284                    context: "Unable to load source".to_string(),
285                    error,
286                })?;
287            Ok(source)
288        } else {
289            Err(CacheError {
290                context: format!("Missing {} source", path.display()),
291                error: io::Error::new(io::ErrorKind::NotFound, "Not Found"),
292            })
293        }
294    }
295
296    pub async fn write_artifacts(&self, artifacts: &Artifacts) -> Result<(), CacheError> {
297        match &artifacts {
298            Artifacts::CapabilityBinary(capability) => {
299                let path = self.capabilities_dir(
300                    &capability.ident.author,
301                    &capability.ident.name,
302                    &capability.ident.version,
303                );
304                capability
305                    .write_to_directory(&path)
306                    .await
307                    .map_err(|e| CacheError {
308                        context: format!("Failed to write artifacts to {}", path.display()),
309                        error: e,
310                    })
311            }
312            Artifacts::CapabilitySource(capability) => {
313                let path = self.capabilities_dir(
314                    &capability.manifest.capability.author,
315                    &capability.manifest.capability.name,
316                    &capability.manifest.capability.version,
317                );
318                capability
319                    .write_to_directory(&path)
320                    .await
321                    .map_err(|e| CacheError {
322                        context: format!("Failed to write artifacts to {}", path.display()),
323                        error: e,
324                    })
325            }
326            Artifacts::Interface(interface) => {
327                let path = self.interface_dir(
328                    &interface.manifest.capability.author,
329                    &interface.manifest.capability.name,
330                    &interface.manifest.capability.version,
331                );
332                fs::create_dir_all(&path).await.map_err(|e| CacheError {
333                    context: format!("Failed to create  {}", path.display()),
334                    error: e,
335                })?;
336                let manifest = interface.manifest.clone();
337                let cargo_path = path.join("Cargo.toml");
338                let cargo = manifest.clone().to_interface_manifest();
339                let cargo = toml::to_string_pretty(&cargo).map_err(|e| CacheError {
340                    context: format!("Failed to serialize Cargo.toml to {}", cargo_path.display()),
341                    error: io::Error::new(io::ErrorKind::InvalidData, e),
342                })?;
343                fs::write(&cargo_path, cargo)
344                    .await
345                    .map_err(|e| CacheError {
346                        context: format!("Failed to write Cargo.toml to {}", cargo_path.display()),
347                        error: e,
348                    })?;
349                interface
350                    .write_to_directory(&path)
351                    .await
352                    .map_err(|e| CacheError {
353                        context: format!("Failed to write artifacts to {}", path.display()),
354                        error: e,
355                    })
356            }
357            Artifacts::Module(Module::Binary(binary)) => {
358                let path = self.root.join("anon").join(&binary.spec.hash);
359                binary
360                    .write_to_directory(&path)
361                    .await
362                    .map_err(|e| CacheError {
363                        context: format!("Failed to write artifacts to {}", path.display()),
364                        error: e,
365                    })
366            }
367            Artifacts::Module(Module::Source(source)) => {
368                let hash = source.hash();
369                let path = self.root.join("anon").join(hash);
370                source
371                    .write_to_directory(&path)
372                    .await
373                    .map_err(|e| CacheError {
374                        context: format!("Failed to write artifacts to {}", path.display()),
375                        error: e,
376                    })
377            }
378        }
379    }
380
381    pub async fn load_playbook(&self, playbook: Playbook) -> Result<LoadedPlaybook, CacheError> {
382        let binary = self.get_binary(&playbook.hash).await?;
383        let mut paths = HashMap::new();
384
385        for cap in &binary.spec.capabilities {
386            let path = self
387                .capability_binary_path(&cap.author, &cap.package, &cap.version)
388                .await?;
389            paths.insert(cap.package.clone(), path);
390        }
391
392        Ok(LoadedPlaybook {
393            binary,
394            configurations: playbook.configurations,
395            paths,
396        })
397    }
398}
399
400pub(crate) fn resolve_dependency_path(dep: &mut Dependency, base: &std::path::Path) {
401    if let Dependency::Detailed(detail) = dep {
402        if let Some(ref mut p) = detail.path {
403            let path = std::path::Path::new(p.as_str());
404            if path.is_relative() {
405                let absolute = base.join(&path);
406                *p = absolute
407                    .canonicalize()
408                    .unwrap_or(absolute)
409                    .to_string_lossy()
410                    .into_owned();
411            }
412        }
413    }
414}