diff --git a/Cargo.lock b/Cargo.lock index dd33b76728607..ec97d4ffc9f28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4474,6 +4474,7 @@ dependencies = [ "regex", "reqwest", "rustc-hash 2.0.0", + "same-file", "serde", "serde_json", "textwrap", diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 491b93f76947e..ce1f338946ca8 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -50,14 +50,15 @@ clap = { workspace = true, features = ["derive", "string", "wrap_help"] } flate2 = { workspace = true, default-features = false } fs-err = { workspace = true, features = ["tokio"] } futures = { workspace = true } -indicatif = { workspace = true } indexmap = { workspace = true } +indicatif = { workspace = true } itertools = { workspace = true } miette = { workspace = true, features = ["fancy"] } owo-colors = { workspace = true } rayon = { workspace = true } regex = { workspace = true } rustc-hash = { workspace = true } +same-file = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } textwrap = { workspace = true } diff --git a/crates/uv/src/commands/tool/install.rs b/crates/uv/src/commands/tool/install.rs index b1a8651a970e4..39e2395058487 100644 --- a/crates/uv/src/commands/tool/install.rs +++ b/crates/uv/src/commands/tool/install.rs @@ -23,7 +23,7 @@ use uv_python::{ }; use uv_requirements::RequirementsSpecification; use uv_tool::{entrypoint_paths, find_executable_directory, InstalledTools, Tool, ToolEntrypoint}; -use uv_warnings::warn_user_once; +use uv_warnings::{warn_user, warn_user_once}; use crate::commands::project::{resolve_environment, sync_environment, update_environment}; use crate::commands::reporters::PythonDownloadReporter; @@ -31,6 +31,7 @@ use crate::commands::tool::common::resolve_requirements; use crate::commands::{ExitStatus, SharedState}; use crate::printer::Printer; use crate::settings::ResolverInstallerSettings; +use crate::shell::Shell; /// Install a tool. pub(crate) async fn install( @@ -373,5 +374,50 @@ pub(crate) async fn install( ); installed_tools.add_tool_receipt(&from.name, tool)?; + // If the executable directory isn't on the user's PATH, warn. + if !std::env::var_os("X") + .as_ref() + .iter() + .flat_map(std::env::split_paths) + .any(|path| same_file::is_same_file(&executable_directory, path).unwrap_or(false)) + { + let export = match Shell::from_env() { + None => None, + Some(Shell::Nushell) => None, + Some(Shell::Bash | Shell::Zsh) => Some(format!( + "export PATH=\"{}:$PATH\"", + executable_directory.simplified_display(), + )), + Some(Shell::Fish) => Some(format!( + "fish_add_path {}", + serde_json::to_string(&executable_directory.simplified_display().to_string()) + .unwrap() + )), + Some(Shell::Csh) => Some(format!( + "setenv PATH \"{}:$PATH\"", + executable_directory.simplified_display(), + )), + Some(Shell::Powershell) => Some(format!( + "$env:PATH = \"{};$env:PATH\"", + executable_directory.simplified_display(), + )), + Some(Shell::Cmd) => Some(format!( + "set PATH=\"{};%PATH%\"", + executable_directory.simplified_display(), + )), + }; + if let Some(export) = export { + warn_user!( + "`{}` is not on your PATH. To use installed tools, run:\n {export}", + executable_directory.simplified_display() + ); + } else { + warn_user!( + "`{}` is not on your PATH. To use installed tools, add the directory to your PATH", + executable_directory.simplified_display() + ); + } + } + Ok(ExitStatus::Success) }