From 8d26e11380361facdb117b784d34c5007e756d0e Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Thu, 19 Sep 2024 06:21:05 -0500 Subject: [PATCH] Avoid deleting the project environment directory if it is not a virtual environment (#7522) Closes https://github.com/astral-sh/uv/issues/7519 --- crates/uv/src/commands/project/mod.rs | 11 ++++++ crates/uv/tests/sync.rs | 52 ++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 87acc01381d9..06823f5521f3 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -120,6 +120,9 @@ pub(crate) enum ProjectError { #[error("Environment marker is empty")] EmptyEnvironment, + #[error("Project virtual environment directory `{0}` cannot be used because it has existing, non-virtual environment content")] + InvalidProjectEnvironmentDir(PathBuf), + #[error("Failed to parse `pyproject.toml`")] TomlParse(#[source] toml::de::Error), @@ -488,6 +491,14 @@ pub(crate) async fn get_or_init_environment( FoundInterpreter::Interpreter(interpreter) => { let venv = workspace.venv(); + // Before deleting the target directory, we confirm that it is either (1) a virtual + // environment or (2) an empty directory. + if PythonEnvironment::from_root(&venv, cache).is_err() + && fs_err::read_dir(&venv).is_ok_and(|mut dir| dir.next().is_some()) + { + return Err(ProjectError::InvalidProjectEnvironmentDir(venv)); + } + // Remove the existing virtual environment if it doesn't meet the requirements. match fs_err::remove_dir_all(&venv) { Ok(()) => { diff --git a/crates/uv/tests/sync.rs b/crates/uv/tests/sync.rs index e8b8aa0834b4..e24a912c12d8 100644 --- a/crates/uv/tests/sync.rs +++ b/crates/uv/tests/sync.rs @@ -1613,7 +1613,9 @@ fn convert_to_package() -> Result<()> { #[test] fn sync_custom_environment_path() -> Result<()> { - let mut context = TestContext::new("3.12"); + let mut context = TestContext::new_with_versions(&["3.11", "3.12"]) + .with_filtered_virtualenv_bin() + .with_filtered_python_names(); let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str( @@ -1633,6 +1635,8 @@ fn sync_custom_environment_path() -> Result<()> { ----- stdout ----- ----- stderr ----- + Using Python 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: .venv Resolved 2 packages in [TIME] Prepared 1 package in [TIME] Installed 1 package in [TIME] @@ -1733,6 +1737,52 @@ fn sync_custom_environment_path() -> Result<()> { .child(".venv") .assert(predicate::path::is_dir()); + // If the directory already exists and is not a virtual environment we should fail with an error + fs_err::remove_dir_all(context.temp_dir.join("foo"))?; + fs_err::create_dir(context.temp_dir.join("foo"))?; + fs_err::write(context.temp_dir.join("foo").join("file"), b"")?; + uv_snapshot!(context.filters(), context.sync().env("UV_PROJECT_ENVIRONMENT", "foo"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + warning: Ignoring existing virtual environment linked to non-existent Python interpreter: foo/[BIN]/python + Using Python 3.12.[X] interpreter at: [PYTHON-3.12] + error: Project virtual environment directory `[TEMP_DIR]/foo` cannot be used because it has existing, non-virtual environment content + "###); + + // But if it's just an incompatible virtual environment... + fs_err::remove_dir_all(context.temp_dir.join("foo"))?; + uv_snapshot!(context.filters(), context.venv().arg("foo").arg("--python").arg("3.11"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using Python 3.11.[X] interpreter at: [PYTHON-3.11] + Creating virtual environment at: foo + Activate with: source foo/[BIN]/activate + "###); + + // Even with some extraneous content... + fs_err::write(context.temp_dir.join("foo").join("file"), b"")?; + + // We can delete and use it + uv_snapshot!(context.filters(), context.sync().env("UV_PROJECT_ENVIRONMENT", "foo"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using Python 3.12.[X] interpreter at: [PYTHON-3.12] + Removed virtual environment at: foo + Creating virtual environment at: foo + Resolved 2 packages in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 + "###); + Ok(()) }