tempo_ext/installer/
platform.rs1use std::{
4 env, fs, io,
5 path::{Path, PathBuf},
6};
7
8use crate::installer::error::InstallerError;
9
10pub(super) fn platform_binary_name(extension: &str) -> String {
12 let (os, arch) = platform_tuple();
13 format!("tempo-{extension}-{os}-{arch}")
14}
15
16fn platform_tuple() -> (&'static str, &'static str) {
17 let os = if cfg!(target_os = "macos") {
18 "darwin"
19 } else if cfg!(target_os = "linux") {
20 "linux"
21 } else {
22 "unknown"
23 };
24
25 let arch = if cfg!(target_arch = "aarch64") {
26 "arm64"
27 } else if cfg!(target_arch = "x86_64") {
28 "amd64"
29 } else {
30 "unknown"
31 };
32
33 (os, arch)
34}
35
36pub(crate) fn find_in_path(binary: &str) -> Option<PathBuf> {
38 let path_env = env::var_os("PATH")?;
39 let candidates = binary_candidates(binary);
40
41 for dir in env::split_paths(&path_env) {
42 for name in &candidates {
43 let path = dir.join(name);
44 if path.is_file() {
45 return Some(path);
46 }
47 }
48 }
49
50 None
51}
52
53pub(crate) fn home_dir() -> Option<PathBuf> {
55 dirs_next::home_dir()
56}
57
58pub(crate) fn default_local_bin() -> Result<PathBuf, InstallerError> {
60 let home = home_dir().ok_or(InstallerError::HomeDirMissing)?;
61 Ok(home.join(".local").join("bin"))
62}
63
64pub(super) fn executable_name(binary: &str) -> String {
66 binary.to_string()
67}
68
69pub(crate) fn binary_candidates(base: &str) -> Vec<String> {
71 vec![base.to_string()]
72}
73
74pub(super) fn check_dir_writable(dir: &Path) -> Result<(), InstallerError> {
76 tempfile::NamedTempFile::new_in(dir).map_err(|err| {
77 InstallerError::Io(std::io::Error::new(
78 err.kind(),
79 format!("directory not writable: {}: {err}", dir.display()),
80 ))
81 })?;
82 Ok(())
83}
84
85pub(super) fn set_executable_permissions(file: &fs::File) -> io::Result<()> {
87 use std::os::unix::fs::PermissionsExt;
88
89 let mut perms = file.metadata()?.permissions();
90 perms.set_mode(0o755);
91 file.set_permissions(perms)?;
92 Ok(())
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use crate::test_util::ENV_MUTEX;
99
100 #[test]
101 fn platform_binary_name_format() {
102 let name = platform_binary_name("wallet");
103 assert!(
104 name.starts_with("tempo-wallet-"),
105 "expected prefix 'tempo-wallet-', got: {name}"
106 );
107
108 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
109 assert_eq!(name, "tempo-wallet-darwin-arm64");
110
111 #[cfg(all(target_os = "macos", target_arch = "x86_64"))]
112 assert_eq!(name, "tempo-wallet-darwin-amd64");
113
114 #[cfg(all(target_os = "linux", target_arch = "aarch64"))]
115 assert_eq!(name, "tempo-wallet-linux-arm64");
116
117 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
118 assert_eq!(name, "tempo-wallet-linux-amd64");
119 }
120
121 #[test]
122 fn executable_name_passthrough() {
123 assert_eq!(executable_name("tempo-wallet"), "tempo-wallet");
124 }
125
126 #[test]
127 fn binary_candidates_single() {
128 assert_eq!(
129 binary_candidates("tempo-wallet"),
130 vec!["tempo-wallet".to_string()]
131 );
132 }
133
134 #[test]
135 fn home_dir_from_env() {
136 let _lock = ENV_MUTEX.lock().unwrap();
137 let original = std::env::var_os("HOME");
138 unsafe { std::env::set_var("HOME", "/test/home") };
139 assert_eq!(home_dir(), Some(PathBuf::from("/test/home")));
140 match original {
141 Some(v) => unsafe { std::env::set_var("HOME", v) },
142 None => unsafe { std::env::remove_var("HOME") },
143 }
144 }
145
146 #[test]
147 fn default_local_bin_path() {
148 let _lock = ENV_MUTEX.lock().unwrap();
149 let original = std::env::var_os("HOME");
150 unsafe { std::env::set_var("HOME", "/test/home") };
151 let result = default_local_bin().unwrap();
152 assert_eq!(result, PathBuf::from("/test/home/.local/bin"));
153 match original {
154 Some(v) => unsafe { std::env::set_var("HOME", v) },
155 None => unsafe { std::env::remove_var("HOME") },
156 }
157 }
158
159 #[test]
160 fn check_dir_writable_on_tempdir() {
161 let dir = tempfile::tempdir().unwrap();
162 assert!(check_dir_writable(dir.path()).is_ok());
163 }
164
165 #[test]
166 fn check_dir_writable_on_nonexistent() {
167 let result = check_dir_writable(Path::new("/nonexistent-test-dir-12345"));
168 assert!(result.is_err());
169 }
170
171 #[test]
172 fn set_executable_permissions_sets_mode() {
173 use std::os::unix::fs::PermissionsExt;
174 let tmp = tempfile::NamedTempFile::new().unwrap();
175 set_executable_permissions(tmp.as_file()).unwrap();
176 let perms = tmp.as_file().metadata().unwrap().permissions();
177 assert_eq!(perms.mode() & 0o755, 0o755);
178 }
179
180 #[test]
181 fn find_in_path_finds_binary() {
182 let _lock = ENV_MUTEX.lock().unwrap();
183 let dir = tempfile::tempdir().unwrap();
184 let bin_path = dir.path().join("test-tempo-binary");
185 fs::write(&bin_path, "fake binary").unwrap();
186 {
187 use std::os::unix::fs::PermissionsExt;
188 fs::set_permissions(&bin_path, fs::Permissions::from_mode(0o755)).unwrap();
189 }
190
191 let original = std::env::var_os("PATH");
192 let new_path = format!(
193 "{}:{}",
194 dir.path().display(),
195 original.as_deref().unwrap_or_default().to_string_lossy()
196 );
197 unsafe { std::env::set_var("PATH", &new_path) };
198
199 let found = find_in_path("test-tempo-binary");
200 assert_eq!(found, Some(bin_path));
201
202 match original {
203 Some(v) => unsafe { std::env::set_var("PATH", v) },
204 None => unsafe { std::env::remove_var("PATH") },
205 }
206 }
207
208 #[test]
209 fn find_in_path_returns_none_for_missing() {
210 assert!(find_in_path("nonexistent-binary-xyz-12345").is_none());
211 }
212}