Skip to content
Open
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
74 changes: 74 additions & 0 deletions crates/uv/src/install_source.rs
Original file line number Diff line number Diff line change
@@ -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);
}
Comment on lines +24 to +32
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @woodruffw on this approach

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some things to note:

  • OsStr is case-sensitive, we may not work for case-insensitive file systems (someone would have to manually rename the path, unlikely, but something worth considering)
  • We could probably avoid allocating components and early-return, but this hyper-optimization territory

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This approach LGTM -- there are definitely some edge cases with custom prefixes, but 99.99% of Homebrew users who use a brew installed uv should have readlink -f $(which uv) match /<prefix>/Cellar/uv/....

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Another alternative to this approach is to do a PATH lookup on brew, then match that prefix to the uncanonicalized prefix for current_exe. But I think this approach is cleaner and involves fewer system calls.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note in the error case we're rarely concerned about performance. However, I agree we don't need to get fancy here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree the custom-prefix edge cases are possible (albeit rare); if we see reports there we could potentially add a fallback that compares against brew --prefix / brew --cellar, this would add more overhead/failure modes which is why we aren't doing it already.


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
);
}
}
25 changes: 21 additions & 4 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1223,10 +1227,23 @@ 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} hint: You installed uv using {}. To update uv, run:\n {}",
source.description(),
source.update_instructions(),
base = BASE_MESSAGE
),
None => format!(
"{base} Please use your package manager to update uv.",
base = BASE_MESSAGE
),
};

anyhow::bail!(message);
}
Commands::GenerateShellCompletion(args) => {
args.shell.generate(&mut Cli::command(), &mut stdout());
Expand Down
Loading