tidalcycles_rs/
install.rs

1use std::cmp::Ordering;
2use std::fs;
3use std::process::Command;
4
5pub fn ensure_gh_installed() -> Option<std::path::PathBuf> {
6    if let Ok(path) = which::which("gh") {
7        Some(path)
8    } else if std::path::Path::new(r"C:\Program Files\GitHub CLI\gh.exe").exists() {
9        Some(std::path::PathBuf::from(
10            r"C:\Program Files\GitHub CLI\gh.exe",
11        ))
12    } else {
13        // Try to install GitHub CLI via winget with elevation
14        let status = Command::new("powershell")
15            .args([
16                "-Command",
17                "Start-Process winget -ArgumentList 'install --id=GitHub.cli -e --accept-source-agreements --accept-package-agreements' -Verb RunAs -Wait",
18            ])
19            .status();
20        match status {
21            Ok(s) if s.success() => {
22                if let Ok(path) = which::which("gh") {
23                    Some(path)
24                } else if std::path::Path::new(r"C:\Program Files\GitHub CLI\gh.exe").exists() {
25                    Some(std::path::PathBuf::from(
26                        r"C:\Program Files\GitHub CLI\gh.exe",
27                    ))
28                } else {
29                    None
30                }
31            }
32            _ => None,
33        }
34    }
35}
36
37pub fn ensure_supercollider_installed() -> Option<std::path::PathBuf> {
38    // Try to find sclang in PATH
39    if let Ok(path) = which::which("sclang") {
40        return Some(path);
41    }
42
43    // Try to find the latest SuperCollider in Program Files
44    let program_files =
45        std::env::var("ProgramFiles").unwrap_or_else(|_| r"C:\Program Files".to_string());
46    let sc_prefix = "SuperCollider-";
47    let mut latest_version: Option<(semver::Version, std::path::PathBuf)> = None;
48
49    if let Ok(entries) = fs::read_dir(&program_files) {
50        for entry in entries.flatten() {
51            let file_name = entry.file_name();
52            let file_name = file_name.to_string_lossy();
53            if file_name.starts_with(sc_prefix) {
54                let version_str = &file_name[sc_prefix.len()..];
55                if let Ok(version) = semver::Version::parse(version_str) {
56                    let exe_path = entry.path().join("sclang.exe");
57                    if exe_path.exists() {
58                        match &latest_version {
59                            Some((latest, _)) if version.cmp(latest) == Ordering::Greater => {
60                                latest_version = Some((version, exe_path));
61                            }
62                            None => {
63                                latest_version = Some((version, exe_path));
64                            }
65                            _ => {}
66                        }
67                    }
68                }
69            }
70        }
71    }
72
73    if let Some((_, path)) = latest_version {
74        return Some(path);
75    }
76
77    // Try to install SuperCollider via winget with elevation
78    let status = Command::new("powershell")
79        .args([
80            "-Command",
81            "Start-Process winget -ArgumentList 'install --id=SuperCollider.SuperCollider -e --accept-source-agreements --accept-package-agreements' -Verb RunAs -Wait",
82        ])
83        .status();
84
85    match status {
86        Ok(s) if s.success() => {
87            // Try again to find sclang in PATH
88            if let Ok(path) = which::which("sclang") {
89                Some(path)
90            } else {
91                // Try again to find the latest SuperCollider in Program Files
92                let mut latest_version: Option<(semver::Version, std::path::PathBuf)> = None;
93                if let Ok(entries) = fs::read_dir(&program_files) {
94                    for entry in entries.flatten() {
95                        let file_name = entry.file_name();
96                        let file_name = file_name.to_string_lossy();
97                        if file_name.starts_with(sc_prefix) {
98                            let version_str = &file_name[sc_prefix.len()..];
99                            if let Ok(version) = semver::Version::parse(version_str) {
100                                let exe_path = entry.path().join("sclang.exe");
101                                if exe_path.exists() {
102                                    match &latest_version {
103                                        Some((latest, _))
104                                            if version.cmp(latest) == Ordering::Greater =>
105                                        {
106                                            latest_version = Some((version, exe_path));
107                                        }
108                                        None => {
109                                            latest_version = Some((version, exe_path));
110                                        }
111                                        _ => {}
112                                    }
113                                }
114                            }
115                        }
116                    }
117                }
118                latest_version.map(|(_, path)| path)
119            }
120        }
121        _ => None,
122    }
123}
124
125pub fn ensure_ghcup_installed() -> Option<std::path::PathBuf> {
126    if let Ok(path) = which::which("ghcup") {
127        Some(path)
128    } else if std::path::Path::new(r"C:\ghcup\bin\ghcup.exe").exists() {
129        Some(std::path::PathBuf::from(r"C:\ghcup\bin\ghcup.exe"))
130    } else {
131        let ps_script = r#"Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; & ([ScriptBlock]::Create((Invoke-WebRequest https://www.haskell.org/ghcup/sh/bootstrap-haskell.ps1 -UseBasicParsing))) -Interactive -DisableCurl"#;
132        let status = Command::new("powershell")
133            .args([
134                "-Command",
135                &format!(
136                    "Start-Process powershell -Verb runAs -ArgumentList '{}'",
137                    ps_script.replace("'", "''")
138                ),
139            ])
140            .status();
141        match status {
142            Ok(s) if s.success() => {
143                if let Ok(path) = which::which("ghcup") {
144                    Some(path)
145                } else if std::path::Path::new(r"C:\ghcup\bin\ghcup.exe").exists() {
146                    Some(std::path::PathBuf::from(r"C:\ghcup\bin\ghcup.exe"))
147                } else {
148                    None
149                }
150            }
151            _ => None,
152        }
153    }
154}
155
156pub fn ensure_cabal_installed() -> Option<std::path::PathBuf> {
157    if let Ok(path) = which::which("cabal") {
158        Some(path)
159    } else if std::path::Path::new(r"C:\ghcup\bin\cabal.exe").exists() {
160        Some(std::path::PathBuf::from(r"C:\ghcup\bin\cabal.exe"))
161    } else {
162        // Try to install ghcup (which installs cabal) if not present
163        let _ = ensure_ghcup_installed();
164        if let Ok(path) = which::which("cabal") {
165            Some(path)
166        } else if std::path::Path::new(r"C:\ghcup\bin\cabal.exe").exists() {
167            Some(std::path::PathBuf::from(r"C:\ghcup\bin\cabal.exe"))
168        } else {
169            None
170        }
171    }
172}