which/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use common::cache::{self, CacheStatic};
4use dashmap::DashMap;
5use rayon::prelude::*;
6use std::{
7    borrow::Cow,
8    env,
9    path::{Path, PathBuf},
10    sync::LazyLock,
11};
12
13/// Errors when trying to resolve a path.
14#[derive(Debug)]
15pub enum Error {
16    /// If path couldn't be found.
17    NotFound(String),
18}
19impl std::fmt::Display for Error {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        match self {
22            Self::NotFound(e) => write!(f, "Could not find {e} in path"),
23        }
24    }
25}
26impl std::error::Error for Error {}
27
28/// The User's PATH variable, removing ~/.local/bin to prevent
29/// Antimony from using itself when a profile has been integrated.
30pub static PATH: LazyLock<Vec<PathBuf>> = LazyLock::new(|| {
31    let path = env::var("PATH").unwrap_or("/usr/bin".to_string());
32    path.split(':')
33        .filter(|e| !e.contains("/.local/bin"))
34        .map(PathBuf::from)
35        .filter(|root| root.exists())
36        .collect::<Vec<_>>()
37});
38
39/// The cache store.
40static CACHE: CacheStatic<String, Cow<'static, str>> = LazyLock::new(DashMap::default);
41
42/// The underlying cache, storing path -> resolved path lookups.
43static WHICH: LazyLock<cache::Cache<String, Cow<'static, str>>> =
44    LazyLock::new(|| cache::Cache::new(&CACHE));
45
46/// Resolve the provided path in the environment's PATH variable.
47/// Note that this implementation will return a path as-is if it exists,
48/// which means that if binary exists in the current folder, it will
49/// be resolved to that. It will also just return absolute paths as-is,
50/// even if they aren't executable.
51pub fn which(path: &str) -> Result<&'static str, Error> {
52    match WHICH.get(path) {
53        Some(resolved) => Ok(resolved),
54        None => {
55            let resolved = if Path::new(path).exists() {
56                path.to_string()
57            } else {
58                PATH.par_iter()
59                    .find_map_any(|root: &PathBuf| {
60                        let candidate = root.join(path);
61                        if candidate.exists() {
62                            Some(candidate.to_string_lossy().into_owned())
63                        } else {
64                            None
65                        }
66                    })
67                    .ok_or(Error::NotFound(path.to_string()))?
68            };
69            Ok(WHICH.insert(path.to_string(), Cow::Owned(resolved)))
70        }
71    }
72}