Skip to content

Commit 8dd079f

Browse files
Suggest uv tool update-shell in PowerShell (#11846)
## Summary We need to decouple the "Is this shell supported by `update-shell`?" logic from the "Does this shell have known configuration files?" logic, specifically for Windows, which we can always update but not via configuration files. Closes #11803.
1 parent 3398cb1 commit 8dd079f

File tree

4 files changed

+97
-98
lines changed

4 files changed

+97
-98
lines changed

crates/uv-shell/src/lib.rs

+8
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,14 @@ impl Shell {
8282
parse_shell_from_path(path.as_ref())
8383
}
8484

85+
/// Returns `true` if the shell supports a `PATH` update command.
86+
pub fn supports_update(self) -> bool {
87+
match self {
88+
Shell::Powershell | Shell::Cmd => true,
89+
shell => !shell.configuration_files().is_empty(),
90+
}
91+
}
92+
8593
/// Return the configuration files that should be modified to append to a shell's `PATH`.
8694
///
8795
/// Some of the logic here is based on rustup's rc file detection.

crates/uv/src/commands/python/install.rs

+5-14
Original file line numberDiff line numberDiff line change
@@ -709,20 +709,11 @@ fn warn_if_not_on_path(bin: &Path) {
709709
if !Shell::contains_path(bin) {
710710
if let Some(shell) = Shell::from_env() {
711711
if let Some(command) = shell.prepend_path(bin) {
712-
if shell.configuration_files().is_empty() {
713-
warn_user!(
714-
"`{}` is not on your PATH. To use the installed Python executable, run `{}`.",
715-
bin.simplified_display().cyan(),
716-
command.green()
717-
);
718-
} else {
719-
// TODO(zanieb): Update when we add `uv python update-shell` to match `uv tool`
720-
warn_user!(
721-
"`{}` is not on your PATH. To use the installed Python executable, run `{}`.",
722-
bin.simplified_display().cyan(),
723-
command.green(),
724-
);
725-
}
712+
warn_user!(
713+
"`{}` is not on your PATH. To use the installed Python executable, run `{}`.",
714+
bin.simplified_display().cyan(),
715+
command.green(),
716+
);
726717
} else {
727718
warn_user!(
728719
"`{}` is not on your PATH. To use the installed Python executable, add the directory to your PATH.",

crates/uv/src/commands/tool/common.rs

+6-6
Original file line numberDiff line numberDiff line change
@@ -301,18 +301,18 @@ pub(crate) fn install_executables(
301301
if !Shell::contains_path(&executable_directory) {
302302
if let Some(shell) = Shell::from_env() {
303303
if let Some(command) = shell.prepend_path(&executable_directory) {
304-
if shell.configuration_files().is_empty() {
304+
if shell.supports_update() {
305305
warn_user!(
306-
"`{}` is not on your PATH. To use installed tools, run `{}`.",
306+
"`{}` is not on your PATH. To use installed tools, run `{}` or `{}`.",
307307
executable_directory.simplified_display().cyan(),
308-
command.green()
308+
command.green(),
309+
"uv tool update-shell".green()
309310
);
310311
} else {
311312
warn_user!(
312-
"`{}` is not on your PATH. To use installed tools, run `{}` or `{}`.",
313+
"`{}` is not on your PATH. To use installed tools, run `{}`.",
313314
executable_directory.simplified_display().cyan(),
314-
command.green(),
315-
"uv tool update-shell".green()
315+
command.green()
316316
);
317317
}
318318
} else {

crates/uv/src/commands/tool/update_shell.rs

+78-78
Original file line numberDiff line numberDiff line change
@@ -48,89 +48,89 @@ pub(crate) async fn update_shell(printer: Printer) -> Result<ExitStatus> {
4848
"Executable directory {} is already in PATH",
4949
executable_directory.simplified_display().cyan()
5050
)?;
51-
Ok(ExitStatus::Success)
52-
} else {
53-
// Determine the current shell.
54-
let Some(shell) = Shell::from_env() else {
55-
return Err(anyhow::anyhow!("The executable directory {} is not in PATH, but the current shell could not be determined", executable_directory.simplified_display().cyan()));
56-
};
57-
58-
// Look up the configuration files (e.g., `.bashrc`, `.zshrc`) for the shell.
59-
let files = shell.configuration_files();
60-
if files.is_empty() {
61-
return Err(anyhow::anyhow!("The executable directory {} is not in PATH, but updating {shell} is currently unsupported", executable_directory.simplified_display().cyan()));
62-
}
51+
return Ok(ExitStatus::Success);
52+
}
6353

64-
// Prepare the command (e.g., `export PATH="$HOME/.cargo/bin:$PATH"`).
65-
let Some(command) = shell.prepend_path(&executable_directory) else {
66-
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()));
67-
};
68-
69-
// Update each file, as necessary.
70-
let mut updated = false;
71-
for file in files {
72-
// Search for the command in the file, to avoid redundant updates.
73-
match fs_err::tokio::read_to_string(&file).await {
74-
Ok(contents) => {
75-
if contents.contains(&command) {
76-
debug!(
77-
"Skipping already-updated configuration file: {}",
78-
file.simplified_display()
79-
);
80-
continue;
81-
}
82-
83-
// Append the command to the file.
84-
fs_err::tokio::OpenOptions::new()
85-
.create(true)
86-
.truncate(true)
87-
.write(true)
88-
.open(&file)
89-
.await?
90-
.write_all(format!("{contents}\n# uv\n{command}\n").as_bytes())
91-
.await?;
92-
93-
writeln!(
94-
printer.stderr(),
95-
"Updated configuration file: {}",
96-
file.simplified_display().cyan()
97-
)?;
98-
updated = true;
99-
}
100-
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
101-
// Ensure that the directory containing the file exists.
102-
if let Some(parent) = file.parent() {
103-
fs_err::tokio::create_dir_all(&parent).await?;
104-
}
105-
106-
// Append the command to the file.
107-
fs_err::tokio::OpenOptions::new()
108-
.create(true)
109-
.truncate(true)
110-
.write(true)
111-
.open(&file)
112-
.await?
113-
.write_all(format!("# uv\n{command}\n").as_bytes())
114-
.await?;
115-
116-
writeln!(
117-
printer.stderr(),
118-
"Created configuration file: {}",
119-
file.simplified_display().cyan()
120-
)?;
121-
updated = true;
54+
// Determine the current shell.
55+
let Some(shell) = Shell::from_env() else {
56+
return Err(anyhow::anyhow!("The executable directory {} is not in PATH, but the current shell could not be determined", executable_directory.simplified_display().cyan()));
57+
};
58+
59+
// Look up the configuration files (e.g., `.bashrc`, `.zshrc`) for the shell.
60+
let files = shell.configuration_files();
61+
if files.is_empty() {
62+
return Err(anyhow::anyhow!("The executable directory {} is not in PATH, but updating {shell} is currently unsupported", executable_directory.simplified_display().cyan()));
63+
}
64+
65+
// Prepare the command (e.g., `export PATH="$HOME/.cargo/bin:$PATH"`).
66+
let Some(command) = shell.prepend_path(&executable_directory) else {
67+
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()));
68+
};
69+
70+
// Update each file, as necessary.
71+
let mut updated = false;
72+
for file in files {
73+
// Search for the command in the file, to avoid redundant updates.
74+
match fs_err::tokio::read_to_string(&file).await {
75+
Ok(contents) => {
76+
if contents.contains(&command) {
77+
debug!(
78+
"Skipping already-updated configuration file: {}",
79+
file.simplified_display()
80+
);
81+
continue;
12282
}
123-
Err(err) => {
124-
return Err(err.into());
83+
84+
// Append the command to the file.
85+
fs_err::tokio::OpenOptions::new()
86+
.create(true)
87+
.truncate(true)
88+
.write(true)
89+
.open(&file)
90+
.await?
91+
.write_all(format!("{contents}\n# uv\n{command}\n").as_bytes())
92+
.await?;
93+
94+
writeln!(
95+
printer.stderr(),
96+
"Updated configuration file: {}",
97+
file.simplified_display().cyan()
98+
)?;
99+
updated = true;
100+
}
101+
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
102+
// Ensure that the directory containing the file exists.
103+
if let Some(parent) = file.parent() {
104+
fs_err::tokio::create_dir_all(&parent).await?;
125105
}
106+
107+
// Append the command to the file.
108+
fs_err::tokio::OpenOptions::new()
109+
.create(true)
110+
.truncate(true)
111+
.write(true)
112+
.open(&file)
113+
.await?
114+
.write_all(format!("# uv\n{command}\n").as_bytes())
115+
.await?;
116+
117+
writeln!(
118+
printer.stderr(),
119+
"Created configuration file: {}",
120+
file.simplified_display().cyan()
121+
)?;
122+
updated = true;
123+
}
124+
Err(err) => {
125+
return Err(err.into());
126126
}
127127
}
128+
}
128129

129-
if updated {
130-
writeln!(printer.stderr(), "Restart your shell to apply changes")?;
131-
Ok(ExitStatus::Success)
132-
} else {
133-
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()))
134-
}
130+
if updated {
131+
writeln!(printer.stderr(), "Restart your shell to apply changes")?;
132+
Ok(ExitStatus::Success)
133+
} else {
134+
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()))
135135
}
136136
}

0 commit comments

Comments
 (0)