Avoid removing empty directories when constructing virtual environments (#14822)

Closes https://github.com/astral-sh/uv/issues/14815

I tested this with the docker-compose reproduction. You can also see a
regression test change at
2ae4464b7e
This commit is contained in:
Zanie Blue 2025-07-22 13:50:14 -05:00 committed by GitHub
parent f0151f3a18
commit 076677da20
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 59 additions and 8 deletions

View file

@ -10,7 +10,7 @@ use fs_err as fs;
use fs_err::File; use fs_err::File;
use itertools::Itertools; use itertools::Itertools;
use owo_colors::OwoColorize; use owo_colors::OwoColorize;
use tracing::debug; use tracing::{debug, trace};
use uv_configuration::PreviewMode; use uv_configuration::PreviewMode;
use uv_fs::{CWD, Simplified, cachedir}; use uv_fs::{CWD, Simplified, cachedir};
@ -85,6 +85,18 @@ pub(crate) fn create(
format!("File exists at `{}`", location.user_display()), format!("File exists at `{}`", location.user_display()),
))); )));
} }
Ok(metadata)
if metadata.is_dir()
&& location
.read_dir()
.is_ok_and(|mut dir| dir.next().is_none()) =>
{
// If it's an empty directory, we can proceed
trace!(
"Using empty directory at `{}` for virtual environment",
location.user_display()
);
}
Ok(metadata) if metadata.is_dir() => { Ok(metadata) if metadata.is_dir() => {
let name = if uv_fs::is_virtualenv_base(location) { let name = if uv_fs::is_virtualenv_base(location) {
"virtual environment" "virtual environment"
@ -100,13 +112,6 @@ pub(crate) fn create(
remove_virtualenv(location)?; remove_virtualenv(location)?;
fs::create_dir_all(location)?; fs::create_dir_all(location)?;
} }
OnExisting::Fail
if location
.read_dir()
.is_ok_and(|mut dir| dir.next().is_none()) =>
{
debug!("Ignoring empty directory");
}
OnExisting::Fail => { OnExisting::Fail => {
match confirm_clear(location, name)? { match confirm_clear(location, name)? {
Some(true) => { Some(true) => {

View file

@ -11393,3 +11393,49 @@ fn sync_config_settings_package() -> Result<()> {
Ok(()) Ok(())
} }
/// Ensure that when we sync to an empty virtual environment directory, we don't attempt to remove
/// it, which breaks Docker volume mounts.
#[test]
#[cfg(unix)]
fn sync_does_not_remove_empty_virtual_environment_directory() -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let context = TestContext::new_with_versions(&["3.12"]);
let project_dir = context.temp_dir.child("project");
fs_err::create_dir(&project_dir)?;
let pyproject_toml = project_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig"]
"#,
)?;
let venv_dir = project_dir.child(".venv");
fs_err::create_dir(&venv_dir)?;
// Ensure the parent is read-only, to prevent deletion of the virtual environment
fs_err::set_permissions(&project_dir, std::fs::Permissions::from_mode(0o555))?;
// Note we do _not_ fail to create the virtual environment — we fail later when writing to the
// project directory
uv_snapshot!(context.filters(), context.sync().current_dir(&project_dir), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: .venv
Resolved 2 packages in [TIME]
error: failed to write to file `[TEMP_DIR]/project/uv.lock`: Permission denied (os error 13)
");
Ok(())
}