1use std::{collections::HashSet, env::consts::OS};
2
3use crate::{exec::Ctx, overlays::Overlay};
4use anyhow::Result;
5use serde::{Deserialize, Serialize};
6use which::which;
7
8#[cfg(test)]
9use mockall::{automock, mock, predicate::*};
10
11pub async fn install(ctx: &Ctx, overlay: &Overlay) -> Result<()> {
12 let finder = WhichFinder;
13 match OS {
14 "linux" => install_linux(ctx, overlay, finder).await?,
15 "macos" => install_macos(ctx, overlay, finder).await?,
16 "windows" => install_windows(ctx, overlay, finder).await?,
17 _ => println!("Unsupported OS: {}", OS),
18 }
19 Ok(())
20}
21
22#[cfg_attr(test, automock)]
23trait BinFinder {
24 fn find_first(&self, bins: Vec<String>) -> Option<String>;
25}
26
27struct WhichFinder;
28
29impl BinFinder for WhichFinder {
30 fn find_first(&self, bins: Vec<String>) -> Option<String> {
31 bins.into_iter().find(|bin| which(bin).is_ok())
32 }
33}
34
35
36#[cfg_attr(test, automock)]
37trait Installer {
38 async fn install(&self, pkgs: Vec<String>) -> Result<()>;
39}
40
41struct ArchInstaller;
42
43struct AptInstaller;
44
45struct BrewInstaller;
46
47
48async fn install_windows<T: BinFinder>(ctx: &Ctx, overlay: &Overlay, finder: T) -> Result<()> {
49 todo!();
50 Ok(())
51}
52
53async fn install_macos<T: BinFinder>(ctx: &Ctx, overlay: &Overlay, finder: T) -> Result<()> {
54 if let Ok(brew) = which("brew") {
55 println!("brew is installed at {}", brew.display());
56 } else {
57 println!("brew is not installed");
58 }
59 Ok(())
60}
61
62async fn install_linux<T: BinFinder>(ctx: &Ctx, overlay: &Overlay, finder: T) -> Result<()> {
63 match finder.find_first(vec![
64 "yay".to_string(),
65 "paru".to_string(),
66 "pacman".to_string(),
67 "apt".to_string(),
68 ]) {
69 Some(bin) => match bin.as_str() {
70 "yay" | "paru" | "pacman" => install_arch(ctx, overlay, &bin).await?,
71 "apt" => install_apt(ctx, overlay, &bin).await?,
72 _ => println!("Unknown bin {}", bin),
73 },
74 None => println!("No package manager found"),
75 }
76 let pkgs = get_archlinux_packages(ctx, overlay).await;
77 println!("{:?}", pkgs);
78 Ok(())
79}
80
81async fn install_apt(ctx: &Ctx, overlay: &Overlay, bin: &str) -> Result<()> {
82 println!("Installer for apt using {}", bin);
83 todo!();
84 Ok(())
85}
86
87async fn install_arch(ctx: &Ctx, overlay: &Overlay, bin: &str) -> Result<()> {
88 println!("Installer for Archlinux using {}", bin);
89 todo!();
90 Ok(())
91}
92
93async fn get_archlinux_packages(ctx: &Ctx, overlay: &Overlay) -> HashSet<String> {
94 let mut packages: HashSet<String> = HashSet::new();
95 if let Some(install) = &overlay.install {
96 if let Some(archlinux) = &install.archlinux {
97 packages.extend(archlinux.packages.iter().cloned());
98 }
99 }
100 if let Some(uses) = &overlay.uses {
101 for name in uses {
102 let used = ctx.repository.get(name).expect("failed");
103 if ctx.debug {
104 println!("{:#?}", overlay);
105 }
106 packages = packages
107 .union(&Box::pin(get_archlinux_packages(ctx, &used)).await)
108 .cloned()
109 .collect();
110 }
111 }
112 packages
113}
114
115#[derive(Debug, Deserialize, Serialize, Clone)]
116#[serde(from = "AllAptForms")]
117pub struct AptConfig {
118 pub packages: Vec<String>,
119}
120
121impl From<AllAptForms> for AptConfig {
122 fn from(f: AllAptForms) -> Self {
123 match f {
124 AllAptForms::Flat(packages) => Self { packages },
125 AllAptForms::Full { packages } => Self { packages },
126 }
127 }
128}
129
130#[derive(Debug, Deserialize)]
131#[serde(untagged)]
132pub enum AllAptForms {
133 Flat(Vec<String>),
134 Full { packages: Vec<String> },
135}
136
137#[derive(Debug, Deserialize, Serialize, Clone)]
138#[serde(from = "AllArchlinuxForms")]
139pub struct ArchlinuxConfig {
140 pub packages: Vec<String>,
141}
142
143impl From<AllArchlinuxForms> for ArchlinuxConfig {
144 fn from(f: AllArchlinuxForms) -> Self {
145 match f {
146 AllArchlinuxForms::Flat(packages) => Self { packages },
147 AllArchlinuxForms::Full { packages } => Self { packages },
148 }
149 }
150}
151
152#[derive(Debug, Deserialize)]
153#[serde(untagged)]
154pub enum AllArchlinuxForms {
155 Flat(Vec<String>),
156 Full { packages: Vec<String> },
157}
158
159#[derive(Debug, Deserialize, Serialize, Clone)]
160#[serde(from = "AllBrewForms")]
161pub struct BrewConfig {
162 pub taps: Option<Vec<String>>,
163 pub packages: Option<Vec<BrewPackage>>,
164}
165
166impl From<AllBrewForms> for BrewConfig {
167 fn from(f: AllBrewForms) -> Self {
168 match f {
169 AllBrewForms::Flat(packages) => Self {
170 taps: None,
171 packages: Some(
172 packages
173 .into_iter()
174 .map(|name| BrewPackage {
175 name,
176 options: None,
177 })
178 .collect(),
179 ),
180 },
181 AllBrewForms::Full { taps, packages } => Self { taps, packages },
182 }
183 }
184}
185
186#[derive(Debug, Deserialize)]
187#[serde(untagged)]
188pub enum AllBrewForms {
189 Flat(Vec<String>),
190 Full {
191 taps: Option<Vec<String>>,
192 packages: Option<Vec<BrewPackage>>,
193 },
194}
195
196#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)]
197#[serde(from = "AllBrewPackageForms")]
198pub struct BrewPackage {
199 pub name: String,
200 pub options: Option<String>,
201}
202
203impl From<AllBrewPackageForms> for BrewPackage {
204 fn from(f: AllBrewPackageForms) -> Self {
205 match f {
206 AllBrewPackageForms::Str(name) => Self {
207 name,
208 options: None,
209 },
210 AllBrewPackageForms::Full { name, options } => Self { name, options },
211 }
212 }
213}
214
215#[derive(Debug, Deserialize, Clone)]
216#[serde(untagged)]
217pub enum AllBrewPackageForms {
218 Str(String),
219 Full {
220 name: String,
221 options: Option<String>,
222 },
223}
224
225#[derive(Debug, Deserialize, Serialize, Clone)]
226pub struct InstallConfig {
227 pub pre: Option<Vec<String>>,
228 pub apt: Option<AptConfig>,
229 pub archlinux: Option<ArchlinuxConfig>,
230 pub brew: Option<BrewConfig>,
231 pub post: Option<Vec<String>>,
232}
233
234#[cfg(test)]
235mod tests {
236 use config::{Config, File, FileFormat};
237 use pretty_assertions::assert_eq;
238 use rstest::rstest;
239
240 use super::*;
241
242 #[derive(Debug, Deserialize)]
243 pub struct Data {
244 pub install: Option<InstallConfig>,
245 }
246
247 #[rstest]
248 #[case::basic_yaml(
249 FileFormat::Yaml,
250 r#"
251install:
252 pre:
253 - echo "Hello, World!"
254 archlinux:
255 - pkg1
256 - pkg2
257 apt:
258 - pkg1
259 - pkg2
260 brew:
261 - pkg1
262 - pkg2
263 post:
264 - echo "Goodbye, World!"
265"#
266 )]
267 #[case::full_yaml(
268 FileFormat::Yaml,
269 r#"
270install:
271 pre:
272 - echo "Hello, World!"
273 archlinux:
274 packages:
275 - pkg1
276 - pkg2
277 apt:
278 packages:
279 - pkg1
280 - pkg2
281 brew:
282 packages:
283 - pkg1
284 - pkg2
285 post:
286 - echo "Goodbye, World!"
287"#
288 )]
289 #[case::basic_toml(
290 FileFormat::Toml,
291 r#"
292[install]
293pre = ['echo "Hello, World!"']
294archlinux = ["pkg1", "pkg2"]
295apt = ["pkg1", "pkg2"]
296brew = ["pkg1", "pkg2"]
297post = ['echo "Goodbye, World!"']
298"#
299 )]
300 #[case::full_toml(
301 FileFormat::Toml,
302 r#"
303[install]
304pre = ['echo "Hello, World!"']
305archlinux.packages = ["pkg1", "pkg2"]
306apt.packages = ["pkg1", "pkg2"]
307brew.packages = ["pkg1", "pkg2"]
308post = ['echo "Goodbye, World!"']
309"#
310 )]
311 fn test_config(#[case] format: FileFormat, #[case] content: &str) {
312 let c = Config::builder()
313 .add_source(File::from_str(content, format))
314 .build()
315 .unwrap();
316
317 let data: Data = c.try_deserialize().unwrap();
319 let install = data.install.unwrap();
320
321 assert_eq!(install.pre.unwrap()[0], "echo \"Hello, World!\"");
322 assert_eq!(install.archlinux.unwrap().packages, ["pkg1", "pkg2"]);
323 assert_eq!(install.apt.unwrap().packages, ["pkg1", "pkg2"]);
324 assert_eq!(
325 install.brew.unwrap().packages.unwrap(),
326 vec![
327 BrewPackage {
328 name: String::from("pkg1"),
329 options: None,
330 },
331 BrewPackage {
332 name: String::from("pkg2"),
333 options: None,
334 },
335 ]
336 );
337 assert_eq!(install.post.unwrap()[0], "echo \"Goodbye, World!\"");
338 }
339
340 #[rstest]
341 #[case::yaml(
342 FileFormat::Yaml,
343 r#"
344install:
345 brew:
346 taps:
347 - my/repo
348 packages:
349 - pkg1
350 - name: pkg2
351 options: '--cask'
352"#
353 )]
354 #[case::toml(
355 FileFormat::Toml,
356 r#"
357[install]
358brew.taps = ["my/repo"]
359brew.packages = ["pkg1", {name="pkg2", options="--cask"}]
360"#
361 )]
362 fn test_brew_config(#[case] format: FileFormat, #[case] content: &str) {
363 let c = Config::builder()
364 .add_source(File::from_str(content, format))
365 .build()
366 .unwrap();
367
368 let data: Data = c.try_deserialize().unwrap();
370 let brew = data.install.unwrap().brew.unwrap();
371
372 assert_eq!(brew.taps.unwrap(), vec!["my/repo"]);
373 assert_eq!(
374 brew.packages.unwrap(),
375 vec![
376 BrewPackage {
377 name: String::from("pkg1"),
378 options: None,
379 },
380 BrewPackage {
381 name: String::from("pkg2"),
382 options: Some(String::from("--cask")),
383 },
384 ]
385 );
386 }
387}