crosup_core/
graph.rs

1use anyhow::Error;
2use crosup_installers::{
3    apk::ApkInstaller, apt::AptInstaller, brew::BrewInstaller, curl::CurlInstaller,
4    dnf::DnfInstaller, emerge::EmergeInstaller, fleek::FleekInstaller, git::GitInstaller,
5    home_manager::HomeManagerInstaller, nix::NixInstaller, pacman::PacmanInstaller,
6    slackpkg::SlackpkgInstaller, yum::YumInstaller, zypper::ZypperInstaller, Installer,
7};
8use crosup_macros::{
9    add_vertex, add_vertex_with_condition, convert_generic_installer, downcast_installer,
10};
11use crosup_types::{
12    configuration::Configuration,
13    curl::{default_brew_installer, default_nix_installer},
14};
15use os_release::OsRelease;
16use owo_colors::OwoColorize;
17use ssh2::Session;
18
19#[derive(Clone)]
20pub struct Vertex {
21    name: String,
22    dependencies: Vec<String>,
23    provider: String,
24    apt: Option<AptInstaller>,
25    brew: Option<BrewInstaller>,
26    curl: Option<CurlInstaller>,
27    git: Option<GitInstaller>,
28    nix: Option<NixInstaller>,
29    yum: Option<YumInstaller>,
30    dnf: Option<DnfInstaller>,
31    zypper: Option<ZypperInstaller>,
32    apk: Option<ApkInstaller>,
33    pacman: Option<PacmanInstaller>,
34    emerge: Option<EmergeInstaller>,
35    slackpkg: Option<SlackpkgInstaller>,
36    fleek: Option<FleekInstaller>,
37    home_manager: Option<HomeManagerInstaller>,
38}
39
40impl From<Box<dyn Installer + 'static>> for Vertex {
41    fn from(installer: Box<dyn Installer + 'static>) -> Self {
42        Self {
43            name: installer.name().to_string(),
44            dependencies: installer
45                .dependencies()
46                .iter()
47                .map(|x| x.to_string())
48                .collect(),
49            provider: installer.provider().to_string(),
50            apt: downcast_installer!("apt", installer, AptInstaller),
51            brew: downcast_installer!("brew", installer, BrewInstaller),
52            curl: downcast_installer!("curl", installer, CurlInstaller),
53            git: downcast_installer!("git", installer, GitInstaller),
54            nix: downcast_installer!("nix", installer, NixInstaller),
55            yum: downcast_installer!("yum", installer, YumInstaller),
56            dnf: downcast_installer!("dnf", installer, DnfInstaller),
57            zypper: downcast_installer!("zypper", installer, ZypperInstaller),
58            apk: downcast_installer!("apk", installer, ApkInstaller),
59            pacman: downcast_installer!("pacman", installer, PacmanInstaller),
60            emerge: downcast_installer!("emerge", installer, EmergeInstaller),
61            slackpkg: downcast_installer!("slackpkg", installer, SlackpkgInstaller),
62            fleek: downcast_installer!("fleek", installer, FleekInstaller),
63            home_manager: downcast_installer!("home-manager", installer, HomeManagerInstaller),
64        }
65    }
66}
67
68impl Into<Box<dyn Installer>> for Vertex {
69    fn into(self) -> Box<dyn Installer> {
70        match self.provider.as_str() {
71            "apt" => Box::new(self.apt.unwrap()),
72            "brew" => Box::new(self.brew.unwrap()),
73            "curl" => Box::new(self.curl.unwrap()),
74            "git" => Box::new(self.git.unwrap()),
75            "nix" => Box::new(self.nix.unwrap()),
76            "yum" => Box::new(self.yum.unwrap()),
77            "dnf" => Box::new(self.dnf.unwrap()),
78            "zypper" => Box::new(self.zypper.unwrap()),
79            "apk" => Box::new(self.apk.unwrap()),
80            "pacman" => Box::new(self.pacman.unwrap()),
81            "emerge" => Box::new(self.emerge.unwrap()),
82            "slackpkg" => Box::new(self.slackpkg.unwrap()),
83            "fleek" => Box::new(self.fleek.unwrap()),
84            "home-manager" => Box::new(self.home_manager.unwrap()),
85            _ => panic!("Unknown installer: {}", self.name),
86        }
87    }
88}
89
90#[derive(Clone, Debug)]
91pub struct Edge {
92    from: usize,
93    to: usize,
94}
95
96#[derive(Clone)]
97pub struct InstallerGraph {
98    vertices: Vec<Vertex>,
99    edges: Vec<Edge>,
100}
101
102impl Into<Vec<Box<dyn Installer>>> for InstallerGraph {
103    fn into(self) -> Vec<Box<dyn Installer>> {
104        self.vertices.into_iter().map(|x| x.into()).collect()
105    }
106}
107
108pub fn autodetect_installer(config: &mut Configuration) {
109    if let Some(generic_install) = &config.install {
110        // detect linux
111        if cfg!(target_os = "linux") {
112            // determine linux distribution using os-release
113            if let Ok(os_release) = OsRelease::new() {
114                let os = os_release.id.to_lowercase();
115                let os = os.as_str();
116
117                let package_manager = match os {
118                    "ubuntu" | "debian" | "linuxmint" | "pop" | "elementary" | "zorin" => {
119                        convert_generic_installer!(config, generic_install, apt);
120                        "apt-get"
121                    }
122                    "fedora" | "centos" | "rhel" | "rocky" | "amazon" => {
123                        convert_generic_installer!(config, generic_install, dnf);
124                        "dnf"
125                    }
126                    "opensuse" | "sles" => {
127                        convert_generic_installer!(config, generic_install, zypper);
128                        "zypper"
129                    }
130                    "arch" | "manjaro" => {
131                        convert_generic_installer!(config, generic_install, pacman);
132                        "pacman"
133                    }
134                    "gentoo" => {
135                        convert_generic_installer!(config, generic_install, emerge);
136                        "emerge"
137                    }
138                    "alpine" => {
139                        convert_generic_installer!(config, generic_install, apk);
140                        "apk"
141                    }
142                    "slackware" => {
143                        convert_generic_installer!(config, generic_install, slackpkg);
144                        "slackpkg"
145                    }
146                    _ => panic!("Unsupported OS: {}", os),
147                };
148
149                let os_pretty = os_release.pretty_name;
150                println!("-> Detected OS:🐧 {}", os_pretty.magenta());
151                println!(
152                    "-> Using package manager: 📦 {}",
153                    package_manager.bright_green()
154                );
155            }
156        }
157        if cfg!(target_os = "macos") {
158            println!("-> Detected OS: 🍎 macOS");
159            println!("-> Using package manager: 📦 {}", "brew".bright_green());
160            convert_generic_installer!(config, generic_install, brew);
161        }
162    }
163}
164
165pub fn build_installer_graph(
166    config: &mut Configuration,
167    session: Option<Session>,
168) -> (InstallerGraph, Vec<Box<dyn Installer>>) {
169    let mut graph = InstallerGraph::new();
170
171    if config.clone().nix.is_some()
172        || config.clone().fleek.is_some()
173        || config.clone().packages.is_some()
174    {
175        if let Some(curl) = config.clone().curl {
176            if !curl.into_iter().any(|(_, y)| y.script.contains_key("nix")) {
177                let nix = default_nix_installer();
178                graph.add_vertex(Vertex::from(Box::new(CurlInstaller {
179                    name: nix.name.clone(),
180                    session: session.clone(),
181                    ..CurlInstaller::from(nix.clone())
182                }) as Box<dyn Installer>));
183            }
184        } else {
185            let nix = default_nix_installer();
186            graph.add_vertex(Vertex::from(Box::new(CurlInstaller {
187                name: nix.name.clone(),
188                session: session.clone(),
189                ..CurlInstaller::from(nix.clone())
190            }) as Box<dyn Installer>));
191        }
192    }
193
194    if config.clone().brew.is_some() {
195        if let Some(curl) = config.clone().curl {
196            if !curl.into_iter().any(|(_, y)| y.script.contains_key("brew")) {
197                let brew = default_brew_installer();
198                graph.add_vertex(Vertex::from(Box::new(CurlInstaller {
199                    name: brew.name.clone(),
200                    session: session.clone(),
201                    ..CurlInstaller::from(brew.clone())
202                }) as Box<dyn Installer>));
203            }
204        } else {
205            let brew = default_brew_installer();
206            graph.add_vertex(Vertex::from(Box::new(CurlInstaller {
207                name: brew.name.clone(),
208                session: session.clone(),
209                ..CurlInstaller::from(brew.clone())
210            }) as Box<dyn Installer>));
211        }
212    }
213
214    autodetect_installer(config);
215
216    if cfg!(target_os = "linux") {
217        // determine linux distribution using os-release
218        if let Ok(os_release) = OsRelease::new() {
219            let os = os_release.id.to_lowercase();
220            let os = os.as_str();
221
222            match os {
223                "ubuntu" | "debian" | "linuxmint" | "pop" | "elementary" | "zorin" => {
224                    add_vertex!(graph, AptInstaller, config, apt, pkg, session);
225                }
226                _ => {}
227            };
228        }
229    }
230
231    add_vertex!(graph, CurlInstaller, config, curl, script, session);
232    add_vertex!(graph, GitInstaller, config, git, repo, session);
233    add_vertex!(graph, NixInstaller, config, nix, pkg, session);
234    add_vertex!(graph, YumInstaller, config, yum, pkg, session);
235    add_vertex!(graph, DnfInstaller, config, dnf, pkg, session);
236    add_vertex!(graph, ZypperInstaller, config, zypper, pkg, session);
237    add_vertex!(graph, ApkInstaller, config, apk, pkg, session);
238    add_vertex!(graph, PacmanInstaller, config, pacman, pkg, session);
239    add_vertex!(graph, EmergeInstaller, config, emerge, pkg, session);
240    add_vertex!(graph, SlackpkgInstaller, config, slackpkg, pkg, session);
241    add_vertex!(graph, FleekInstaller, config, fleek, pkg, session);
242    add_vertex_with_condition!(graph, BrewInstaller, config, brew, pkg, session);
243
244    if let Some(package) = config.clone().packages {
245        package.iter().for_each(|name| {
246            graph.add_vertex(Vertex::from(Box::new(HomeManagerInstaller {
247                name: name.clone(),
248                session: session.clone(),
249                provider: "home-manager".into(),
250                packages: Some(vec![name.clone()]),
251                dependencies: vec!["nix".into()],
252                ..Default::default()
253            }) as Box<dyn Installer>));
254        });
255    }
256
257    setup_dependencies(&mut graph);
258
259    (graph.clone(), graph.into())
260}
261
262fn setup_dependencies(graph: &mut InstallerGraph) {
263    let mut edges = vec![];
264
265    for (i, vertex) in graph.vertices.iter().enumerate() {
266        for dependency in vertex.dependencies.iter() {
267            if let Some(j) = graph.vertices.iter().position(|x| x.name == *dependency) {
268                edges.push(Edge { from: i, to: j });
269            }
270        }
271    }
272
273    graph.edges = edges;
274}
275
276impl InstallerGraph {
277    pub fn new() -> Self {
278        Self {
279            vertices: vec![],
280            edges: vec![],
281        }
282    }
283
284    pub fn add_vertex(&mut self, vertex: Vertex) -> usize {
285        self.vertices.push(vertex);
286        self.vertices.len() - 1
287    }
288
289    pub fn add_edge(&mut self, from: usize, to: usize) {
290        self.edges.push(Edge { from, to });
291    }
292
293    pub fn contains(&self, name: &str) -> bool {
294        self.vertices.iter().any(|x| x.name == name)
295    }
296
297    pub fn install_all(&self) -> Result<(), Error> {
298        let mut visited = vec![false; self.vertices.len()];
299
300        for (index, vertex) in self.vertices.iter().enumerate() {
301            if !visited[index] {
302                self.install(vertex.clone().into(), &mut visited)?;
303            }
304        }
305
306        Ok(())
307    }
308
309    pub fn install(
310        &self,
311        package: Box<dyn Installer>,
312        visited: &mut Vec<bool>,
313    ) -> Result<(), Error> {
314        let index = self
315            .vertices
316            .iter()
317            .position(|x| x.name == package.name())
318            .unwrap();
319
320        if visited[index] {
321            return Ok(());
322        }
323
324        for edge in self.edges.iter().filter(|x| x.from == index) {
325            self.install(self.vertices[edge.to].clone().into(), visited)?;
326        }
327
328        package.install()?;
329
330        visited[index] = true;
331
332        Ok(())
333    }
334
335    pub fn size(&self) -> usize {
336        self.vertices.len()
337    }
338}