From c8486da495674686467b75709cebc8f4f11553f5 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Tue, 22 Jul 2025 08:13:38 -0500 Subject: [PATCH] Update virtual environment removal to delete `pyvenv.cfg` last (#14808) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- crates/uv-virtualenv/src/lib.rs | 2 +- crates/uv-virtualenv/src/virtualenv.rs | 34 +++++++++++++++++++++----- crates/uv/src/commands/project/mod.rs | 8 +++--- 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/crates/uv-virtualenv/src/lib.rs b/crates/uv-virtualenv/src/lib.rs index b04a500a5..bcf1e9f97 100644 --- a/crates/uv-virtualenv/src/lib.rs +++ b/crates/uv-virtualenv/src/lib.rs @@ -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; diff --git a/crates/uv-virtualenv/src/virtualenv.rs b/crates/uv-virtualenv/src/virtualenv.rs index fb22a0724..2227b71b9 100644 --- a/crates/uv-virtualenv/src/virtualenv.rs +++ b/crates/uv-virtualenv/src/virtualenv.rs @@ -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, 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(()) } diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index becd2a26e..3ad530edd 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -43,6 +43,7 @@ use uv_scripts::Pep723ItemRef; use uv_settings::PythonInstallMirrors; use uv_static::EnvVars; use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy}; +use uv_virtualenv::remove_virtualenv; use uv_warnings::{warn_user, warn_user_once}; use uv_workspace::dependency_groups::DependencyGroupError; use uv_workspace::pyproject::PyProjectToml; @@ -1373,7 +1374,7 @@ impl ProjectEnvironment { // Remove the existing virtual environment if it doesn't meet the requirements. if replace { - match fs_err::remove_dir_all(&root) { + match remove_virtualenv(&root) { Ok(()) => { writeln!( printer.stderr(), @@ -1381,8 +1382,9 @@ impl ProjectEnvironment { root.user_display().cyan() )?; } - Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} - Err(e) => return Err(e.into()), + Err(uv_virtualenv::Error::Io(err)) + if err.kind() == std::io::ErrorKind::NotFound => {} + Err(err) => return Err(err.into()), } }