diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 8086962646..e8eb65968d 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -18,7 +18,9 @@ use crate::cmd::CmdLineRunner; use crate::config::config_file::config_root; use crate::config::{Config, Settings}; use crate::duration::parse_into_timestamp; -use crate::file::{display_path, remove_all_with_progress, remove_all_with_warning}; +use crate::file::{ + canonicalize_cached, display_path, remove_all_with_progress, remove_all_with_warning, +}; use crate::install_before::resolve_before_date; use crate::install_context::InstallContext; use crate::lockfile::{PlatformInfo, ProvenanceType}; @@ -1346,21 +1348,16 @@ pub trait Backend: Debug + Send + Sync { }; // Canonicalize to resolve any ".." components before checking. // If target doesn't exist (canonicalize fails), don't skip - treat as needing install - let Ok(target) = target.canonicalize() else { - return None; - }; + let target = canonicalize_cached(&target)?; // Canonicalize INSTALLS too for consistent comparison (handles symlinked data dirs) - let installs = dirs::INSTALLS - .canonicalize() - .unwrap_or(dirs::INSTALLS.to_path_buf()); + let installs = + canonicalize_cached(&dirs::INSTALLS).unwrap_or(dirs::INSTALLS.to_path_buf()); if target.starts_with(&installs) { return Some(path); } // Also check shared install directories for shared_dir in env::shared_install_dirs() { - let shared = shared_dir - .canonicalize() - .unwrap_or(shared_dir.to_path_buf()); + let shared = canonicalize_cached(&shared_dir).unwrap_or(shared_dir.to_path_buf()); if target.starts_with(&shared) { return Some(path); } diff --git a/src/cli/activate.rs b/src/cli/activate.rs index 8db9e763b6..6b2b6c0ff0 100644 --- a/src/cli/activate.rs +++ b/src/cli/activate.rs @@ -2,7 +2,7 @@ use std::path::{Path, PathBuf}; use crate::config::Settings; use crate::env::PATH_KEY; -use crate::file::touch_dir; +use crate::file::{canonicalize_cached, canonicalize_or_self, touch_dir}; use crate::path_env::PathEnv; use crate::shell::{ActivateOptions, ActivatePrelude, Shell, ShellType, get_shell}; use crate::toolset::env_cache::CachedEnv; @@ -193,12 +193,10 @@ fn remove_shims() -> std::io::Result> { return Ok(None); } - let shims = dirs::SHIMS - .canonicalize() - .unwrap_or(dirs::SHIMS.to_path_buf()); + let shims = canonicalize_or_self(&dirs::SHIMS); if env::PATH .iter() - .filter_map(|p| p.canonicalize().ok()) + .filter_map(|p| canonicalize_cached(p)) .contains(&shims) { let path_env = PathEnv::from_iter(env::PATH.clone()); @@ -211,17 +209,15 @@ fn remove_shims() -> std::io::Result> { } fn is_dir_in_path(dir: &Path) -> bool { - let dir = dir.canonicalize().unwrap_or(dir.to_path_buf()); + let dir = canonicalize_or_self(dir); env::PATH .clone() .into_iter() - .any(|p| p.canonicalize().unwrap_or(p) == dir) + .any(|p| canonicalize_or_self(&p) == dir) } fn is_dir_not_in_nix(dir: &Path) -> bool { - !dir.canonicalize() - .unwrap_or(dir.to_path_buf()) - .starts_with("/nix/") + !canonicalize_or_self(dir).starts_with("/nix/") } static AFTER_LONG_HELP: &str = color_print::cstr!( diff --git a/src/cli/doctor/mod.rs b/src/cli/doctor/mod.rs index 38d170c1d8..0eb0632be0 100644 --- a/src/cli/doctor/mod.rs +++ b/src/cli/doctor/mod.rs @@ -11,7 +11,7 @@ use crate::cli::version; use crate::cli::version::VERSION; use crate::config::{Config, IGNORED_CONFIG_FILES, Settings}; use crate::env::PATH_KEY; -use crate::file::display_path; +use crate::file::{canonicalize_cached, canonicalize_or_self, display_path}; use crate::git::Git; use crate::plugins::PluginType; use crate::plugins::core::CORE_PLUGINS; @@ -525,13 +525,13 @@ impl Doctor { return; } - let resolve = |p: &PathBuf| p.canonicalize().unwrap_or_else(|_| p.clone()); + let resolve = |p: &PathBuf| canonicalize_or_self(p); // Resolve all mise-managed paths for comparison let mise_paths_resolved: HashSet = mise_paths.iter().map(resolve).collect(); // Also exclude the mise binary's own directory - let mise_bin_parent = env::MISE_BIN.parent().and_then(|p| p.canonicalize().ok()); + let mise_bin_parent = env::MISE_BIN.parent().and_then(canonicalize_cached); // Find the index of the first mise-managed path in the current PATH // Note: mise_bin_parent is intentionally excluded here — it's a directory like diff --git a/src/cli/hook_env.rs b/src/cli/hook_env.rs index 29ff61a883..633c156d5b 100644 --- a/src/cli/hook_env.rs +++ b/src/cli/hook_env.rs @@ -3,7 +3,7 @@ use crate::direnv::DirenvDiff; use crate::env::{__MISE_DIFF, PATH_KEY, TERM_WIDTH}; use crate::env::{join_paths, split_paths}; use crate::env_diff::{EnvDiff, EnvDiffOperation, EnvMap}; -use crate::file::display_rel_path; +use crate::file::{canonicalize_cached, display_rel_path}; use crate::hook_env::{PREV_SESSION, WatchFilePattern}; use crate::shell::{ShellType, get_shell}; use crate::toolset::Toolset; @@ -324,12 +324,12 @@ impl HookEnv { // Use canonicalized paths for comparison to handle symlinks, relative paths, // and other path variants that refer to the same filesystem location. let post_canonical: HashSet = - post.iter().filter_map(|p| p.canonicalize().ok()).collect(); + post.iter().filter_map(|p| canonicalize_cached(p)).collect(); let user_additions_set: HashSet<_> = pre.iter().chain(post_user.iter()).collect(); let user_additions_canonical: HashSet = pre .iter() .chain(post_user.iter()) - .filter_map(|p| p.canonicalize().ok()) + .filter_map(|p| canonicalize_cached(p)) .collect(); let tool_paths_filtered: Vec = tool_paths @@ -343,7 +343,7 @@ impl HookEnv { if post.contains(p) { return false; } - if let Ok(canonical) = p.canonicalize() + if let Some(canonical) = canonicalize_cached(p) && post_canonical.contains(&canonical) { return false; @@ -353,7 +353,7 @@ impl HookEnv { if user_additions_set.contains(p) { return false; } - if let Ok(canonical) = p.canonicalize() + if let Some(canonical) = canonicalize_cached(p) && user_additions_canonical.contains(&canonical) { return false; @@ -375,7 +375,7 @@ impl HookEnv { if user_additions_set.contains(p) { return false; } - if let Ok(canonical) = p.canonicalize() + if let Some(canonical) = canonicalize_cached(p) && user_additions_canonical.contains(&canonical) { return false; diff --git a/src/config/settings.rs b/src/config/settings.rs index 989c31aa57..7cedbc8f39 100644 --- a/src/config/settings.rs +++ b/src/config/settings.rs @@ -601,7 +601,7 @@ impl Settings { .iter() .filter(|p| !p.to_string_lossy().is_empty()) .map(file::replace_path) - .filter_map(|p| p.canonicalize().ok()) + .filter_map(|p| file::canonicalize_cached(&p)) } pub fn global_tools_file(&self) -> PathBuf { diff --git a/src/file.rs b/src/file.rs index 0895d0c975..3265f5f5c9 100644 --- a/src/file.rs +++ b/src/file.rs @@ -738,6 +738,34 @@ pub fn which_non_pristine>(name: P) -> Option { _which(name, &env::PATH_NON_PRISTINE) } +/// Canonicalize a path and cache successful resolutions for the current process. +/// +/// Use this for repeated comparisons against stable roots or PATH entries. Failed +/// canonicalizations are not cached because many callers handle paths that may be +/// created later in the same process. +pub fn canonicalize_cached(path: &Path) -> Option { + static CACHE: Lazy>> = Lazy::new(Default::default); + + if !path.is_absolute() { + return path.canonicalize().ok(); + } + if let Some(path) = CACHE.lock().unwrap().get(path).cloned() { + return Some(path); + } + let canonicalized = path.canonicalize().ok()?; + CACHE + .lock() + .unwrap() + .insert(path.to_path_buf(), canonicalized.clone()); + Some(canonicalized) +} + +/// Canonicalize a path using the process cache, falling back to the original +/// path when canonicalization fails. +pub fn canonicalize_or_self(path: &Path) -> PathBuf { + canonicalize_cached(path).unwrap_or_else(|| path.to_path_buf()) +} + /// Build a PATH value with mise shims filtered out, suitable for passing to /// subprocesses via `.env("PATH", ...)`. Prevents infinite recursion when a /// subprocess (e.g. `gh auth token`, `git credential fill`) resolves to a diff --git a/src/shims.rs b/src/shims.rs index 33c36b4875..4ef321b2e3 100644 --- a/src/shims.rs +++ b/src/shims.rs @@ -88,25 +88,22 @@ async fn which_shim(config: &mut Arc, bin_name: &str) -> Result } } // fallback for "system" - let mise_bin = fs::canonicalize(&*env::MISE_BIN).unwrap_or_else(|_| env::MISE_BIN.clone()); - let user_shims = fs::canonicalize(*dirs::SHIMS).unwrap_or_default(); + let mise_bin = file::canonicalize_or_self(&env::MISE_BIN); + let user_shims = file::canonicalize_cached(&dirs::SHIMS); let sys_shims = { let p = env::MISE_SYSTEM_DATA_DIR.join("shims"); - if p.exists() { - fs::canonicalize(&p).unwrap_or(p) - } else { - PathBuf::new() - } + file::canonicalize_cached(&p) }; for path in &*env::PATH { - let canon_path = fs::canonicalize(path).unwrap_or_default(); - if canon_path == user_shims || canon_path == sys_shims { + if let Some(canon_path) = file::canonicalize_cached(path) + && (user_shims.as_ref() == Some(&canon_path) || sys_shims.as_ref() == Some(&canon_path)) + { continue; } let bin = path.join(bin_name); if bin.exists() { // Skip if this binary is a mise shim (symlink pointing to the mise binary) - if fs::canonicalize(&bin).unwrap_or_default() == mise_bin { + if file::canonicalize_cached(&bin).is_some_and(|bin| bin == mise_bin) { continue; } trace!("shim[{bin_name}] SYSTEM {bin}", bin = display_path(&bin)); diff --git a/src/task/task_helpers.rs b/src/task/task_helpers.rs index 9c643a90bc..35f675a872 100644 --- a/src/task/task_helpers.rs +++ b/src/task/task_helpers.rs @@ -1,3 +1,4 @@ +use crate::file::canonicalize_or_self; use crate::task::Task; use std::path::{Path, PathBuf}; @@ -11,5 +12,5 @@ pub fn task_needs_permit(task: &Task) -> bool { /// Canonicalize a path for use as cache key /// Falls back to original path if canonicalization fails pub fn canonicalize_path(path: &Path) -> PathBuf { - path.canonicalize().unwrap_or_else(|_| path.to_path_buf()) + canonicalize_or_self(path) }