Skip to content
Merged
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
55 changes: 32 additions & 23 deletions crates/uv-python/src/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -967,6 +967,31 @@ impl InterpreterInfo {
pub(crate) fn query_cached(executable: &Path, cache: &Cache) -> Result<Self, Error> {
let absolute = std::path::absolute(executable)?;

// Provide a better error message if the link is broken or the file does not exist. Since
// `canonicalize_executable` does not resolve the file on Windows, we must re-use this logic
// for the subsequent metadata read as we may not have actually resolved the path.
let handle_io_error = |err: io::Error| -> Error {
if err.kind() == io::ErrorKind::NotFound {
// Check if it looks like a venv interpreter where the underlying Python
// installation was removed.
if absolute
.symlink_metadata()
.is_ok_and(|metadata| metadata.is_symlink())
{
Error::BrokenSymlink(BrokenSymlink {
path: executable.to_path_buf(),
venv: uv_fs::is_virtualenv_executable(executable),
})
} else {
Error::NotFound(executable.to_path_buf())
}
} else {
err.into()
}
};

let canonical = canonicalize_executable(&absolute).map_err(handle_io_error)?;

let cache_entry = cache.entry(
CacheBucket::Interpreter,
// Shard interpreter metadata by host architecture, operating system, and version, to
Expand All @@ -978,33 +1003,17 @@ impl InterpreterInfo {
)),
// We use the absolute path for the cache entry to avoid cache collisions for relative
// paths. But we don't want to query the executable with symbolic links resolved because
// that can change reported values, e.g., `sys.executable`.
format!("{}.msgpack", cache_digest(&absolute)),
// that can change reported values, e.g., `sys.executable`. We include the canonical
// path in the cache entry as well, otherwise we can have cache collisions if an
// absolute path refers to different interpreters with matching ctimes, e.g., if you
// have a `.venv/bin/python` pointing to both Python 3.12 and Python 3.13 that were
// modified at the same time.
format!("{}.msgpack", cache_digest(&(&absolute, &canonical))),
);

// We check the timestamp of the canonicalized executable to check if an underlying
// interpreter has been modified.
let modified = canonicalize_executable(&absolute)
.and_then(Timestamp::from_path)
.map_err(|err| {
if err.kind() == io::ErrorKind::NotFound {
// Check if it looks like a venv interpreter where the underlying Python
// installation was removed.
if absolute
.symlink_metadata()
.is_ok_and(|metadata| metadata.is_symlink())
{
Error::BrokenSymlink(BrokenSymlink {
path: executable.to_path_buf(),
venv: uv_fs::is_virtualenv_executable(executable),
})
} else {
Error::NotFound(executable.to_path_buf())
}
} else {
err.into()
}
})?;
let modified = Timestamp::from_path(canonical).map_err(handle_io_error)?;

// Read from the cache.
if cache
Expand Down
Loading