Skip to main content

cli/
platform.rs

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