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()), } }