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
20 changes: 16 additions & 4 deletions crates/uv-virtualenv/src/virtualenv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,15 +109,27 @@ pub(crate) fn create(
}
OnExisting::Remove => {
debug!("Removing existing {name} due to `--clear`");
remove_virtualenv(location)?;
fs::create_dir_all(location)?;
// Before removing the virtual environment, we need to canonicalize the path
// because `Path::metadata` will follow the symlink but we're still operating on
// the unresolved path and will remove the symlink itself.
let location = location
.canonicalize()
.unwrap_or_else(|_| location.to_path_buf());
remove_virtualenv(&location)?;
fs::create_dir_all(&location)?;
}
OnExisting::Fail => {
match confirm_clear(location, name)? {
Some(true) => {
debug!("Removing existing {name} due to confirmation");
remove_virtualenv(location)?;
fs::create_dir_all(location)?;
// Before removing the virtual environment, we need to canonicalize the
// path because `Path::metadata` will follow the symlink but we're still
// operating on the unresolved path and will remove the symlink itself.
let location = location
.canonicalize()
.unwrap_or_else(|_| location.to_path_buf());
remove_virtualenv(&location)?;
fs::create_dir_all(&location)?;
}
Some(false) => {
let hint = format!(
Expand Down
184 changes: 184 additions & 0 deletions crates/uv/tests/it/venv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ use predicates::prelude::*;
use uv_python::{PYTHON_VERSION_FILENAME, PYTHON_VERSIONS_FILENAME};
use uv_static::EnvVars;

#[cfg(unix)]
use fs_err::os::unix::fs::symlink;

use crate::common::{TestContext, uv_snapshot};

#[test]
Expand Down Expand Up @@ -1388,3 +1391,184 @@ fn venv_python_preference() {
Activate with: source .venv/[BIN]/activate
");
}

#[test]
#[cfg(unix)]
fn create_venv_symlink_clear_preservation() -> Result<()> {
let context = TestContext::new_with_versions(&["3.12"]);

// Create a target directory
let target_dir = context.temp_dir.child("target");
target_dir.create_dir_all()?;

// Create a symlink pointing to the target directory
let symlink_path = context.temp_dir.child(".venv");
symlink(&target_dir, &symlink_path)?;

// Verify symlink exists
assert!(symlink_path.path().is_symlink());

// Create virtual environment at symlink location
uv_snapshot!(context.filters(), context.venv()
.arg(symlink_path.as_os_str())
.arg("--python")
.arg("3.12"), @r###"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: .venv
Activate with: source .venv/[BIN]/activate
"###
);

// Verify symlink is still preserved after creation
assert!(symlink_path.path().is_symlink());

// Run uv venv with --clear to test symlink preservation during clear
uv_snapshot!(context.filters(), context.venv()
.arg(symlink_path.as_os_str())
.arg("--clear")
.arg("--python")
.arg("3.12"), @r###"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: .venv
Activate with: source .venv/[BIN]/activate
"###
);

// Verify symlink is STILL preserved after --clear
assert!(symlink_path.path().is_symlink());

Ok(())
}

#[test]
#[cfg(unix)]
fn create_venv_symlink_recreate_preservation() -> Result<()> {
let context = TestContext::new_with_versions(&["3.12"]);

// Create a target directory
let target_dir = context.temp_dir.child("target");
target_dir.create_dir_all()?;

// Create a symlink pointing to the target directory
let symlink_path = context.temp_dir.child(".venv");
symlink(&target_dir, &symlink_path)?;

// Verify symlink exists
assert!(symlink_path.path().is_symlink());

// Create virtual environment at symlink location
uv_snapshot!(context.filters(), context.venv()
.arg(symlink_path.as_os_str())
.arg("--python")
.arg("3.12"), @r###"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: .venv
Activate with: source .venv/[BIN]/activate
"###
);

// Verify symlink is preserved after first creation
assert!(symlink_path.path().is_symlink());

// Run uv venv again WITHOUT --clear to test recreation behavior
uv_snapshot!(context.filters(), context.venv()
.arg(symlink_path.as_os_str())
.arg("--python")
.arg("3.12"), @r###"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: .venv
warning: A virtual environment already exists at `.venv`. In the future, uv will require `--clear` to replace it
Activate with: source .venv/[BIN]/activate
"###
);

// Verify symlink is STILL preserved after recreation
assert!(symlink_path.path().is_symlink());

Ok(())
}

#[test]
#[cfg(unix)]
fn create_venv_nested_symlink_preservation() -> Result<()> {
let context = TestContext::new_with_versions(&["3.12"]);

// Create a target directory
let target_dir = context.temp_dir.child("target");
target_dir.create_dir_all()?;

// Create first symlink level: intermediate -> target
let intermediate_link = context.temp_dir.child("intermediate");
symlink(&target_dir, &intermediate_link)?;

// Create second symlink level: .venv -> intermediate (nested symlink)
let symlink_path = context.temp_dir.child(".venv");
symlink(&intermediate_link, &symlink_path)?;

// Verify nested symlink exists
assert!(symlink_path.path().is_symlink());
assert!(intermediate_link.path().is_symlink());

// Create virtual environment at nested symlink location
uv_snapshot!(context.filters(), context.venv()
.arg(symlink_path.as_os_str())
.arg("--python")
.arg("3.12"), @r###"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: .venv
Activate with: source .venv/[BIN]/activate
"###
);

// Verify both symlinks are preserved
assert!(symlink_path.path().is_symlink());
assert!(intermediate_link.path().is_symlink());

// Run uv venv again to test nested symlink preservation during recreation
uv_snapshot!(context.filters(), context.venv()
.arg(symlink_path.as_os_str())
.arg("--python")
.arg("3.12"), @r###"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: .venv
warning: A virtual environment already exists at `.venv`. In the future, uv will require `--clear` to replace it
Activate with: source .venv/[BIN]/activate
"###
);

// Verify nested symlinks are STILL preserved
assert!(symlink_path.path().is_symlink());
assert!(intermediate_link.path().is_symlink());

Ok(())
}
Loading