Skip to main content

cli/
platform.rs

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