Skip to content
Merged
Show file tree
Hide file tree
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
2 changes: 1 addition & 1 deletion crates/uv-virtualenv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
34 changes: 28 additions & 6 deletions crates/uv-virtualenv/src/virtualenv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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!(
Expand Down Expand Up @@ -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)?;
Expand All @@ -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(())
}
Expand Down
8 changes: 5 additions & 3 deletions crates/uv/src/commands/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1373,16 +1374,17 @@ 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(),
"Removed virtual environment at: {}",
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()),
}
}

Expand Down
Loading