Skip to main content

cuenv_homebrew/
formula.rs

1//! Homebrew formula generation.
2//!
3//! Generates Ruby formula files for Homebrew from release artifacts.
4
5use cuenv_release::Target;
6use std::collections::HashMap;
7
8/// Binary information for a platform.
9#[derive(Debug, Clone)]
10pub struct BinaryInfo {
11    /// Download URL
12    pub url: String,
13    /// SHA256 checksum
14    pub sha256: String,
15}
16
17/// Data for generating a Homebrew formula.
18#[derive(Debug, Clone)]
19pub struct FormulaData {
20    /// Formula class name (e.g., "Cuenv")
21    pub class_name: String,
22    /// Description
23    pub desc: String,
24    /// Homepage URL
25    pub homepage: String,
26    /// License identifier
27    pub license: String,
28    /// Version
29    pub version: String,
30    /// Binary info per target
31    pub binaries: HashMap<Target, BinaryInfo>,
32}
33
34/// Homebrew formula generator.
35pub struct FormulaGenerator;
36
37impl FormulaGenerator {
38    /// Generates a Ruby formula from the data.
39    #[must_use]
40    #[allow(clippy::format_push_string, clippy::too_many_lines)]
41    pub fn generate(data: &FormulaData) -> String {
42        let mut formula = format!(
43            r#"class {} < Formula
44  desc "{}"
45  homepage "{}"
46  version "{}"
47  license "{}"
48"#,
49            data.class_name, data.desc, data.homepage, data.version, data.license
50        );
51
52        // macOS section (Apple Silicon only)
53        formula.push_str("\n  on_macos do\n");
54        if let Some(info) = data.binaries.get(&Target::DarwinArm64) {
55            formula.push_str(&format!(
56                r#"    on_arm do
57      url "{}"
58      sha256 "{}"
59    end
60"#,
61                info.url, info.sha256
62            ));
63        }
64        formula.push_str("  end\n");
65
66        // Linux section
67        formula.push_str("\n  on_linux do\n");
68        if let Some(info) = data.binaries.get(&Target::LinuxArm64) {
69            formula.push_str(&format!(
70                r#"    on_arm do
71      url "{}"
72      sha256 "{}"
73    end
74"#,
75                info.url, info.sha256
76            ));
77        }
78        if let Some(info) = data.binaries.get(&Target::LinuxX64) {
79            formula.push_str(&format!(
80                r#"    on_intel do
81      url "{}"
82      sha256 "{}"
83    end
84"#,
85                info.url, info.sha256
86            ));
87        }
88        formula.push_str("  end\n");
89
90        // Install and test sections
91        let binary_name = data.class_name.to_lowercase();
92        formula.push_str("\n  def install\n");
93        formula.push_str(&format!("    bin.install \"{binary_name}\"\n"));
94        formula.push_str("  end\n\n");
95        formula.push_str("  test do\n");
96        // Ruby string interpolation: #{bin} - we need literal #{ in the output
97        formula.push_str(&format!(
98            "    assert_match version.to_s, shell_output(\"#{{bin}}/{binary_name} --version\")\n"
99        ));
100        formula.push_str("  end\nend\n");
101
102        formula
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn test_generate_formula() {
112        let mut binaries = HashMap::new();
113        binaries.insert(
114            Target::DarwinArm64,
115            BinaryInfo {
116                url: "https://example.com/darwin-arm64.tar.gz".to_string(),
117                sha256: "abc123".to_string(),
118            },
119        );
120        binaries.insert(
121            Target::LinuxX64,
122            BinaryInfo {
123                url: "https://example.com/linux-x64.tar.gz".to_string(),
124                sha256: "def456".to_string(),
125            },
126        );
127
128        let data = FormulaData {
129            class_name: "Cuenv".to_string(),
130            desc: "Test description".to_string(),
131            homepage: "https://github.com/cuenv/cuenv".to_string(),
132            license: "AGPL-3.0-or-later".to_string(),
133            version: "0.16.0".to_string(),
134            binaries,
135        };
136
137        let formula = FormulaGenerator::generate(&data);
138
139        assert!(formula.contains("class Cuenv < Formula"));
140        assert!(formula.contains("version \"0.16.0\""));
141        assert!(formula.contains("on_macos do"));
142        assert!(formula.contains("on_linux do"));
143        assert!(formula.contains("sha256 \"abc123\""));
144    }
145
146    #[test]
147    fn test_binary_info_clone() {
148        let info = BinaryInfo {
149            url: "https://example.com/binary.tar.gz".to_string(),
150            sha256: "abc123def456".to_string(),
151        };
152        let cloned = info.clone();
153        assert_eq!(info.url, cloned.url);
154        assert_eq!(info.sha256, cloned.sha256);
155    }
156
157    #[test]
158    fn test_binary_info_debug() {
159        let info = BinaryInfo {
160            url: "https://example.com/binary.tar.gz".to_string(),
161            sha256: "abc123".to_string(),
162        };
163        let debug_str = format!("{info:?}");
164        assert!(debug_str.contains("BinaryInfo"));
165        assert!(debug_str.contains("https://example.com"));
166        assert!(debug_str.contains("abc123"));
167    }
168
169    #[test]
170    fn test_formula_data_clone() {
171        let data = FormulaData {
172            class_name: "Test".to_string(),
173            desc: "Test desc".to_string(),
174            homepage: "https://test.com".to_string(),
175            license: "MIT".to_string(),
176            version: "1.0.0".to_string(),
177            binaries: HashMap::new(),
178        };
179        let cloned = data.clone();
180        assert_eq!(data.class_name, cloned.class_name);
181        assert_eq!(data.version, cloned.version);
182    }
183
184    #[test]
185    fn test_formula_data_debug() {
186        let data = FormulaData {
187            class_name: "Myapp".to_string(),
188            desc: "My app".to_string(),
189            homepage: "https://myapp.com".to_string(),
190            license: "Apache-2.0".to_string(),
191            version: "2.1.0".to_string(),
192            binaries: HashMap::new(),
193        };
194        let debug_str = format!("{data:?}");
195        assert!(debug_str.contains("FormulaData"));
196        assert!(debug_str.contains("Myapp"));
197    }
198
199    #[test]
200    fn test_generate_formula_linux_arm64() {
201        let mut binaries = HashMap::new();
202        binaries.insert(
203            Target::LinuxArm64,
204            BinaryInfo {
205                url: "https://example.com/linux-arm64.tar.gz".to_string(),
206                sha256: "arm64hash".to_string(),
207            },
208        );
209
210        let data = FormulaData {
211            class_name: "Test".to_string(),
212            desc: "Test".to_string(),
213            homepage: "https://test.com".to_string(),
214            license: "MIT".to_string(),
215            version: "1.0.0".to_string(),
216            binaries,
217        };
218
219        let formula = FormulaGenerator::generate(&data);
220        assert!(formula.contains("on_arm do"));
221        assert!(formula.contains("arm64hash"));
222    }
223
224    #[test]
225    fn test_generate_formula_all_platforms() {
226        let mut binaries = HashMap::new();
227        binaries.insert(
228            Target::DarwinArm64,
229            BinaryInfo {
230                url: "https://example.com/darwin-arm64.tar.gz".to_string(),
231                sha256: "darwin_arm_hash".to_string(),
232            },
233        );
234        binaries.insert(
235            Target::LinuxArm64,
236            BinaryInfo {
237                url: "https://example.com/linux-arm64.tar.gz".to_string(),
238                sha256: "linux_arm_hash".to_string(),
239            },
240        );
241        binaries.insert(
242            Target::LinuxX64,
243            BinaryInfo {
244                url: "https://example.com/linux-x64.tar.gz".to_string(),
245                sha256: "linux_x64_hash".to_string(),
246            },
247        );
248
249        let data = FormulaData {
250            class_name: "Multiplatform".to_string(),
251            desc: "Multi-platform app".to_string(),
252            homepage: "https://example.com".to_string(),
253            license: "BSD-3-Clause".to_string(),
254            version: "3.2.1".to_string(),
255            binaries,
256        };
257
258        let formula = FormulaGenerator::generate(&data);
259        assert!(formula.contains("darwin_arm_hash"));
260        assert!(formula.contains("linux_arm_hash"));
261        assert!(formula.contains("linux_x64_hash"));
262        assert!(formula.contains("on_intel do"));
263    }
264
265    #[test]
266    fn test_generate_formula_install_section() {
267        let data = FormulaData {
268            class_name: "Myapp".to_string(),
269            desc: "desc".to_string(),
270            homepage: "https://x.com".to_string(),
271            license: "MIT".to_string(),
272            version: "1.0.0".to_string(),
273            binaries: HashMap::new(),
274        };
275
276        let formula = FormulaGenerator::generate(&data);
277        assert!(formula.contains("def install"));
278        assert!(formula.contains("bin.install \"myapp\""));
279    }
280
281    #[test]
282    fn test_generate_formula_test_section() {
283        let data = FormulaData {
284            class_name: "Cuenv".to_string(),
285            desc: "desc".to_string(),
286            homepage: "https://x.com".to_string(),
287            license: "MIT".to_string(),
288            version: "1.0.0".to_string(),
289            binaries: HashMap::new(),
290        };
291
292        let formula = FormulaGenerator::generate(&data);
293        assert!(formula.contains("test do"));
294        assert!(formula.contains("assert_match version.to_s"));
295        assert!(formula.contains("cuenv --version"));
296    }
297
298    #[test]
299    fn test_generate_formula_empty_binaries() {
300        let data = FormulaData {
301            class_name: "Empty".to_string(),
302            desc: "No binaries".to_string(),
303            homepage: "https://empty.com".to_string(),
304            license: "GPL-3.0".to_string(),
305            version: "0.0.1".to_string(),
306            binaries: HashMap::new(),
307        };
308
309        let formula = FormulaGenerator::generate(&data);
310        // Should still generate valid structure even with no binaries
311        assert!(formula.contains("class Empty < Formula"));
312        assert!(formula.contains("on_macos do"));
313        assert!(formula.contains("on_linux do"));
314        assert!(formula.ends_with("end\n"));
315    }
316
317    #[test]
318    fn test_formula_special_characters_in_desc() {
319        let data = FormulaData {
320            class_name: "Test".to_string(),
321            desc: "App with special chars: &, <, >, quotes".to_string(),
322            homepage: "https://test.com".to_string(),
323            license: "MIT".to_string(),
324            version: "1.0.0".to_string(),
325            binaries: HashMap::new(),
326        };
327
328        let formula = FormulaGenerator::generate(&data);
329        assert!(formula.contains("App with special chars"));
330    }
331}