aster/config/
search_path.rs1use std::{
2 env::{self},
3 ffi::{OsStr, OsString},
4 path::PathBuf,
5};
6
7use anyhow::{Context, Result};
8
9use crate::config::Config;
10
11pub struct SearchPaths {
12 paths: Vec<PathBuf>,
13}
14
15impl SearchPaths {
16 pub fn builder() -> Self {
17 let mut paths = Config::global()
18 .get_aster_search_paths()
19 .unwrap_or_default();
20
21 paths.push("~/.local/bin".into());
22
23 #[cfg(unix)]
24 {
25 paths.push("/usr/local/bin".into());
26 }
27
28 if cfg!(target_os = "macos") {
29 paths.push("/opt/homebrew/bin".into());
30 paths.push("/opt/local/bin".into());
31 }
32
33 Self {
34 paths: paths
35 .into_iter()
36 .map(|s| PathBuf::from(shellexpand::tilde(&s).as_ref()))
37 .collect(),
38 }
39 }
40
41 pub fn with_npm(mut self) -> Self {
42 if cfg!(windows) {
43 if let Some(appdata) = dirs::data_dir() {
44 self.paths.push(appdata.join("npm"));
45 }
46 } else if let Some(home) = dirs::home_dir() {
47 self.paths.push(home.join(".npm-global/bin"));
48 }
49 self
50 }
51
52 pub fn path(self) -> Result<OsString> {
53 env::join_paths(
54 self.paths.into_iter().chain(
55 env::var_os("PATH")
56 .as_ref()
57 .map(env::split_paths)
58 .into_iter()
59 .flatten(),
60 ),
61 )
62 .map_err(Into::into)
63 }
64
65 pub fn resolve<N>(self, name: N) -> Result<PathBuf>
66 where
67 N: AsRef<OsStr>,
68 {
69 which::which_in_global(name.as_ref(), Some(self.path()?))?
70 .next()
71 .with_context(|| {
72 format!(
73 "could not resolve command '{}': file does not exist",
74 name.as_ref().to_string_lossy()
75 )
76 })
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn test_path_preserves_existing_path() {
86 let search_paths = SearchPaths::builder();
87 let combined_path = search_paths.path().unwrap();
88
89 if let Some(existing_path) = env::var_os("PATH") {
90 let combined_str = combined_path.to_string_lossy();
91 let existing_str = existing_path.to_string_lossy();
92
93 assert!(combined_str.contains(&existing_str.to_string()));
94 }
95 }
96
97 #[test]
98 fn test_resolve_nonexistent_executable() {
99 let search_paths = SearchPaths::builder();
100
101 let result = search_paths.resolve("nonexistent_executable_12345_abcdef");
102
103 assert!(
104 result.is_err(),
105 "Resolving nonexistent executable should return an error"
106 );
107 }
108
109 #[test]
110 fn test_resolve_common_executable() {
111 let search_paths = SearchPaths::builder();
112
113 #[cfg(unix)]
114 let test_executable = "sh";
115
116 #[cfg(windows)]
117 let test_executable = "cmd";
118
119 search_paths
120 .resolve(test_executable)
121 .expect("should resolve sh (or cmd on Windows)");
122 }
123}