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.
This commit is contained in:
liam 2025-12-04 13:44:07 -05:00 committed by GitHub
parent d3cd94ecaf
commit ee6e3be815
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 94 additions and 4 deletions

View File

@ -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<Self> {
let canonical = path.canonicalize().unwrap_or_else(|_| PathBuf::from(path));
let components = canonical
.components()
.map(|component| component.as_os_str().to_owned())
.collect::<Vec<_>>();
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> {
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
);
}
}

View File

@ -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<ExitStatus> {
}
#[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());