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#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
26pub struct LoadedPlaybook {
27 pub binary: ModuleBinary,
28 #[serde(default)]
30 pub configurations: HashMap<String, Option<serde_json::Value>>,
31 #[serde(default)]
32 pub paths: HashMap<String, PathBuf>,
33}
34
35pub 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}