dot_over/actions/
install.rs

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        // Deserialize the entire file as single struct
318        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        // Deserialize the entire file as single struct
369        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}