diff --git a/crates/uv-shell/src/lib.rs b/crates/uv-shell/src/lib.rs index a8689f764a755..a8a0a96abd90e 100644 --- a/crates/uv-shell/src/lib.rs +++ b/crates/uv-shell/src/lib.rs @@ -82,6 +82,14 @@ impl Shell { parse_shell_from_path(path.as_ref()) } + /// Returns `true` if the shell supports a `PATH` update command. + pub fn supports_update(self) -> bool { + match self { + Shell::Powershell | Shell::Cmd => true, + shell => !shell.configuration_files().is_empty(), + } + } + /// Return the configuration files that should be modified to append to a shell's `PATH`. /// /// Some of the logic here is based on rustup's rc file detection. diff --git a/crates/uv/src/commands/python/install.rs b/crates/uv/src/commands/python/install.rs index d64d219bfd5e8..840743002c57b 100644 --- a/crates/uv/src/commands/python/install.rs +++ b/crates/uv/src/commands/python/install.rs @@ -712,20 +712,11 @@ fn warn_if_not_on_path(bin: &Path) { if !Shell::contains_path(bin) { if let Some(shell) = Shell::from_env() { if let Some(command) = shell.prepend_path(bin) { - if shell.configuration_files().is_empty() { - warn_user!( - "`{}` is not on your PATH. To use the installed Python executable, run `{}`.", - bin.simplified_display().cyan(), - command.green() - ); - } else { - // TODO(zanieb): Update when we add `uv python update-shell` to match `uv tool` - warn_user!( - "`{}` is not on your PATH. To use the installed Python executable, run `{}`.", - bin.simplified_display().cyan(), - command.green(), - ); - } + warn_user!( + "`{}` is not on your PATH. To use the installed Python executable, run `{}`.", + bin.simplified_display().cyan(), + command.green(), + ); } else { warn_user!( "`{}` is not on your PATH. To use the installed Python executable, add the directory to your PATH.", diff --git a/crates/uv/src/commands/tool/common.rs b/crates/uv/src/commands/tool/common.rs index 3e2f283eb75b7..b4a958099cf23 100644 --- a/crates/uv/src/commands/tool/common.rs +++ b/crates/uv/src/commands/tool/common.rs @@ -301,18 +301,18 @@ pub(crate) fn install_executables( if !Shell::contains_path(&executable_directory) { if let Some(shell) = Shell::from_env() { if let Some(command) = shell.prepend_path(&executable_directory) { - if shell.configuration_files().is_empty() { + if shell.supports_update() { warn_user!( - "`{}` is not on your PATH. To use installed tools, run `{}`.", + "`{}` is not on your PATH. To use installed tools, run `{}` or `{}`.", executable_directory.simplified_display().cyan(), - command.green() + command.green(), + "uv tool update-shell".green() ); } else { warn_user!( - "`{}` is not on your PATH. To use installed tools, run `{}` or `{}`.", + "`{}` is not on your PATH. To use installed tools, run `{}`.", executable_directory.simplified_display().cyan(), - command.green(), - "uv tool update-shell".green() + command.green() ); } } else { diff --git a/crates/uv/src/commands/tool/update_shell.rs b/crates/uv/src/commands/tool/update_shell.rs index 9a61917618dc9..36cf7c91cdcd5 100644 --- a/crates/uv/src/commands/tool/update_shell.rs +++ b/crates/uv/src/commands/tool/update_shell.rs @@ -48,89 +48,89 @@ pub(crate) async fn update_shell(printer: Printer) -> Result { "Executable directory {} is already in PATH", executable_directory.simplified_display().cyan() )?; - Ok(ExitStatus::Success) - } else { - // Determine the current shell. - let Some(shell) = Shell::from_env() else { - return Err(anyhow::anyhow!("The executable directory {} is not in PATH, but the current shell could not be determined", executable_directory.simplified_display().cyan())); - }; - - // Look up the configuration files (e.g., `.bashrc`, `.zshrc`) for the shell. - let files = shell.configuration_files(); - if files.is_empty() { - return Err(anyhow::anyhow!("The executable directory {} is not in PATH, but updating {shell} is currently unsupported", executable_directory.simplified_display().cyan())); - } + return Ok(ExitStatus::Success); + } - // Prepare the command (e.g., `export PATH="$HOME/.cargo/bin:$PATH"`). - let Some(command) = shell.prepend_path(&executable_directory) else { - return Err(anyhow::anyhow!("The executable directory {} is not in PATH, but the necessary command to update {shell} could not be determined", executable_directory.simplified_display().cyan())); - }; - - // Update each file, as necessary. - let mut updated = false; - for file in files { - // Search for the command in the file, to avoid redundant updates. - match fs_err::tokio::read_to_string(&file).await { - Ok(contents) => { - if contents.contains(&command) { - debug!( - "Skipping already-updated configuration file: {}", - file.simplified_display() - ); - continue; - } - - // Append the command to the file. - fs_err::tokio::OpenOptions::new() - .create(true) - .truncate(true) - .write(true) - .open(&file) - .await? - .write_all(format!("{contents}\n# uv\n{command}\n").as_bytes()) - .await?; - - writeln!( - printer.stderr(), - "Updated configuration file: {}", - file.simplified_display().cyan() - )?; - updated = true; - } - Err(err) if err.kind() == std::io::ErrorKind::NotFound => { - // Ensure that the directory containing the file exists. - if let Some(parent) = file.parent() { - fs_err::tokio::create_dir_all(&parent).await?; - } - - // Append the command to the file. - fs_err::tokio::OpenOptions::new() - .create(true) - .truncate(true) - .write(true) - .open(&file) - .await? - .write_all(format!("# uv\n{command}\n").as_bytes()) - .await?; - - writeln!( - printer.stderr(), - "Created configuration file: {}", - file.simplified_display().cyan() - )?; - updated = true; + // Determine the current shell. + let Some(shell) = Shell::from_env() else { + return Err(anyhow::anyhow!("The executable directory {} is not in PATH, but the current shell could not be determined", executable_directory.simplified_display().cyan())); + }; + + // Look up the configuration files (e.g., `.bashrc`, `.zshrc`) for the shell. + let files = shell.configuration_files(); + if files.is_empty() { + return Err(anyhow::anyhow!("The executable directory {} is not in PATH, but updating {shell} is currently unsupported", executable_directory.simplified_display().cyan())); + } + + // Prepare the command (e.g., `export PATH="$HOME/.cargo/bin:$PATH"`). + let Some(command) = shell.prepend_path(&executable_directory) else { + return Err(anyhow::anyhow!("The executable directory {} is not in PATH, but the necessary command to update {shell} could not be determined", executable_directory.simplified_display().cyan())); + }; + + // Update each file, as necessary. + let mut updated = false; + for file in files { + // Search for the command in the file, to avoid redundant updates. + match fs_err::tokio::read_to_string(&file).await { + Ok(contents) => { + if contents.contains(&command) { + debug!( + "Skipping already-updated configuration file: {}", + file.simplified_display() + ); + continue; } - Err(err) => { - return Err(err.into()); + + // Append the command to the file. + fs_err::tokio::OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .open(&file) + .await? + .write_all(format!("{contents}\n# uv\n{command}\n").as_bytes()) + .await?; + + writeln!( + printer.stderr(), + "Updated configuration file: {}", + file.simplified_display().cyan() + )?; + updated = true; + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + // Ensure that the directory containing the file exists. + if let Some(parent) = file.parent() { + fs_err::tokio::create_dir_all(&parent).await?; } + + // Append the command to the file. + fs_err::tokio::OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .open(&file) + .await? + .write_all(format!("# uv\n{command}\n").as_bytes()) + .await?; + + writeln!( + printer.stderr(), + "Created configuration file: {}", + file.simplified_display().cyan() + )?; + updated = true; + } + Err(err) => { + return Err(err.into()); } } + } - if updated { - writeln!(printer.stderr(), "Restart your shell to apply changes")?; - Ok(ExitStatus::Success) - } else { - Err(anyhow::anyhow!("The executable directory {} is not in PATH, but the {shell} configuration files are already up-to-date", executable_directory.simplified_display().cyan())) - } + if updated { + writeln!(printer.stderr(), "Restart your shell to apply changes")?; + Ok(ExitStatus::Success) + } else { + Err(anyhow::anyhow!("The executable directory {} is not in PATH, but the {shell} configuration files are already up-to-date", executable_directory.simplified_display().cyan())) } }