1use std::path::PathBuf;
17use std::process::{Command, Stdio};
18
19use directories::ProjectDirs;
20
21pub fn cache_root(proj_dirs: &ProjectDirs) -> PathBuf {
23 proj_dirs.cache_dir().to_path_buf()
24}
25
26pub fn routines_root(proj_dirs: &ProjectDirs) -> PathBuf {
28 cache_root(proj_dirs).join("routines")
29}
30
31pub fn routine_root(proj_dirs: &ProjectDirs, name: &str) -> PathBuf {
33 routines_root(proj_dirs).join(name)
34}
35
36pub fn routine_version_root(proj_dirs: &ProjectDirs, name: &str, version: &str) -> PathBuf {
38 routine_root(proj_dirs, name).join(version)
39}
40
41pub fn routine_target_root(
43 proj_dirs: &ProjectDirs,
44 name: &str,
45 version: &str,
46 target_triple: &str,
47) -> PathBuf {
48 routine_version_root(proj_dirs, name, version).join(target_triple)
49}
50
51pub fn plugin_dir(
53 proj_dirs: &ProjectDirs,
54 name: &str,
55 version: &str,
56 target_triple: &str,
57) -> PathBuf {
58 routine_target_root(proj_dirs, name, version, target_triple).join("plugin")
59}
60
61pub fn descriptor_path(
63 proj_dirs: &ProjectDirs,
64 name: &str,
65 version: &str,
66 target_triple: &str,
67) -> PathBuf {
68 routine_target_root(proj_dirs, name, version, target_triple).join("descriptor.json")
69}
70
71pub fn platform_library_filename(crate_name: &str) -> String {
75 if cfg!(windows) {
76 format!("{crate_name}.dll")
77 } else if cfg!(target_os = "macos") {
78 format!("lib{crate_name}.dylib")
79 } else {
80 format!("lib{crate_name}.so")
81 }
82}
83
84pub fn host_target_triple() -> Result<String, String> {
97 let out = Command::new("rustc")
98 .args(["-vV"])
99 .stderr(Stdio::inherit())
100 .output()
101 .map_err(|e| format!("Failed to run rustc: {e}"))?;
102 if !out.status.success() {
103 return Err("rustc -vV failed".to_string());
104 }
105 let stdout = String::from_utf8_lossy(&out.stdout);
106 for line in stdout.lines() {
107 if let Some(rest) = line.strip_prefix("host: ") {
108 return Ok(rest.trim().to_string());
109 }
110 }
111 Err("Failed to parse rustc host triple".to_string())
112}
113
114pub fn installed_routine_names(proj_dirs: &ProjectDirs) -> Vec<String> {
116 let root = routines_root(proj_dirs);
117 if !root.exists() {
118 return Vec::new();
119 }
120 let mut names: Vec<String> = std::fs::read_dir(&root)
121 .into_iter()
122 .flatten()
123 .filter_map(|e| e.ok())
124 .filter(|e| e.path().is_dir())
125 .filter_map(|e| e.file_name().to_str().map(|s| s.to_string()))
126 .collect();
127 names.sort();
128 names
129}
130
131pub fn installed_versions_for_routine(proj_dirs: &ProjectDirs, name: &str) -> Vec<String> {
135 let root = routine_root(proj_dirs, name);
136 if !root.exists() {
137 return Vec::new();
138 }
139 let mut versions: Vec<String> = std::fs::read_dir(&root)
140 .into_iter()
141 .flatten()
142 .filter_map(|e| e.ok())
143 .filter(|e| e.path().is_dir())
144 .filter_map(|e| e.file_name().to_str().map(|s| s.to_string()))
145 .collect();
146 versions.sort();
147 versions
148}
149
150pub fn find_latest_installed_version(proj_dirs: &ProjectDirs, name: &str) -> Option<String> {
154 let root = routine_root(proj_dirs, name);
155 let mut versions: Vec<String> = std::fs::read_dir(&root)
156 .ok()?
157 .filter_map(|e| e.ok())
158 .filter(|e| e.path().is_dir())
159 .filter_map(|e| e.file_name().to_str().map(|s| s.to_string()))
160 .collect();
161 versions.sort();
162 versions.pop()
163}
164
165pub fn plugin_library_path(
167 proj_dirs: &ProjectDirs,
168 name: &str,
169 version: &str,
170 target_triple: &str,
171) -> PathBuf {
172 plugin_dir(proj_dirs, name, version, target_triple).join(platform_library_filename(name))
173}
174
175pub fn is_installed_for_target(
177 proj_dirs: &ProjectDirs,
178 name: &str,
179 version: &str,
180 target_triple: &str,
181) -> bool {
182 plugin_library_path(proj_dirs, name, version, target_triple).exists()
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188 use directories::ProjectDirs;
189
190 fn test_proj_dirs() -> ProjectDirs {
191 ProjectDirs::from("com", "cotis", "cotis-cli-test").expect("valid qualifier/org/app")
192 }
193
194 #[test]
195 fn platform_library_filename_matches_host() {
196 if cfg!(windows) {
197 assert_eq!(platform_library_filename("my_routine"), "my_routine.dll");
198 } else if cfg!(target_os = "macos") {
199 assert_eq!(
200 platform_library_filename("my_routine"),
201 "libmy_routine.dylib"
202 );
203 } else {
204 assert_eq!(platform_library_filename("my_routine"), "libmy_routine.so");
205 }
206 }
207
208 #[test]
209 fn descriptor_path_follows_cache_layout() {
210 let proj_dirs = test_proj_dirs();
211 let path = descriptor_path(&proj_dirs, "web_builder", "0.1.0", "x86_64-pc-windows-msvc");
212 assert!(
213 path.ends_with(
214 PathBuf::from("routines")
215 .join("web_builder")
216 .join("0.1.0")
217 .join("x86_64-pc-windows-msvc")
218 .join("descriptor.json")
219 )
220 );
221 }
222
223 #[test]
224 fn plugin_dir_is_under_target_root() {
225 let proj_dirs = test_proj_dirs();
226 let path = plugin_dir(
227 &proj_dirs,
228 "web_builder",
229 "dev",
230 "aarch64-unknown-linux-gnu",
231 );
232 assert!(
233 path.ends_with(
234 PathBuf::from("routines")
235 .join("web_builder")
236 .join("dev")
237 .join("aarch64-unknown-linux-gnu")
238 .join("plugin")
239 )
240 );
241 }
242}