Skip to content

Commit ee6e3be

Browse files
authored
Add brew specific message for uv self update (#16838)
Resolves #16833 `uv self update` could error with a better message if it knows the user has installed it with brew. This diff adds an `InstallSource` we can use the detect where a `uv` binary comes from and augment error messages with better context. We're only adding brew for now, but it could easily be extend with other detection heuristics.
1 parent d3cd94e commit ee6e3be

2 files changed

Lines changed: 94 additions & 4 deletions

File tree

crates/uv/src/install_source.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#![cfg(not(feature = "self-update"))]
2+
3+
use std::{
4+
ffi::OsStr,
5+
path::{Path, PathBuf},
6+
};
7+
8+
/// Known sources for uv installations.
9+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10+
pub(crate) enum InstallSource {
11+
Homebrew,
12+
}
13+
14+
impl InstallSource {
15+
/// Attempt to infer the install source for the given executable path.
16+
fn from_path(path: &Path) -> Option<Self> {
17+
let canonical = path.canonicalize().unwrap_or_else(|_| PathBuf::from(path));
18+
19+
let components = canonical
20+
.components()
21+
.map(|component| component.as_os_str().to_owned())
22+
.collect::<Vec<_>>();
23+
24+
let cellar = OsStr::new("Cellar");
25+
let formula = OsStr::new("uv");
26+
27+
if components
28+
.windows(2)
29+
.any(|window| window[0] == cellar && window[1] == formula)
30+
{
31+
return Some(Self::Homebrew);
32+
}
33+
34+
None
35+
}
36+
37+
/// Detect how uv was installed by inspecting the current executable path.
38+
pub(crate) fn detect() -> Option<Self> {
39+
Self::from_path(&std::env::current_exe().ok()?)
40+
}
41+
42+
pub(crate) fn description(self) -> &'static str {
43+
match self {
44+
Self::Homebrew => "Homebrew",
45+
}
46+
}
47+
48+
pub(crate) fn update_instructions(self) -> &'static str {
49+
match self {
50+
Self::Homebrew => "brew update && brew upgrade uv",
51+
}
52+
}
53+
}
54+
55+
#[cfg(test)]
56+
mod tests {
57+
use super::*;
58+
59+
#[test]
60+
fn detects_homebrew_cellar() {
61+
assert_eq!(
62+
InstallSource::from_path(Path::new("/opt/homebrew/Cellar/uv/0.9.11/bin/uv")),
63+
Some(InstallSource::Homebrew)
64+
);
65+
}
66+
67+
#[test]
68+
fn ignores_non_cellar_paths() {
69+
assert_eq!(
70+
InstallSource::from_path(Path::new("/usr/local/bin/uv")),
71+
None
72+
);
73+
}
74+
}

crates/uv/src/lib.rs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ use settings::PipTreeSettings;
2121
use tokio::task::spawn_blocking;
2222
use tracing::{debug, instrument, trace};
2323

24+
#[cfg(not(feature = "self-update"))]
25+
use crate::install_source::InstallSource;
2426
use uv_cache::{Cache, Refresh};
2527
use uv_cache_info::Timestamp;
2628
#[cfg(feature = "self-update")]
@@ -59,6 +61,8 @@ use crate::settings::{
5961

6062
pub(crate) mod child;
6163
pub(crate) mod commands;
64+
#[cfg(not(feature = "self-update"))]
65+
mod install_source;
6266
pub(crate) mod logging;
6367
pub(crate) mod printer;
6468
pub(crate) mod settings;
@@ -1249,10 +1253,22 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
12491253
}
12501254
#[cfg(not(feature = "self-update"))]
12511255
Commands::Self_(_) => {
1252-
anyhow::bail!(
1253-
"uv was installed through an external package manager, and self-update \
1254-
is not available. Please use your package manager to update uv."
1255-
);
1256+
const BASE_MESSAGE: &str =
1257+
"uv was installed through an external package manager and cannot update itself.";
1258+
1259+
let message = match InstallSource::detect() {
1260+
Some(source) => format!(
1261+
"{base}\n\n{hint}{colon} You installed uv using {}. To update uv, run `{}`",
1262+
source.description(),
1263+
source.update_instructions().green(),
1264+
hint = "hint".bold().cyan(),
1265+
colon = ":".bold(),
1266+
base = BASE_MESSAGE
1267+
),
1268+
None => format!("{BASE_MESSAGE} Please use your package manager to update uv."),
1269+
};
1270+
1271+
anyhow::bail!(message);
12561272
}
12571273
Commands::GenerateShellCompletion(args) => {
12581274
args.shell.generate(&mut Cli::command(), &mut stdout());

0 commit comments

Comments
 (0)