diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index d3766df97a44b..ffb6c1ef08e86 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -1148,6 +1148,8 @@ pub(crate) fn centralized_environment_root( upgradeable: bool, cache: &Cache, ) -> PathBuf { + let workspace_path = fs_err::canonicalize(workspace.install_path()) + .unwrap_or_else(|_| workspace.install_path().clone()); let interpreter_key = interpreter.key(); // Use the workspace path to isolate projects and the interpreter key to maximize intra-project // environment re-use while avoiding clashes with incompatible environments. Ignoring the patch @@ -1158,12 +1160,12 @@ pub(crate) fn centralized_environment_root( .is_some_and(|link| link.exists()) { ( - cache_digest(&(workspace.install_path(), installation.minor_version_key())), + cache_digest(&(&workspace_path, installation.minor_version_key())), interpreter.python_minor_version(), ) } else { ( - cache_digest(&(workspace.install_path(), &interpreter_key)), + cache_digest(&(&workspace_path, &interpreter_key)), interpreter.python_version().clone(), ) }; @@ -1173,8 +1175,7 @@ pub(crate) fn centralized_environment_root( .as_ref() .and_then(|project| cache_name(project.name.as_ref(), Some(100))) .or_else(|| { - workspace - .install_path() + workspace_path .file_name() .and_then(|name| name.to_str()) .and_then(|name| cache_name(name, Some(100))) diff --git a/crates/uv/tests/sync/centralized_project_envs.rs b/crates/uv/tests/sync/centralized_project_envs.rs index c2040f56e8053..94045f2c53cd6 100644 --- a/crates/uv/tests/sync/centralized_project_envs.rs +++ b/crates/uv/tests/sync/centralized_project_envs.rs @@ -270,6 +270,50 @@ fn sync_centralized_env_avoids_project_name_collisions() -> Result<()> { Ok(()) } +#[test] +#[cfg(unix)] +fn sync_centralized_env_reuses_symlinked_workspace() -> Result<()> { + let context = uv_test::test_context_with_versions!(&["3.12"]); + let project = context.temp_dir.child("project"); + project.create_dir_all()?; + project.child("pyproject.toml").write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + "#, + )?; + + let symlink = context.temp_dir.child("project-link"); + uv_fs::create_symlink(project.path(), symlink.path())?; + + context + .sync() + .arg("--preview-features") + .arg("centralized-project-envs") + .arg("--project") + .arg(project.path()) + .assert() + .success(); + let first = fs_err::read_link(project.child(".venv").path())?; + + fs_err::remove_file(project.child(".venv").path())?; + context + .sync() + .arg("--preview-features") + .arg("centralized-project-envs") + .arg("--project") + .arg(symlink.path()) + .assert() + .success(); + let second = fs_err::read_link(project.child(".venv").path())?; + + assert_eq!(first, second); + Ok(()) +} + #[test] fn sync_centralized_env_respects_explicit_environments() -> Result<()> { let context = uv_test::test_context_with_versions!(&["3.12"]);