Skip to main content

cli/
platform.rs

1use anyhow::{Result, bail};
2use std::path::{Path, PathBuf};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
5pub(crate) enum OperatingSystem {
6    Macos,
7    Linux,
8    Windows,
9}
10
11impl OperatingSystem {
12    pub(crate) const ALL: [Self; 3] = [Self::Macos, Self::Linux, Self::Windows];
13
14    pub(crate) const fn as_str(self) -> &'static str {
15        match self {
16            Self::Macos => "macos",
17            Self::Linux => "linux",
18            Self::Windows => "windows",
19        }
20    }
21
22    pub(crate) const fn is_unix(self) -> bool {
23        matches!(self, Self::Macos | Self::Linux)
24    }
25}
26
27pub fn executable_name_for_os(os: &str) -> &'static str {
28    if os == "windows" {
29        "shine.exe"
30    } else {
31        "shine"
32    }
33}
34
35pub fn current_executable_name() -> &'static str {
36    executable_name_for_os(std::env::consts::OS)
37}
38
39pub(crate) fn current_platform() -> OperatingSystem {
40    match std::env::consts::OS {
41        "macos" => OperatingSystem::Macos,
42        "linux" => OperatingSystem::Linux,
43        "windows" => OperatingSystem::Windows,
44        other => panic!("unsupported operating system: {other}"),
45    }
46}
47
48pub fn release_target(os: &str, arch: &str) -> Result<String> {
49    let normalized_os = match os {
50        "macos" => "darwin",
51        "linux" => "linux",
52        "windows" => "windows",
53        other => bail!("unsupported operating system: {other}"),
54    };
55
56    let normalized_arch = match arch {
57        "x86_64" => "x86_64",
58        "aarch64" => "aarch64",
59        other => bail!("unsupported architecture: {other}"),
60    };
61
62    Ok(format!("{normalized_os}-{normalized_arch}"))
63}
64
65pub fn default_self_install_dest() -> Result<PathBuf> {
66    default_self_install_dest_for(
67        std::env::consts::OS,
68        std::env::var_os("LOCALAPPDATA").map(PathBuf::from),
69    )
70}
71
72pub fn default_self_install_dest_for(os: &str, local_appdata: Option<PathBuf>) -> Result<PathBuf> {
73    match os {
74        "windows" => {
75            let Some(local_appdata) = local_appdata else {
76                bail!("LOCALAPPDATA is not set; pass --dest <PATH> to choose an install location");
77            };
78            Ok(local_appdata
79                .join("Programs")
80                .join("shine")
81                .join(executable_name_for_os(os)))
82        }
83        _ => Ok(PathBuf::from("/usr/local/bin").join(executable_name_for_os(os))),
84    }
85}
86
87/// True when `command` resolves to a file on the current `PATH`. On Windows the
88/// common executable extensions (`.exe`, `.cmd`, `.bat`) are also probed. Used for
89/// diagnostic "is this runtime available?" hints, not as an execution gate.
90pub fn command_exists_on_path(command: &str) -> bool {
91    let var_name = if cfg!(windows) { "Path" } else { "PATH" };
92    let Some(path_value) = std::env::var_os(var_name) else {
93        return false;
94    };
95    let candidates: Vec<String> = if cfg!(windows) {
96        vec![
97            command.to_string(),
98            format!("{command}.exe"),
99            format!("{command}.cmd"),
100            format!("{command}.bat"),
101        ]
102    } else {
103        vec![command.to_string()]
104    };
105    std::env::split_paths(&path_value)
106        .any(|dir| candidates.iter().any(|name| dir.join(name).is_file()))
107}
108
109pub fn current_path_contains_dir(dir: &Path) -> bool {
110    let var_name = if cfg!(windows) { "Path" } else { "PATH" };
111    let Some(path_value) = std::env::var_os(var_name) else {
112        return false;
113    };
114    std::env::split_paths(&path_value).any(|entry| paths_equivalent(&entry, dir))
115}
116
117pub fn path_install_hint(dir: &Path) -> String {
118    if cfg!(windows) {
119        let escaped = dir.display().to_string().replace('\'', "''");
120        format!(
121            "Add it to your user PATH, for example: [Environment]::SetEnvironmentVariable('Path', [Environment]::GetEnvironmentVariable('Path', 'User') + ';{escaped}', 'User')"
122        )
123    } else {
124        format!(
125            "Add it to PATH, for example: export PATH=\"{}:$PATH\"",
126            dir.display()
127        )
128    }
129}
130
131fn paths_equivalent(left: &Path, right: &Path) -> bool {
132    let left = normalize_for_compare(left);
133    let right = normalize_for_compare(right);
134    if cfg!(windows) {
135        left.eq_ignore_ascii_case(&right)
136    } else {
137        left == right
138    }
139}
140
141fn normalize_for_compare(path: &Path) -> String {
142    path.to_string_lossy()
143        .trim_end_matches(std::path::MAIN_SEPARATOR)
144        .to_string()
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn release_target_maps_supported_platforms() {
153        assert_eq!(
154            release_target("macos", "aarch64").unwrap(),
155            "darwin-aarch64"
156        );
157        assert_eq!(release_target("linux", "x86_64").unwrap(), "linux-x86_64");
158        assert_eq!(
159            release_target("windows", "x86_64").unwrap(),
160            "windows-x86_64"
161        );
162        assert_eq!(
163            release_target("windows", "aarch64").unwrap(),
164            "windows-aarch64"
165        );
166    }
167
168    #[test]
169    fn operating_system_names_and_unix_group_are_stable() {
170        assert_eq!(OperatingSystem::Macos.as_str(), "macos");
171        assert_eq!(OperatingSystem::Linux.as_str(), "linux");
172        assert_eq!(OperatingSystem::Windows.as_str(), "windows");
173        assert!(OperatingSystem::Macos.is_unix());
174        assert!(OperatingSystem::Linux.is_unix());
175        assert!(!OperatingSystem::Windows.is_unix());
176    }
177
178    #[test]
179    fn executable_name_is_platform_specific() {
180        assert_eq!(executable_name_for_os("linux"), "shine");
181        assert_eq!(executable_name_for_os("macos"), "shine");
182        assert_eq!(executable_name_for_os("windows"), "shine.exe");
183    }
184
185    #[test]
186    fn default_self_install_dest_is_platform_specific() {
187        assert_eq!(
188            default_self_install_dest_for("linux", None).unwrap(),
189            PathBuf::from("/usr/local/bin/shine")
190        );
191        assert_eq!(
192            default_self_install_dest_for(
193                "windows",
194                Some(PathBuf::from(r"C:\Users\me\AppData\Local"))
195            )
196            .unwrap(),
197            PathBuf::from(r"C:\Users\me\AppData\Local")
198                .join("Programs")
199                .join("shine")
200                .join("shine.exe")
201        );
202    }
203
204    #[test]
205    fn windows_default_requires_local_appdata() {
206        let err = default_self_install_dest_for("windows", None).unwrap_err();
207        assert!(err.to_string().contains("LOCALAPPDATA"));
208    }
209}