Skip to main content

tempo_ext/installer/
platform.rs

1//! Platform detection and binary path utilities.
2
3use std::{
4    env, fs, io,
5    path::{Path, PathBuf},
6};
7
8use crate::installer::error::InstallerError;
9
10/// Builds the platform-specific binary name (e.g. `tempo-wallet-darwin-arm64`).
11pub(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
36/// Searches `PATH` for a binary by name, returning the first match.
37pub(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
53/// Returns the user's home directory via `dirs_next`.
54pub(crate) fn home_dir() -> Option<PathBuf> {
55    dirs_next::home_dir()
56}
57
58/// Returns `~/.local/bin`, the default install directory on Unix.
59pub(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
64/// Returns the platform executable filename (identity on Unix, `.exe` on Windows).
65pub(super) fn executable_name(binary: &str) -> String {
66    binary.to_string()
67}
68
69/// Returns candidate filenames to search for a binary (platform-dependent).
70pub(crate) fn binary_candidates(base: &str) -> Vec<String> {
71    vec![base.to_string()]
72}
73
74/// Verifies that `dir` is writable by creating a temporary file in it.
75pub(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
85/// Sets the file mode to `0o755` (owner rwx, group/other rx).
86pub(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}