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