use crate::config::Settings;
use crate::dirs;
use std::collections::HashSet;
use std::env::{join_paths, split_paths};
use std::ffi::{OsStr, OsString};
use std::fmt;
use std::fmt::{Display, Formatter};
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::atomic::{AtomicBool, Ordering};
pub struct PathEnv {
pre: Vec<PathBuf>,
mise: Vec<PathBuf>,
post: Vec<PathBuf>,
seen_shims: bool,
}
impl PathEnv {
pub fn new() -> Self {
Self {
pre: Vec::new(),
mise: Vec::new(),
post: Vec::new(),
seen_shims: false,
}
}
pub fn add(&mut self, path: PathBuf) {
for part in split_paths(&path) {
self.mise.push(part);
}
}
pub fn to_vec(&self) -> Vec<PathBuf> {
let mut seen = HashSet::new();
self.pre
.iter()
.chain(self.mise.iter())
.chain(self.post.iter())
.filter(|p| seen.insert(*p))
.map(|p| p.to_path_buf())
.collect()
}
pub fn to_vec_verbatim(&self) -> Vec<PathBuf> {
self.pre
.iter()
.chain(self.mise.iter())
.chain(self.post.iter())
.map(|p| p.to_path_buf())
.collect()
}
pub fn join(&self) -> OsString {
let joined = join_paths(self.to_vec()).unwrap();
warn_if_cmd_ignores_path(&joined);
joined
}
pub fn join_verbatim(&self) -> OsString {
join_paths(self.to_vec_verbatim()).unwrap()
}
}
impl Display for PathEnv {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.join().to_string_lossy())
}
}
impl FromIterator<PathBuf> for PathEnv {
fn from_iter<T: IntoIterator<Item = PathBuf>>(paths: T) -> Self {
let settings = Settings::get();
let preserve_shims = settings.not_found_auto_install;
let mut path_env = Self::new();
for path in paths {
if path_env.seen_shims {
path_env.post.push(path);
} else if crate::file::paths_eq(&crate::file::replace_path(&path), &dirs::SHIMS)
&& !settings.activate_aggressive
{
path_env.seen_shims = true;
if preserve_shims {
path_env.post.push(path);
}
} else {
path_env.pre.push(path);
}
}
if !path_env.seen_shims {
path_env.post = path_env.pre;
path_env.pre = Vec::new();
}
path_env
}
}
impl PathEnv {
pub fn from_path_str(path: &str) -> Self {
Self::from_iter(split_paths(path))
}
}
impl FromStr for PathEnv {
type Err = eyre::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::from_path_str(s))
}
}
pub(crate) fn mise_install_dirs() -> Vec<PathBuf> {
let mut install_dirs = vec![dirs::INSTALLS.to_path_buf()];
install_dirs.extend(crate::env::shared_install_dirs());
install_dirs
}
pub(crate) fn is_mise_install_path(path: &std::path::Path, install_dirs: &[PathBuf]) -> bool {
if install_dirs.iter().any(|d| path.starts_with(d)) {
return true;
}
let Some(path) = crate::file::canonicalize_cached(path) else {
return false;
};
install_dirs
.iter()
.filter_map(|d| crate::file::canonicalize_cached(d))
.any(|d| path.starts_with(d))
}
pub(crate) const WINDOWS_CMD_PATH_LIMIT: usize = 8191;
pub(crate) fn path_len_utf16(path: &OsStr) -> usize {
path.to_string_lossy().encode_utf16().count()
}
pub(crate) fn cmd_would_ignore_path(len: usize) -> bool {
cfg!(windows) && len > WINDOWS_CMD_PATH_LIMIT
}
static WARNED_CMD_PATH_LIMIT: AtomicBool = AtomicBool::new(false);
fn warn_if_cmd_ignores_path(path: &OsStr) {
let len = path_len_utf16(path);
if !cmd_would_ignore_path(len) {
return;
}
if WARNED_CMD_PATH_LIMIT.swap(true, Ordering::Relaxed) {
return;
}
warn!(
"PATH is {} characters, longer than the {} cmd.exe accepts. cmd.exe ignores an \
inherited variable that long outright, so anything run through it — npm, npx, batch \
scripts — stops finding whatever it looks up on PATH: \
https://mise.jdx.dev/troubleshooting.html#path-limits",
len, WINDOWS_CMD_PATH_LIMIT
);
}
pub fn warn_if_cmd_ignores_path_str(path: &str) {
warn_if_cmd_ignores_path(OsStr::new(path));
}
#[cfg(test)]
mod dedup_tests {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn to_vec_drops_later_exact_duplicates() {
let mut path_env = PathEnv::from_iter(
["/stale-extra", "/usr/bin", "/stale-extra", "/bin"].map(PathBuf::from),
);
path_env.add("/stale-extra".into());
path_env.add("/tool".into());
assert_eq!(
path_env.to_vec(),
["/stale-extra", "/tool", "/usr/bin", "/bin"].map(PathBuf::from)
);
}
#[test]
fn to_vec_verbatim_preserves_user_duplicates() {
let mut path_env =
PathEnv::from_iter(["/shared", "/usr/bin", "/shared"].map(PathBuf::from));
path_env.add("/tool".into());
assert_eq!(
path_env.to_vec_verbatim(),
["/tool", "/shared", "/usr/bin", "/shared"].map(PathBuf::from)
);
}
#[test]
fn to_vec_dedups_by_path_components_not_bytes() {
let path_env = PathEnv::from_iter(["/dir", "/dir/", "/Dir", "/dir-2"].map(PathBuf::from));
assert_eq!(
path_env.to_vec(),
["/dir", "/Dir", "/dir-2"].map(PathBuf::from)
);
}
#[cfg(windows)]
#[test]
fn to_vec_collapses_drive_letter_case() {
let path_env = PathEnv::from_iter([r"C:\x", r"c:\x", r"C:\y"].map(PathBuf::from));
assert_eq!(path_env.to_vec(), [r"C:\x", r"C:\y"].map(PathBuf::from));
}
#[test]
fn cmd_would_ignore_path_at_the_boundary() {
assert!(!cmd_would_ignore_path(0));
assert!(!cmd_would_ignore_path(WINDOWS_CMD_PATH_LIMIT));
assert_eq!(
cmd_would_ignore_path(WINDOWS_CMD_PATH_LIMIT + 1),
cfg!(windows),
"the limit is a cmd.exe property, so it must never fire off Windows"
);
}
#[test]
fn path_len_is_utf16_units_not_bytes() {
let s = "あ".repeat(WINDOWS_CMD_PATH_LIMIT);
assert_eq!(path_len_utf16(OsStr::new(&s)), WINDOWS_CMD_PATH_LIMIT);
assert!(
s.len() > WINDOWS_CMD_PATH_LIMIT,
"byte length must differ, or this test proves nothing"
);
assert!(!cmd_would_ignore_path(path_len_utf16(OsStr::new(&s))));
}
}
#[cfg(unix)]
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use crate::config::Config;
use super::*;
#[tokio::test]
async fn test_path_env() {
let _config = Config::get().await.unwrap();
let shims = dirs::SHIMS.to_str().unwrap();
let mut path_env = PathEnv::from_iter(
[
"/before-1",
"/before-2",
"/before-3",
shims,
"/after-1",
"/after-2",
"/after-3",
]
.map(PathBuf::from),
);
path_env.add("/1".into());
path_env.add("/2".into());
path_env.add("/3".into());
assert_eq!(
path_env.to_string(),
format!("/before-1:/before-2:/before-3:/1:/2:/3:{shims}:/after-1:/after-2:/after-3")
);
}
#[tokio::test]
async fn test_path_env_no_mise() {
let _config = Config::get().await.unwrap();
let mut path_env = PathEnv::from_iter(
[
"/before-1",
"/before-2",
"/before-3",
"/after-1",
"/after-2",
"/after-3",
]
.map(PathBuf::from),
);
path_env.add("/1".into());
path_env.add("/2".into());
path_env.add("/3".into());
assert_eq!(
path_env.to_string(),
format!("/1:/2:/3:/before-1:/before-2:/before-3:/after-1:/after-2:/after-3")
);
}
#[tokio::test]
async fn test_path_env_with_colon() {
let _config = Config::get().await.unwrap();
let mut path_env = PathEnv::from_iter(["/item1", "/item2"].map(PathBuf::from));
path_env.add("/1:/2".into());
assert_eq!(path_env.to_string(), format!("/1:/2:/item1:/item2"));
}
}