Skip to main content

pistonite_cu/fs/
bin.rs

1//! Binary path registry
2//!
3//! This util provides a unified way of getting the path of a program to run,
4//! for example, let's say you are making a build script for a project, and
5//! you want to find the compiler program `gcc` in the following manner:
6//! - Most users would use the project-provided toolchain at `./toolchain/compiler/gcc`
7//! - If the user want to use the compiler on their system instead, we would first try
8//!   to finding `gcc` in `PATH`, then fallback to the toolchain.
9//! - Or some user may want to specify a particular path on their system with the `CC` environment
10//!   variable. We would try this first
11//!
12//! With `cu::bin`, you can register this logic once, then just use [`cu::which`](function@which)
13//! whenever you need to get the resolved path:
14//! ```rust,no_run
15//! # use pistonite_cu as cu;
16//! use std::path::PathBuf;
17//! use cu::pre::*;
18//!
19//! fn find_gcc(use_system: bool) -> cu::Result<PathBuf> {
20//!     let provided = cu::bin::location("./toolchain/compiler/gcc".as_ref());
21//!     if use_system {
22//!         cu::bin::find("gcc", [
23//!             cu::bin::from_env("CC"),
24//!             cu::bin::in_PATH(),
25//!             provided
26//!         ])
27//!     } else {
28//!         cu::bin::find("gcc", [
29//!             cu::bin::from_env("CC"),
30//!             provided
31//!         ])
32//!     }
33//! }
34//!
35//! # fn main() -> cu::Result<()> {
36//! // would use clap in a real program, here, just an example
37//! let use_system = std::env::args().any(|x| x == "--use-system-compiler");
38//! // register the path with the logic in find_gcc
39//! let _: PathBuf = find_gcc(use_system).context("cannot find gcc")?;
40//!
41//! // ... later in the progam
42//! cu::which("gcc")?.command()
43//!     //... configure the command
44//! #        ;
45//! # Ok(())
46//! # }
47//!
48//! ```
49//!
50use std::collections::BTreeMap;
51use std::path::{Path, PathBuf};
52use std::sync::{LazyLock, RwLock};
53
54use crate::pre::*;
55
56static BIN_PATHS: LazyLock<RwLock<BTreeMap<String, PathBuf>>> =
57    LazyLock::new(|| RwLock::new(BTreeMap::new()));
58
59/// Clear the cached path for the binary name, if previously set
60#[inline(always)]
61pub fn clear(name: impl AsRef<str>) {
62    clear_impl(name.as_ref())
63}
64fn clear_impl(name: &str) {
65    BIN_PATHS
66        .write()
67        .expect("could not lock global bin path map")
68        .remove(name);
69}
70
71/// Get a previously cached path.
72///
73/// Returns None if the path was never cached
74/// (either because it was never referenced, or because previous resolution failed),
75/// or if the path now does not exist
76#[inline(always)]
77pub fn get(name: impl AsRef<str>) -> Option<PathBuf> {
78    get_impl(name.as_ref())
79}
80fn get_impl(name: &str) -> Option<PathBuf> {
81    let guard = BIN_PATHS
82        .read()
83        .expect("could not lock global bin path map");
84    let path = guard.get(name)?;
85    if !path.exists() {
86        clear_impl(name);
87        return None;
88    }
89    Some(path.clone())
90}
91
92/// Check and set bin name to the specified path. Return the absolute path that is set.
93///
94/// This is different from the other functions in the module, as it will overwrite
95/// the existing cache entry.
96///
97/// See [`cu::bin`](self) module-level documentation for more info.
98///
99/// # Example
100/// ```rust,no_run
101/// # use pistonite_cu as cu;
102/// use std::path::PathBuf;
103///
104/// # fn main() -> cu::Result<()> {
105/// assert_eq!(cu::which("ninja")?, PathBuf::from("/usr/bin/ninja"));
106/// cu::bin::set("ninja", "/my/path/to/ninja")?;
107/// // `which` now returns registered value
108/// assert_eq!(cu::which("ninja")?, PathBuf::from("/my/path/to/ninja"));
109/// # Ok(())
110/// # }
111/// ```
112#[inline(always)]
113pub fn set(name: impl AsRef<str>, path: impl AsRef<Path>) -> crate::Result<PathBuf> {
114    set_impl(name.as_ref(), path.as_ref())
115}
116fn set_impl(name: &str, path: &Path) -> crate::Result<PathBuf> {
117    let path = location(path).find(name)?;
118    crate::trace!("setting bin path: '{name}' -> '{}'", path.display());
119    let mut paths = BIN_PATHS
120        .write()
121        .expect("could not lock global bin path map");
122    let old = paths.insert(name.to_string(), path.clone());
123    if let Some(old) = old {
124        crate::trace!("replacing old bin path: '{}'", old.display());
125    }
126    Ok(path)
127}
128
129/// Find an executable in PATH, or use a previously registered path (also available as
130/// `cu::which`).
131///
132/// Result will be cached so finding the same executable name will always result in the same path.
133///
134/// See [`cu::bin`](self) module-level documentation for more info.
135#[inline(always)]
136pub fn which(name: impl AsRef<str>) -> crate::Result<PathBuf> {
137    find_impl(name.as_ref(), std::iter::once(in_PATH()))
138}
139
140/// Resolve an executable at the location provided, or use a previously registered path.
141///
142/// Result will be cached so finding the same executable name will always result in the same path.
143///
144/// See [`cu::bin`](self) module-level documentation for more info.
145#[inline(always)]
146pub fn resolve(name: impl AsRef<str>, path: impl AsRef<Path>) -> crate::Result<PathBuf> {
147    find_impl(name.as_ref(), std::iter::once(location(path.as_ref())))
148}
149
150/// Find the executable using a series of strategies.
151///
152/// If there's already a registered path from a previous find that matches `name`,
153/// that path is returned instead.
154///
155/// See [`cu::bin`](self) module-level documentation for more info.
156#[inline(always)]
157pub fn find<'a, I: IntoIterator<Item = Strategy<'a>>>(
158    name: impl AsRef<str>,
159    strats: I,
160) -> crate::Result<PathBuf> {
161    find_impl(name.as_ref(), strats)
162}
163fn find_impl<'a, I: IntoIterator<Item = Strategy<'a>>>(
164    name: &str,
165    strats: I,
166) -> crate::Result<PathBuf> {
167    {
168        let Ok(paths) = BIN_PATHS.read() else {
169            crate::bail!("could not lock global bin path map");
170        };
171        if let Some(x) = paths.get(name)
172            && x.exists()
173        {
174            crate::trace!("found executable '{name}' from cache: {}", x.display());
175            return Ok(x.clone());
176        }
177    }
178    let Ok(mut paths) = BIN_PATHS.write() else {
179        crate::bail!("could not lock global bin path map");
180    };
181    if let Some(x) = paths.get(name)
182        && x.exists()
183    {
184        crate::trace!("found executable '{name}' from cache: {}", x.display());
185        return Ok(x.clone());
186    }
187    let path = find_with_strats(name, strats)?;
188    crate::trace!("found executable '{name}': {}", path.display());
189    paths.insert(name.to_string(), path.clone());
190    Ok(path)
191}
192
193fn find_with_strats<'a, I: IntoIterator<Item = Strategy<'a>>>(
194    name: &str,
195    strats: I,
196) -> crate::Result<PathBuf> {
197    let mut strats = strats.into_iter();
198    let Some(first) = strats.next() else {
199        crate::bail!("must provide at least one strategy to cu::bin::find");
200    };
201    let mut error = match first.find(name) {
202        Ok(x) => return Ok(x),
203        Err(e) => e,
204    };
205    let mut prev = first;
206    for strat in strats {
207        crate::trace!("could not finding '{name}' {prev}: {error}");
208        crate::trace!("falling back to finding '{name}' {strat}");
209        error = match strat.find(name) {
210            Ok(x) => return Ok(x),
211            Err(e) => e,
212        };
213        prev = strat;
214    }
215
216    crate::trace!("could not finding '{name}' {prev}: {error}");
217    crate::bail!("could not find program '{name}'");
218}
219
220/// Strategy to resolve the given path as path for the program
221///
222/// See [`cu::bin`](self) module-level documentation for more info.
223pub fn location<'a>(path: &'a Path) -> Strategy<'a> {
224    Strategy::Resolve(path)
225}
226
227/// Strategy to find the program in the PATH environment variable
228///
229/// See [`cu::bin`](self) module-level documentation for more info.
230#[allow(non_snake_case)]
231pub fn in_PATH() -> Strategy<'static> {
232    Strategy::Which
233}
234/// Strategy to read the environment variable and resolve that path as the program
235///
236/// See [`cu::bin`](self) module-level documentation for more info.
237pub fn from_env<'a>(env: &'a str) -> Strategy<'a> {
238    Strategy::EnvVar(env)
239}
240
241/// Strategy to find a binary
242///
243/// See [`cu::bin`](self) module-level documentation for more info.
244pub enum Strategy<'a> {
245    /// Find it in PATH (like the `which` command)
246    Which,
247    /// Resolve a provided path
248    Resolve(&'a Path),
249    /// Resolve from path in an environment variable
250    EnvVar(&'a str),
251}
252
253impl Strategy<'_> {
254    fn find(&self, name: &str) -> crate::Result<PathBuf> {
255        match self {
256            Self::Which => {
257                crate::trace!("finding executable '{name}' in PATH");
258                match which::which(name) {
259                    // which already canonicalize it, which ensures it exists
260                    Ok(x) => Ok(x.simplified().to_path_buf()),
261                    Err(e) => Err(e)?,
262                }
263            }
264            Self::Resolve(path) => {
265                crate::trace!("finding executable '{name}' at '{}'", path.display());
266                path.normalize_executable()
267            }
268            Self::EnvVar(v) => {
269                crate::trace!("finding executable '{name}' using environment variable '{v}'");
270                match std::env::var(v) {
271                    Ok(x) if !x.is_empty() => Path::new(&x).normalize_executable(),
272                    _ => crate::bail!("environment variable '{v}' not found"),
273                }
274            }
275        }
276    }
277}
278impl std::fmt::Display for Strategy<'_> {
279    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
280        match self {
281            Strategy::Which => write!(f, "in PATH"),
282            Strategy::Resolve(path) => write!(f, " at '{}'", path.display()),
283            Strategy::EnvVar(env) => write!(f, "using envvar '{env}'"),
284        }
285    }
286}