tidalcycles_rs/
install.rs

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