1use serde::{Deserialize, Serialize};
6use std::collections::BTreeMap;
7
8pub mod detect;
9pub mod registry;
10
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
14pub struct ProgramDep {
15 pub name: String,
17 pub detect: DetectMethod,
19 pub reason: String,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub hint: Option<String>,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub registry: Option<String>,
27 #[serde(default, skip_serializing_if = "Option::is_none")]
30 pub recipe: Option<ProgramRecipe>,
31}
32
33#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
35#[serde(untagged)]
36pub enum DetectMethod {
37 File { file: String },
39 Command { command: String },
41 Version { version: VersionCheck },
43}
44
45#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
46pub struct VersionCheck {
47 pub command: String,
49 pub min: String,
51}
52
53#[derive(Debug, Clone, PartialEq)]
55pub enum DepStatus {
56 Present,
57 Missing,
58 PresentWrongVersion { found: String },
59}
60
61pub fn current_platform() -> String {
65 format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS)
66}
67
68#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
72pub struct ProgramRecipe {
73 pub platforms: BTreeMap<String, PlatformRecipe>,
75}
76
77#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
78pub struct PlatformRecipe {
79 pub url: String,
80 pub sha256: String,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub install_to: Option<String>,
84 #[serde(default)]
85 pub executable: bool,
86 #[serde(default, skip_serializing_if = "Option::is_none")]
87 pub archive: Option<registry::ArchiveSpec>,
88}
89
90impl ProgramRecipe {
91 pub fn for_platform(&self, platform: &str) -> Option<registry::CuratedRecipe> {
95 let p = self.platforms.get(platform)?;
96 Some(registry::CuratedRecipe {
97 description: "author-declared recipe".to_string(),
98 url: p.url.clone(),
99 sha256: p.sha256.clone(),
100 install_to: p.install_to.clone(),
101 executable: p.executable,
102 archive: p.archive.clone(),
103 })
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn program_dep_parses_all_detect_methods() {
113 let y = r#"
114- name: lightpanda
115 detect: { file: "~/.mur/aura/lightpanda" }
116 reason: "render tier"
117 hint: "https://lightpanda.io/download"
118 registry: lightpanda
119- name: gh
120 detect: { command: "gh" }
121 reason: "github ops"
122- name: node
123 detect: { version: { command: "node --version", min: "18.0.0" } }
124 reason: "js runtime"
125"#;
126 let deps: Vec<ProgramDep> = serde_yaml::from_str(y).unwrap();
127 assert_eq!(deps.len(), 3);
128 assert_eq!(deps[0].name, "lightpanda");
129 assert!(
130 matches!(&deps[0].detect, DetectMethod::File { file } if file == "~/.mur/aura/lightpanda")
131 );
132 assert_eq!(deps[0].registry.as_deref(), Some("lightpanda"));
133 assert!(matches!(&deps[1].detect, DetectMethod::Command { command } if command == "gh"));
134 assert!(deps[1].hint.is_none());
135 assert!(
136 matches!(&deps[2].detect, DetectMethod::Version { version } if version.min == "18.0.0")
137 );
138 }
139
140 #[test]
141 fn current_platform_is_arch_dash_os() {
142 let p = current_platform();
143 assert!(p.contains('-'));
144 let (arch, os) = p.split_once('-').unwrap();
146 assert!(!arch.is_empty() && !os.is_empty());
147 }
148
149 #[test]
150 fn program_dep_parses_optional_recipe_and_defaults_none() {
151 let with = r#"
152name: some-tool
153detect: { command: some-tool }
154reason: "x"
155recipe:
156 platforms:
157 aarch64-macos: { url: "u", sha256: "s", install_to: "aura/some-tool", executable: true }
158"#;
159 let d: ProgramDep = serde_yaml::from_str(with).unwrap();
160 assert!(d.recipe.is_some());
161 assert!(d.recipe.unwrap().for_platform("aarch64-macos").is_some());
162
163 let without = "name: x\ndetect: { command: x }\nreason: r\n";
165 let d2: ProgramDep = serde_yaml::from_str(without).unwrap();
166 assert!(d2.recipe.is_none());
167 }
168}
169
170#[cfg(test)]
171mod recipe_tests {
172 use super::*;
173
174 #[test]
175 fn program_recipe_for_platform_converts_to_curated() {
176 let y = r#"
177platforms:
178 aarch64-macos: { url: "https://x/tool", sha256: "abc123", install_to: "aura/tool", executable: true }
179"#;
180 let r: ProgramRecipe = serde_yaml::from_str(y).unwrap();
181 let cur = r.for_platform("aarch64-macos").expect("platform present");
182 assert_eq!(cur.url, "https://x/tool");
183 assert_eq!(cur.sha256, "abc123");
184 assert_eq!(cur.install_to.as_deref(), Some("aura/tool"));
185 assert!(cur.executable);
186 assert!(cur.archive.is_none());
187 assert!(r.for_platform("sparc-solaris").is_none());
188 }
189}