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}