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
87pub 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}