Files
uv/crates/uv-virtualenv/src/virtualenv.rs
T
John Mumm 2df06ebfbc Require uv venv --clear before removing an existing directory (#14309)
By default, `uv venv <venv-name>` currently removes the `<venv-name`>
directory if it exists. This can be surprising behavior: not everyone
expects an existing environment to be overwritten. This PR updates the
default to fail if a non-empty `<venv-name>` directory already exists
and neither `--allow-existing` nor the new `-c/--clear` option is
provided (if a TTY is detected, it prompts first). If it's not a TTY,
then uv will only warn and not fail for now — we'll make this an error
in the future. I've also added a corresponding `UV_VENV_CLEAR` env var.

I've chosen to use `--clear` instead of `--force` for this option
because it is used by the `venv` module and `virtualenv` and will be
familiar to users. I also think its meaning is clearer in this context
than `--force` (which could plausibly mean force overwrite just the
virtual environment files, which is what our current `--allow-existing`
option does).

Closes #1472.

---------

Co-authored-by: Zanie Blue <contact@zanie.dev>
2025-07-17 17:20:21 -05:00

768 lines
27 KiB
Rust

//! Create a virtual environment.
use std::env::consts::EXE_SUFFIX;
use std::io;
use std::io::{BufWriter, Write};
use std::path::Path;
use console::Term;
use fs_err as fs;
use fs_err::File;
use itertools::Itertools;
use owo_colors::OwoColorize;
use tracing::debug;
use uv_configuration::PreviewMode;
use uv_fs::{CWD, Simplified, cachedir};
use uv_pypi_types::Scheme;
use uv_python::managed::{PythonMinorVersionLink, create_link_to_executable};
use uv_python::{Interpreter, VirtualEnvironment};
use uv_shell::escape_posix_for_single_quotes;
use uv_version::version;
use uv_warnings::warn_user_once;
use crate::{Error, Prompt};
/// Activation scripts for the environment, with dependent paths templated out.
const ACTIVATE_TEMPLATES: &[(&str, &str)] = &[
("activate", include_str!("activator/activate")),
("activate.csh", include_str!("activator/activate.csh")),
("activate.fish", include_str!("activator/activate.fish")),
("activate.nu", include_str!("activator/activate.nu")),
("activate.ps1", include_str!("activator/activate.ps1")),
("activate.bat", include_str!("activator/activate.bat")),
("deactivate.bat", include_str!("activator/deactivate.bat")),
("pydoc.bat", include_str!("activator/pydoc.bat")),
(
"activate_this.py",
include_str!("activator/activate_this.py"),
),
];
const VIRTUALENV_PATCH: &str = include_str!("_virtualenv.py");
/// Very basic `.cfg` file format writer.
fn write_cfg(f: &mut impl Write, data: &[(String, String)]) -> io::Result<()> {
for (key, value) in data {
writeln!(f, "{key} = {value}")?;
}
Ok(())
}
/// Create a [`VirtualEnvironment`] at the given location.
#[allow(clippy::fn_params_excessive_bools)]
pub(crate) fn create(
location: &Path,
interpreter: &Interpreter,
prompt: Prompt,
system_site_packages: bool,
on_existing: OnExisting,
relocatable: bool,
seed: bool,
upgradeable: bool,
preview: PreviewMode,
) -> Result<VirtualEnvironment, Error> {
// Determine the base Python executable; that is, the Python executable that should be
// considered the "base" for the virtual environment.
//
// For consistency with the standard library, rely on `sys._base_executable`, _unless_ we're
// using a uv-managed Python (in which case, we can do better for symlinked executables).
let base_python = if cfg!(unix) && interpreter.is_standalone() {
interpreter.find_base_python()?
} else {
interpreter.to_base_python()?
};
debug!(
"Using base executable for virtual environment: {}",
base_python.display()
);
// Validate the existing location.
match location.metadata() {
Ok(metadata) if metadata.is_file() => {
return Err(Error::Io(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("File exists at `{}`", location.user_display()),
)));
}
Ok(metadata) if metadata.is_dir() => {
let name = if uv_fs::is_virtualenv_base(location) {
"virtual environment"
} else {
"directory"
};
match on_existing {
OnExisting::Allow => {
debug!("Allowing existing {name} due to `--allow-existing`");
}
OnExisting::Remove => {
debug!("Removing existing {name} due to `--clear`");
remove_venv_directory(location)?;
}
OnExisting::Fail
if location
.read_dir()
.is_ok_and(|mut dir| dir.next().is_none()) =>
{
debug!("Ignoring empty directory");
}
OnExisting::Fail => {
match confirm_clear(location, name)? {
Some(true) => {
debug!("Removing existing {name} due to confirmation");
remove_venv_directory(location)?;
}
Some(false) => {
let hint = format!(
"Use the `{}` flag or set `{}` to replace the existing {name}",
"--clear".green(),
"UV_VENV_CLEAR=1".green()
);
return Err(Error::Io(io::Error::new(
io::ErrorKind::AlreadyExists,
format!(
"A {name} already exists at: {}\n\n{}{} {hint}",
location.user_display(),
"hint".bold().cyan(),
":".bold(),
),
)));
}
// When we don't have a TTY, warn that the behavior will change in the future
None => {
warn_user_once!(
"A {name} already exists at `{}`. In the future, uv will require `{}` to replace it",
location.user_display(),
"--clear".green(),
);
}
}
}
}
}
Ok(_) => {
// It's not a file or a directory
return Err(Error::Io(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("Object already exists at `{}`", location.user_display()),
)));
}
Err(err) if err.kind() == io::ErrorKind::NotFound => {
fs::create_dir_all(location)?;
}
Err(err) => return Err(Error::Io(err)),
}
let location = std::path::absolute(location)?;
let bin_name = if cfg!(unix) {
"bin"
} else if cfg!(windows) {
"Scripts"
} else {
unimplemented!("Only Windows and Unix are supported")
};
let scripts = location.join(&interpreter.virtualenv().scripts);
let prompt = match prompt {
Prompt::CurrentDirectoryName => CWD
.file_name()
.map(|name| name.to_string_lossy().to_string()),
Prompt::Static(value) => Some(value),
Prompt::None => None,
};
// Add the CACHEDIR.TAG.
cachedir::ensure_tag(&location)?;
// Create a `.gitignore` file to ignore all files in the venv.
fs::write(location.join(".gitignore"), "*")?;
let mut using_minor_version_link = false;
let executable_target = if upgradeable && interpreter.is_standalone() {
if let Some(minor_version_link) = PythonMinorVersionLink::from_executable(
base_python.as_path(),
&interpreter.key(),
preview,
) {
if !minor_version_link.exists() {
base_python.clone()
} else {
let debug_symlink_term = if cfg!(windows) {
"junction"
} else {
"symlink directory"
};
debug!(
"Using {} {} instead of base Python path: {}",
debug_symlink_term,
&minor_version_link.symlink_directory.display(),
&base_python.display()
);
using_minor_version_link = true;
minor_version_link.symlink_executable.clone()
}
} else {
base_python.clone()
}
} else {
base_python.clone()
};
// Per PEP 405, the Python `home` is the parent directory of the interpreter.
// In preview mode, for standalone interpreters, this `home` value will include a
// symlink directory on Unix or junction on Windows to enable transparent Python patch
// upgrades.
let python_home = executable_target
.parent()
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
"The Python interpreter needs to have a parent directory",
)
})?
.to_path_buf();
let python_home = python_home.as_path();
// Different names for the python interpreter
fs::create_dir_all(&scripts)?;
let executable = scripts.join(format!("python{EXE_SUFFIX}"));
#[cfg(unix)]
{
uv_fs::replace_symlink(&executable_target, &executable)?;
uv_fs::replace_symlink(
"python",
scripts.join(format!("python{}", interpreter.python_major())),
)?;
uv_fs::replace_symlink(
"python",
scripts.join(format!(
"python{}.{}",
interpreter.python_major(),
interpreter.python_minor(),
)),
)?;
if interpreter.markers().implementation_name() == "pypy" {
uv_fs::replace_symlink(
"python",
scripts.join(format!("pypy{}", interpreter.python_major())),
)?;
uv_fs::replace_symlink("python", scripts.join("pypy"))?;
}
if interpreter.markers().implementation_name() == "graalpy" {
uv_fs::replace_symlink("python", scripts.join("graalpy"))?;
}
}
// On Windows, we use trampolines that point to an executable target. For standalone
// interpreters, this target path includes a minor version junction to enable
// transparent upgrades.
if cfg!(windows) {
if using_minor_version_link {
let target = scripts.join(WindowsExecutable::Python.exe(interpreter));
create_link_to_executable(target.as_path(), executable_target.clone())
.map_err(Error::Python)?;
let targetw = scripts.join(WindowsExecutable::Pythonw.exe(interpreter));
create_link_to_executable(targetw.as_path(), executable_target)
.map_err(Error::Python)?;
} else {
copy_launcher_windows(
WindowsExecutable::Python,
interpreter,
&base_python,
&scripts,
python_home,
)?;
if interpreter.markers().implementation_name() == "graalpy" {
copy_launcher_windows(
WindowsExecutable::GraalPy,
interpreter,
&base_python,
&scripts,
python_home,
)?;
copy_launcher_windows(
WindowsExecutable::PythonMajor,
interpreter,
&base_python,
&scripts,
python_home,
)?;
} else {
copy_launcher_windows(
WindowsExecutable::Pythonw,
interpreter,
&base_python,
&scripts,
python_home,
)?;
}
if interpreter.markers().implementation_name() == "pypy" {
copy_launcher_windows(
WindowsExecutable::PythonMajor,
interpreter,
&base_python,
&scripts,
python_home,
)?;
copy_launcher_windows(
WindowsExecutable::PythonMajorMinor,
interpreter,
&base_python,
&scripts,
python_home,
)?;
copy_launcher_windows(
WindowsExecutable::PyPy,
interpreter,
&base_python,
&scripts,
python_home,
)?;
copy_launcher_windows(
WindowsExecutable::PyPyMajor,
interpreter,
&base_python,
&scripts,
python_home,
)?;
copy_launcher_windows(
WindowsExecutable::PyPyMajorMinor,
interpreter,
&base_python,
&scripts,
python_home,
)?;
copy_launcher_windows(
WindowsExecutable::PyPyw,
interpreter,
&base_python,
&scripts,
python_home,
)?;
copy_launcher_windows(
WindowsExecutable::PyPyMajorMinorw,
interpreter,
&base_python,
&scripts,
python_home,
)?;
}
}
}
#[cfg(not(any(unix, windows)))]
{
compile_error!("Only Windows and Unix are supported")
}
// Add all the activate scripts for different shells
for (name, template) in ACTIVATE_TEMPLATES {
let path_sep = if cfg!(windows) { ";" } else { ":" };
let relative_site_packages = [
interpreter.virtualenv().purelib.as_path(),
interpreter.virtualenv().platlib.as_path(),
]
.iter()
.dedup()
.map(|path| {
pathdiff::diff_paths(path, &interpreter.virtualenv().scripts)
.expect("Failed to calculate relative path to site-packages")
})
.map(|path| path.simplified().to_str().unwrap().replace('\\', "\\\\"))
.join(path_sep);
let virtual_env_dir = match (relocatable, name.to_owned()) {
(true, "activate") => {
r#"'"$(dirname -- "$(dirname -- "$(realpath -- "$SCRIPT_PATH")")")"'"#.to_string()
}
(true, "activate.bat") => r"%~dp0..".to_string(),
(true, "activate.fish") => {
r#"'"$(dirname -- "$(cd "$(dirname -- "$(status -f)")"; and pwd)")"'"#.to_string()
}
// Note:
// * relocatable activate scripts appear not to be possible in csh and nu shell
// * `activate.ps1` is already relocatable by default.
_ => escape_posix_for_single_quotes(location.simplified().to_str().unwrap()),
};
let activator = template
.replace("{{ VIRTUAL_ENV_DIR }}", &virtual_env_dir)
.replace("{{ BIN_NAME }}", bin_name)
.replace(
"{{ VIRTUAL_PROMPT }}",
prompt.as_deref().unwrap_or_default(),
)
.replace("{{ PATH_SEP }}", path_sep)
.replace("{{ RELATIVE_SITE_PACKAGES }}", &relative_site_packages);
fs::write(scripts.join(name), activator)?;
}
let mut pyvenv_cfg_data: Vec<(String, String)> = vec![
(
"home".to_string(),
python_home.simplified_display().to_string(),
),
(
"implementation".to_string(),
interpreter
.markers()
.platform_python_implementation()
.to_string(),
),
("uv".to_string(), version().to_string()),
(
"version_info".to_string(),
interpreter.markers().python_full_version().string.clone(),
),
(
"include-system-site-packages".to_string(),
if system_site_packages {
"true".to_string()
} else {
"false".to_string()
},
),
];
if relocatable {
pyvenv_cfg_data.push(("relocatable".to_string(), "true".to_string()));
}
if seed {
pyvenv_cfg_data.push(("seed".to_string(), "true".to_string()));
}
if let Some(prompt) = prompt {
pyvenv_cfg_data.push(("prompt".to_string(), prompt));
}
if cfg!(windows) && interpreter.markers().implementation_name() == "graalpy" {
pyvenv_cfg_data.push((
"venvlauncher_command".to_string(),
python_home
.join("graalpy.exe")
.simplified_display()
.to_string(),
));
}
let mut pyvenv_cfg = BufWriter::new(File::create(location.join("pyvenv.cfg"))?);
write_cfg(&mut pyvenv_cfg, &pyvenv_cfg_data)?;
drop(pyvenv_cfg);
// Construct the path to the `site-packages` directory.
let site_packages = location.join(&interpreter.virtualenv().purelib);
fs::create_dir_all(&site_packages)?;
// If necessary, create a symlink from `lib64` to `lib`.
// See: https://github.com/python/cpython/blob/b228655c227b2ca298a8ffac44d14ce3d22f6faa/Lib/venv/__init__.py#L135C11-L135C16
#[cfg(unix)]
if interpreter.pointer_size().is_64()
&& interpreter.markers().os_name() == "posix"
&& interpreter.markers().sys_platform() != "darwin"
{
match fs_err::os::unix::fs::symlink("lib", location.join("lib64")) {
Ok(()) => {}
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
Err(err) => {
return Err(err.into());
}
}
}
// Populate `site-packages` with a `_virtualenv.py` file.
fs::write(site_packages.join("_virtualenv.py"), VIRTUALENV_PATCH)?;
fs::write(site_packages.join("_virtualenv.pth"), "import _virtualenv")?;
Ok(VirtualEnvironment {
scheme: Scheme {
purelib: location.join(&interpreter.virtualenv().purelib),
platlib: location.join(&interpreter.virtualenv().platlib),
scripts: location.join(&interpreter.virtualenv().scripts),
data: location.join(&interpreter.virtualenv().data),
include: location.join(&interpreter.virtualenv().include),
},
root: location,
executable,
base_executable: base_python,
})
}
/// Prompt a confirmation that the virtual environment should be cleared.
///
/// If not a TTY, returns `None`.
fn confirm_clear(location: &Path, name: &'static str) -> Result<Option<bool>, io::Error> {
let term = Term::stderr();
if term.is_term() {
let prompt = format!(
"A {name} already exists at `{}`. Do you want to replace it?",
location.user_display(),
);
let hint = format!(
"Use the `{}` flag or set `{}` to skip this prompt",
"--clear".green(),
"UV_VENV_CLEAR=1".green()
);
Ok(Some(uv_console::confirm_with_hint(
&prompt, &hint, &term, true,
)?))
} else {
Ok(None)
}
}
fn remove_venv_directory(location: &Path) -> Result<(), Error> {
// On Windows, if the current executable is in the directory, guard against
// self-deletion.
#[cfg(windows)]
if let Ok(itself) = std::env::current_exe() {
let target = std::path::absolute(location)?;
if itself.starts_with(&target) {
debug!("Detected self-delete of executable: {}", itself.display());
self_replace::self_delete_outside_path(location)?;
}
}
fs::remove_dir_all(location)?;
fs::create_dir_all(location)?;
Ok(())
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
pub enum OnExisting {
/// Fail if the directory already exists and is non-empty.
#[default]
Fail,
/// Allow an existing directory, overwriting virtual environment files while retaining other
/// files in the directory.
Allow,
/// Remove an existing directory.
Remove,
}
impl OnExisting {
pub fn from_args(allow_existing: bool, clear: bool) -> Self {
if allow_existing {
OnExisting::Allow
} else if clear {
OnExisting::Remove
} else {
OnExisting::default()
}
}
}
#[derive(Debug, Copy, Clone)]
enum WindowsExecutable {
/// The `python.exe` executable (or `venvlauncher.exe` launcher shim).
Python,
/// The `python3.exe` executable (or `venvlauncher.exe` launcher shim).
PythonMajor,
/// The `python3.<minor>.exe` executable (or `venvlauncher.exe` launcher shim).
PythonMajorMinor,
/// The `pythonw.exe` executable (or `venvwlauncher.exe` launcher shim).
Pythonw,
/// The `pypy.exe` executable.
PyPy,
/// The `pypy3.exe` executable.
PyPyMajor,
/// The `pypy3.<minor>.exe` executable.
PyPyMajorMinor,
/// The `pypyw.exe` executable.
PyPyw,
/// The `pypy3.<minor>w.exe` executable.
PyPyMajorMinorw,
// The `graalpy.exe` executable
GraalPy,
}
impl WindowsExecutable {
/// The name of the Python executable.
fn exe(self, interpreter: &Interpreter) -> String {
match self {
WindowsExecutable::Python => String::from("python.exe"),
WindowsExecutable::PythonMajor => {
format!("python{}.exe", interpreter.python_major())
}
WindowsExecutable::PythonMajorMinor => {
format!(
"python{}.{}.exe",
interpreter.python_major(),
interpreter.python_minor()
)
}
WindowsExecutable::Pythonw => String::from("pythonw.exe"),
WindowsExecutable::PyPy => String::from("pypy.exe"),
WindowsExecutable::PyPyMajor => {
format!("pypy{}.exe", interpreter.python_major())
}
WindowsExecutable::PyPyMajorMinor => {
format!(
"pypy{}.{}.exe",
interpreter.python_major(),
interpreter.python_minor()
)
}
WindowsExecutable::PyPyw => String::from("pypyw.exe"),
WindowsExecutable::PyPyMajorMinorw => {
format!(
"pypy{}.{}w.exe",
interpreter.python_major(),
interpreter.python_minor()
)
}
WindowsExecutable::GraalPy => String::from("graalpy.exe"),
}
}
/// The name of the launcher shim.
fn launcher(self, interpreter: &Interpreter) -> &'static str {
match self {
Self::Python | Self::PythonMajor | Self::PythonMajorMinor
if interpreter.gil_disabled() =>
{
"venvlaunchert.exe"
}
Self::Python | Self::PythonMajor | Self::PythonMajorMinor => "venvlauncher.exe",
Self::Pythonw if interpreter.gil_disabled() => "venvwlaunchert.exe",
Self::Pythonw => "venvwlauncher.exe",
// From 3.13 on these should replace the `python.exe` and `pythonw.exe` shims.
// These are not relevant as of now for PyPy as it doesn't yet support Python 3.13.
Self::PyPy | Self::PyPyMajor | Self::PyPyMajorMinor => "venvlauncher.exe",
Self::PyPyw | Self::PyPyMajorMinorw => "venvwlauncher.exe",
WindowsExecutable::GraalPy => "venvlauncher.exe",
}
}
}
/// <https://github.com/python/cpython/blob/d457345bbc6414db0443819290b04a9a4333313d/Lib/venv/__init__.py#L261-L267>
/// <https://github.com/pypa/virtualenv/blob/d9fdf48d69f0d0ca56140cf0381edbb5d6fe09f5/src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py#L78-L83>
///
/// There's two kinds of applications on windows: Those that allocate a console (python.exe)
/// and those that don't because they use window(s) (pythonw.exe).
fn copy_launcher_windows(
executable: WindowsExecutable,
interpreter: &Interpreter,
base_python: &Path,
scripts: &Path,
python_home: &Path,
) -> Result<(), Error> {
// First priority: the `python.exe` and `pythonw.exe` shims.
let shim = interpreter
.stdlib()
.join("venv")
.join("scripts")
.join("nt")
.join(executable.exe(interpreter));
match fs_err::copy(shim, scripts.join(executable.exe(interpreter))) {
Ok(_) => return Ok(()),
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
Err(err) => {
return Err(err.into());
}
}
// Second priority: the `venvlauncher.exe` and `venvwlauncher.exe` shims.
// These are equivalent to the `python.exe` and `pythonw.exe` shims, which were
// renamed in Python 3.13.
let shim = interpreter
.stdlib()
.join("venv")
.join("scripts")
.join("nt")
.join(executable.launcher(interpreter));
match fs_err::copy(shim, scripts.join(executable.exe(interpreter))) {
Ok(_) => return Ok(()),
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
Err(err) => {
return Err(err.into());
}
}
// Third priority: on Conda at least, we can look for the launcher shim next to
// the Python executable itself.
let shim = base_python.with_file_name(executable.launcher(interpreter));
match fs_err::copy(shim, scripts.join(executable.exe(interpreter))) {
Ok(_) => return Ok(()),
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
Err(err) => {
return Err(err.into());
}
}
// Fourth priority: if the launcher shim doesn't exist, assume this is
// an embedded Python. Copy the Python executable itself, along with
// the DLLs, `.pyd` files, and `.zip` files in the same directory.
match fs_err::copy(
base_python.with_file_name(executable.exe(interpreter)),
scripts.join(executable.exe(interpreter)),
) {
Ok(_) => {
// Copy `.dll` and `.pyd` files from the top-level, and from the
// `DLLs` subdirectory (if it exists).
for directory in [
python_home,
interpreter.sys_base_prefix().join("DLLs").as_path(),
] {
let entries = match fs_err::read_dir(directory) {
Ok(read_dir) => read_dir,
Err(err) if err.kind() == io::ErrorKind::NotFound => {
continue;
}
Err(err) => {
return Err(err.into());
}
};
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.extension().is_some_and(|ext| {
ext.eq_ignore_ascii_case("dll") || ext.eq_ignore_ascii_case("pyd")
}) {
if let Some(file_name) = path.file_name() {
fs_err::copy(&path, scripts.join(file_name))?;
}
}
}
}
// Copy `.zip` files from the top-level.
match fs_err::read_dir(python_home) {
Ok(entries) => {
for entry in entries {
let entry = entry?;
let path = entry.path();
if path
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("zip"))
{
if let Some(file_name) = path.file_name() {
fs_err::copy(&path, scripts.join(file_name))?;
}
}
}
}
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
Err(err) => {
return Err(err.into());
}
}
return Ok(());
}
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
Err(err) => {
return Err(err.into());
}
}
Err(Error::NotFound(base_python.user_display().to_string()))
}