Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions crates/uv-shell/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
19 changes: 5 additions & 14 deletions crates/uv/src/commands/python/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
12 changes: 6 additions & 6 deletions crates/uv/src/commands/tool/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
156 changes: 78 additions & 78 deletions crates/uv/src/commands/tool/update_shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,89 +48,89 @@ pub(crate) async fn update_shell(printer: Printer) -> Result<ExitStatus> {
"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()))
}
}
Loading