command_stream/
local_bin.rs1use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5
6#[derive(Debug, Clone, Default, PartialEq, Eq)]
8pub enum PreferLocal {
9 #[default]
11 Off,
12 Cwd,
14 Dirs(Vec<PathBuf>),
16}
17
18pub const PATH_DELIMITER: &str = if cfg!(windows) { ";" } else { ":" };
20
21pub fn path_key<'a, I>(keys: I) -> String
23where
24 I: IntoIterator<Item = &'a String>,
25{
26 if cfg!(windows) {
27 keys.into_iter()
28 .find(|k| k.eq_ignore_ascii_case("path"))
29 .cloned()
30 .unwrap_or_else(|| "Path".to_string())
31 } else {
32 "PATH".to_string()
33 }
34}
35
36fn absolutize(dir: &Path) -> PathBuf {
37 if dir.is_absolute() {
38 dir.to_path_buf()
39 } else {
40 std::env::current_dir()
41 .map(|cwd| cwd.join(dir))
42 .unwrap_or_else(|_| dir.to_path_buf())
43 }
44}
45
46pub fn prefer_local_bin<P: AsRef<Path>>(path: Option<&str>, dirs: &[P]) -> String {
48 let mut parts: Vec<String> = Vec::new();
49 for dir in dirs {
50 let dir = absolutize(dir.as_ref());
51 parts.push(
52 dir.join("node_modules")
53 .join(".bin")
54 .to_string_lossy()
55 .into_owned(),
56 );
57 parts.push(dir.to_string_lossy().into_owned());
58 }
59 if let Some(path) = path {
60 parts.push(path.to_string());
61 }
62 parts.join(PATH_DELIMITER)
63}
64
65pub(crate) fn preferred_path(
67 env: Option<&HashMap<String, String>>,
68 cwd: &Path,
69 preference: &PreferLocal,
70) -> Option<(String, String)> {
71 let dirs = match preference {
72 PreferLocal::Off => return None,
73 PreferLocal::Cwd => vec![cwd.to_path_buf()],
74 PreferLocal::Dirs(dirs) => dirs.clone(),
75 };
76 if dirs.is_empty() {
77 return None;
78 }
79 let (key, current) = if let Some(env) = env {
80 let key = path_key(env.keys());
81 let value = env.get(&key).cloned();
82 (key, value)
83 } else {
84 let vars: Vec<String> = std::env::vars().map(|(key, _)| key).collect();
85 let key = path_key(vars.iter());
86 let value = std::env::var(&key).ok();
87 (key, value)
88 };
89 Some((key, prefer_local_bin(current.as_deref(), &dirs)))
90}
91
92pub(crate) fn apply_prefer_local(
94 command: &mut tokio::process::Command,
95 env: Option<&HashMap<String, String>>,
96 cwd: &Path,
97 preference: &PreferLocal,
98) {
99 if let Some((key, path)) = preferred_path(env, cwd, preference) {
100 command.env(key, path);
101 }
102}