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