Update virtual environment removal to delete pyvenv.cfg last (#14808)

An alternative to https://github.com/astral-sh/uv/pull/14569

This isn't a complete solution to
https://github.com/astral-sh/uv/issues/13986, in the sense that it's
still "fatal" to `uv sync` if we fail to delete an environment, but I
think that's okay — deferring deletion is much more complicated. This at
least doesn't break users once the deletion fails. The downside is we'll
generally treat this virtual environment is valid, even if we nuked a
bunch of it.

Closes https://github.com/astral-sh/uv/issues/13986
This commit is contained in:
Zanie Blue 2025-07-22 08:13:38 -05:00 committed by GitHub
parent 8bffa693b4
commit c8486da495
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 34 additions and 10 deletions

View file

@ -6,7 +6,7 @@ use thiserror::Error;
use uv_configuration::PreviewMode;
use uv_python::{Interpreter, PythonEnvironment};
pub use virtualenv::OnExisting;
pub use virtualenv::{OnExisting, remove_virtualenv};
mod virtualenv;

View file

@ -97,7 +97,8 @@ pub(crate) fn create(
}
OnExisting::Remove => {
debug!("Removing existing {name} due to `--clear`");
remove_venv_directory(location)?;
remove_virtualenv(location)?;
fs::create_dir_all(location)?;
}
OnExisting::Fail
if location
@ -110,7 +111,8 @@ pub(crate) fn create(
match confirm_clear(location, name)? {
Some(true) => {
debug!("Removing existing {name} due to confirmation");
remove_venv_directory(location)?;
remove_virtualenv(location)?;
fs::create_dir_all(location)?;
}
Some(false) => {
let hint = format!(
@ -566,9 +568,10 @@ fn confirm_clear(location: &Path, name: &'static str) -> Result<Option<bool>, io
}
}
fn remove_venv_directory(location: &Path) -> Result<(), Error> {
// On Windows, if the current executable is in the directory, guard against
// self-deletion.
/// Perform a safe removal of a virtual environment.
pub fn remove_virtualenv(location: &Path) -> Result<(), Error> {
// On Windows, if the current executable is in the directory, defer self-deletion since Windows
// won't let you unlink a running executable.
#[cfg(windows)]
if let Ok(itself) = std::env::current_exe() {
let target = std::path::absolute(location)?;
@ -578,8 +581,27 @@ fn remove_venv_directory(location: &Path) -> Result<(), Error> {
}
}
// We defer removal of the `pyvenv.cfg` until the end, so if we fail to remove the environment,
// uv can still identify it as a Python virtual environment that can be deleted.
for entry in fs::read_dir(location)? {
let entry = entry?;
let path = entry.path();
if path == location.join("pyvenv.cfg") {
continue;
}
if path.is_dir() {
fs::remove_dir_all(&path)?;
} else {
fs::remove_file(&path)?;
}
}
match fs::remove_file(location.join("pyvenv.cfg")) {
Ok(()) => {}
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
Err(err) => return Err(err.into()),
}
fs::remove_dir_all(location)?;
fs::create_dir_all(location)?;
Ok(())
}