From ee6e3be815e4d4b18667e521aa3faba4321446cb Mon Sep 17 00:00:00 2001 From: liam Date: Thu, 4 Dec 2025 13:44:07 -0500 Subject: [PATCH] Add brew specific message for `uv self update` (#16838) Resolves https://github.com/astral-sh/uv/issues/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. --- crates/uv/src/install_source.rs | 74 +++++++++++++++++++++++++++++++++ crates/uv/src/lib.rs | 24 +++++++++-- 2 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 crates/uv/src/install_source.rs diff --git a/crates/uv/src/install_source.rs b/crates/uv/src/install_source.rs new file mode 100644 index 000000000..ab0e9c168 --- /dev/null +++ b/crates/uv/src/install_source.rs @@ -0,0 +1,74 @@ +#![cfg(not(feature = "self-update"))] + +use std::{ + ffi::OsStr, + path::{Path, PathBuf}, +}; + +/// Known sources for uv installations. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum InstallSource { + Homebrew, +} + +impl InstallSource { + /// Attempt to infer the install source for the given executable path. + fn from_path(path: &Path) -> Option { + let canonical = path.canonicalize().unwrap_or_else(|_| PathBuf::from(path)); + + let components = canonical + .components() + .map(|component| component.as_os_str().to_owned()) + .collect::>(); + + let cellar = OsStr::new("Cellar"); + let formula = OsStr::new("uv"); + + if components + .windows(2) + .any(|window| window[0] == cellar && window[1] == formula) + { + return Some(Self::Homebrew); + } + + None + } + + /// Detect how uv was installed by inspecting the current executable path. + pub(crate) fn detect() -> Option { + Self::from_path(&std::env::current_exe().ok()?) + } + + pub(crate) fn description(self) -> &'static str { + match self { + Self::Homebrew => "Homebrew", + } + } + + pub(crate) fn update_instructions(self) -> &'static str { + match self { + Self::Homebrew => "brew update && brew upgrade uv", + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn detects_homebrew_cellar() { + assert_eq!( + InstallSource::from_path(Path::new("/opt/homebrew/Cellar/uv/0.9.11/bin/uv")), + Some(InstallSource::Homebrew) + ); + } + + #[test] + fn ignores_non_cellar_paths() { + assert_eq!( + InstallSource::from_path(Path::new("/usr/local/bin/uv")), + None + ); + } +} diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index ff3f59b03..0ca6ca7e4 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -21,6 +21,8 @@ use settings::PipTreeSettings; use tokio::task::spawn_blocking; use tracing::{debug, instrument, trace}; +#[cfg(not(feature = "self-update"))] +use crate::install_source::InstallSource; use uv_cache::{Cache, Refresh}; use uv_cache_info::Timestamp; #[cfg(feature = "self-update")] @@ -59,6 +61,8 @@ use crate::settings::{ pub(crate) mod child; pub(crate) mod commands; +#[cfg(not(feature = "self-update"))] +mod install_source; pub(crate) mod logging; pub(crate) mod printer; pub(crate) mod settings; @@ -1249,10 +1253,22 @@ async fn run(mut cli: Cli) -> Result { } #[cfg(not(feature = "self-update"))] Commands::Self_(_) => { - anyhow::bail!( - "uv was installed through an external package manager, and self-update \ - is not available. Please use your package manager to update uv." - ); + const BASE_MESSAGE: &str = + "uv was installed through an external package manager and cannot update itself."; + + let message = match InstallSource::detect() { + Some(source) => format!( + "{base}\n\n{hint}{colon} You installed uv using {}. To update uv, run `{}`", + source.description(), + source.update_instructions().green(), + hint = "hint".bold().cyan(), + colon = ":".bold(), + base = BASE_MESSAGE + ), + None => format!("{BASE_MESSAGE} Please use your package manager to update uv."), + }; + + anyhow::bail!(message); } Commands::GenerateShellCompletion(args) => { args.shell.generate(&mut Cli::command(), &mut stdout());