Skip to content

Commit 076677d

Browse files
authored
Avoid removing empty directories when constructing virtual environments (#14822)
Closes #14815 I tested this with the docker-compose reproduction. You can also see a regression test change at 2ae4464
1 parent f0151f3 commit 076677d

File tree

2 files changed

+59
-8
lines changed

2 files changed

+59
-8
lines changed

crates/uv-virtualenv/src/virtualenv.rs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use fs_err as fs;
1010
use fs_err::File;
1111
use itertools::Itertools;
1212
use owo_colors::OwoColorize;
13-
use tracing::debug;
13+
use tracing::{debug, trace};
1414

1515
use uv_configuration::PreviewMode;
1616
use uv_fs::{CWD, Simplified, cachedir};
@@ -85,6 +85,18 @@ pub(crate) fn create(
8585
format!("File exists at `{}`", location.user_display()),
8686
)));
8787
}
88+
Ok(metadata)
89+
if metadata.is_dir()
90+
&& location
91+
.read_dir()
92+
.is_ok_and(|mut dir| dir.next().is_none()) =>
93+
{
94+
// If it's an empty directory, we can proceed
95+
trace!(
96+
"Using empty directory at `{}` for virtual environment",
97+
location.user_display()
98+
);
99+
}
88100
Ok(metadata) if metadata.is_dir() => {
89101
let name = if uv_fs::is_virtualenv_base(location) {
90102
"virtual environment"
@@ -100,13 +112,6 @@ pub(crate) fn create(
100112
remove_virtualenv(location)?;
101113
fs::create_dir_all(location)?;
102114
}
103-
OnExisting::Fail
104-
if location
105-
.read_dir()
106-
.is_ok_and(|mut dir| dir.next().is_none()) =>
107-
{
108-
debug!("Ignoring empty directory");
109-
}
110115
OnExisting::Fail => {
111116
match confirm_clear(location, name)? {
112117
Some(true) => {

crates/uv/tests/it/sync.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11393,3 +11393,49 @@ fn sync_config_settings_package() -> Result<()> {
1139311393

1139411394
Ok(())
1139511395
}
11396+
11397+
/// Ensure that when we sync to an empty virtual environment directory, we don't attempt to remove
11398+
/// it, which breaks Docker volume mounts.
11399+
#[test]
11400+
#[cfg(unix)]
11401+
fn sync_does_not_remove_empty_virtual_environment_directory() -> Result<()> {
11402+
use std::os::unix::fs::PermissionsExt;
11403+
11404+
let context = TestContext::new_with_versions(&["3.12"]);
11405+
11406+
let project_dir = context.temp_dir.child("project");
11407+
fs_err::create_dir(&project_dir)?;
11408+
11409+
let pyproject_toml = project_dir.child("pyproject.toml");
11410+
pyproject_toml.write_str(
11411+
r#"
11412+
[project]
11413+
name = "project"
11414+
version = "0.1.0"
11415+
requires-python = ">=3.12"
11416+
dependencies = ["iniconfig"]
11417+
"#,
11418+
)?;
11419+
11420+
let venv_dir = project_dir.child(".venv");
11421+
fs_err::create_dir(&venv_dir)?;
11422+
11423+
// Ensure the parent is read-only, to prevent deletion of the virtual environment
11424+
fs_err::set_permissions(&project_dir, std::fs::Permissions::from_mode(0o555))?;
11425+
11426+
// Note we do _not_ fail to create the virtual environment — we fail later when writing to the
11427+
// project directory
11428+
uv_snapshot!(context.filters(), context.sync().current_dir(&project_dir), @r"
11429+
success: false
11430+
exit_code: 2
11431+
----- stdout -----
11432+
11433+
----- stderr -----
11434+
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
11435+
Creating virtual environment at: .venv
11436+
Resolved 2 packages in [TIME]
11437+
error: failed to write to file `[TEMP_DIR]/project/uv.lock`: Permission denied (os error 13)
11438+
");
11439+
11440+
Ok(())
11441+
}

0 commit comments

Comments
 (0)