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_configuration::PreviewMode;
use uv_python::{Interpreter, PythonEnvironment}; use uv_python::{Interpreter, PythonEnvironment};
pub use virtualenv::OnExisting; pub use virtualenv::{OnExisting, remove_virtualenv};
mod virtualenv; mod virtualenv;

View file

@ -97,7 +97,8 @@ pub(crate) fn create(
} }
OnExisting::Remove => { OnExisting::Remove => {
debug!("Removing existing {name} due to `--clear`"); debug!("Removing existing {name} due to `--clear`");
remove_venv_directory(location)?; remove_virtualenv(location)?;
fs::create_dir_all(location)?;
} }
OnExisting::Fail OnExisting::Fail
if location if location
@ -110,7 +111,8 @@ pub(crate) fn create(
match confirm_clear(location, name)? { match confirm_clear(location, name)? {
Some(true) => { Some(true) => {
debug!("Removing existing {name} due to confirmation"); debug!("Removing existing {name} due to confirmation");
remove_venv_directory(location)?; remove_virtualenv(location)?;
fs::create_dir_all(location)?;
} }
Some(false) => { Some(false) => {
let hint = format!( 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> { /// Perform a safe removal of a virtual environment.
// On Windows, if the current executable is in the directory, guard against pub fn remove_virtualenv(location: &Path) -> Result<(), Error> {
// self-deletion. // 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)] #[cfg(windows)]
if let Ok(itself) = std::env::current_exe() { if let Ok(itself) = std::env::current_exe() {
let target = std::path::absolute(location)?; 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::remove_dir_all(location)?;
fs::create_dir_all(location)?;
Ok(()) Ok(())
} }

View file

@ -43,6 +43,7 @@ use uv_scripts::Pep723ItemRef;
use uv_settings::PythonInstallMirrors; use uv_settings::PythonInstallMirrors;
use uv_static::EnvVars; use uv_static::EnvVars;
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy}; use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
use uv_virtualenv::remove_virtualenv;
use uv_warnings::{warn_user, warn_user_once}; use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::dependency_groups::DependencyGroupError; use uv_workspace::dependency_groups::DependencyGroupError;
use uv_workspace::pyproject::PyProjectToml; use uv_workspace::pyproject::PyProjectToml;
@ -1373,7 +1374,7 @@ impl ProjectEnvironment {
// Remove the existing virtual environment if it doesn't meet the requirements. // Remove the existing virtual environment if it doesn't meet the requirements.
if replace { if replace {
match fs_err::remove_dir_all(&root) { match remove_virtualenv(&root) {
Ok(()) => { Ok(()) => {
writeln!( writeln!(
printer.stderr(), printer.stderr(),
@ -1381,8 +1382,9 @@ impl ProjectEnvironment {
root.user_display().cyan() root.user_display().cyan()
)?; )?;
} }
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} Err(uv_virtualenv::Error::Io(err))
Err(e) => return Err(e.into()), if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(err.into()),
} }
} }