diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index 268fb60e1a15..fa336ccf7be3 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -10,9 +10,9 @@ use tracing::{debug, trace, warn}; use pep508_rs::{MarkerTree, RequirementOrigin, VerbatimUrl}; use pypi_types::{Requirement, RequirementSource, SupportedEnvironments, VerbatimParsedUrl}; -use uv_fs::Simplified; +use uv_fs::{Simplified, CWD}; use uv_normalize::{GroupName, PackageName, DEV_DEPENDENCIES}; -use uv_warnings::warn_user; +use uv_warnings::{warn_user, warn_user_once}; use crate::pyproject::{Project, PyProjectToml, Source, ToolUvWorkspace}; @@ -442,7 +442,7 @@ impl Workspace { /// it is resolved relative to the install path. pub fn venv(&self) -> PathBuf { /// Resolve the `UV_PROJECT_ENVIRONMENT` value, if any. - fn from_environment_variable(workspace: &Workspace) -> Option { + fn from_project_environment_variable(workspace: &Workspace) -> Option { let value = std::env::var_os("UV_PROJECT_ENVIRONMENT")?; if value.is_empty() { @@ -458,8 +458,46 @@ impl Workspace { Some(workspace.install_path.join(path)) } - // TODO(zanieb): Warn if `VIRTUAL_ENV` is set and does not match - from_environment_variable(self).unwrap_or_else(|| self.install_path.join(".venv")) + // Resolve the `VIRTUAL_ENV` variable, if any. + fn from_virtual_env_variable() -> Option { + let value = std::env::var_os("VIRTUAL_ENV")?; + + if value.is_empty() { + return None; + }; + + let path = PathBuf::from(value); + if path.is_absolute() { + return Some(path); + }; + + // Resolve the path relative to current directory. + // Note this differs from `UV_PROJECT_ENVIRONMENT` + Some(CWD.join(path)) + } + + // Determine the default value + let project_env = from_project_environment_variable(self) + .unwrap_or_else(|| self.install_path.join(".venv")); + + // Warn if it conflicts with `VIRTUAL_ENV` + if let Some(from_virtual_env) = from_virtual_env_variable() { + if std::path::absolute(&project_env) + .as_ref() + .unwrap_or(&project_env) + != std::path::absolute(&from_virtual_env) + .as_ref() + .unwrap_or(&from_virtual_env) + { + warn_user_once!( + "`VIRTUAL_ENV={}` does not match the project environment path `{}` and will be ignored", + from_virtual_env.user_display(), + project_env.user_display() + ); + } + } + + project_env } /// The members of the workspace. diff --git a/crates/uv/tests/init.rs b/crates/uv/tests/init.rs index 8f551d5335de..b9f9397fafdc 100644 --- a/crates/uv/tests/init.rs +++ b/crates/uv/tests/init.rs @@ -116,6 +116,7 @@ fn init_application() -> Result<()> { Hello from foo! ----- stderr ----- + warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored Using Python 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtualenv at: .venv Resolved 1 package in [TIME] @@ -296,6 +297,7 @@ fn init_application_package() -> Result<()> { Hello from foo! ----- stderr ----- + warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored Using Python 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtualenv at: .venv Resolved 1 package in [TIME] @@ -367,6 +369,7 @@ fn init_library() -> Result<()> { Hello from foo! ----- stderr ----- + warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored Using Python 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtualenv at: .venv Resolved 1 package in [TIME] diff --git a/crates/uv/tests/run.rs b/crates/uv/tests/run.rs index 67fae95327dc..e393260598c9 100644 --- a/crates/uv/tests/run.rs +++ b/crates/uv/tests/run.rs @@ -1307,6 +1307,7 @@ fn run_from_directory() -> Result<()> { 3.12.[X] ----- stderr ----- + warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored Using Python 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtualenv at: .venv Resolved 1 package in [TIME] diff --git a/crates/uv/tests/sync.rs b/crates/uv/tests/sync.rs index 45916bced17d..bcb361f8eca6 100644 --- a/crates/uv/tests/sync.rs +++ b/crates/uv/tests/sync.rs @@ -1747,3 +1747,128 @@ fn sync_workspace_custom_environment_path() -> Result<()> { Ok(()) } + +// Test for warnings when `VIRTUAL_ENV` is set but will not be respected. +#[test] +fn sync_virtual_env_warning() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + "#, + )?; + + // We should not warn if it matches the project environment + uv_snapshot!(context.filters(), context.sync().env("VIRTUAL_ENV", context.temp_dir.join(".venv")), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 + "###); + + // Including if it's a relative path that matches + uv_snapshot!(context.filters(), context.sync().env("VIRTUAL_ENV", ".venv"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Audited 1 package in [TIME] + "###); + + // But we should warn if it's a different path + uv_snapshot!(context.filters(), context.sync().env("VIRTUAL_ENV", "foo"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `VIRTUAL_ENV=foo` does not match the project environment path `.venv` and will be ignored + Resolved 2 packages in [TIME] + Audited 1 package in [TIME] + "###); + + // Including absolute paths + uv_snapshot!(context.filters(), context.sync().env("VIRTUAL_ENV", context.temp_dir.join("foo")), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `VIRTUAL_ENV=foo` does not match the project environment path `.venv` and will be ignored + Resolved 2 packages in [TIME] + Audited 1 package in [TIME] + "###); + + // We should not warn if the project environment has been customized and matches + uv_snapshot!(context.filters(), context.sync().env("VIRTUAL_ENV", "foo").env("UV_PROJECT_ENVIRONMENT", "foo"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using Python 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtualenv at: foo + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 + "###); + + // But we should warn if they don't match still + uv_snapshot!(context.filters(), context.sync().env("VIRTUAL_ENV", "foo").env("UV_PROJECT_ENVIRONMENT", "bar"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `VIRTUAL_ENV=foo` does not match the project environment path `bar` and will be ignored + Using Python 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtualenv at: bar + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 + "###); + + let child = context.temp_dir.child("child"); + child.create_dir_all()?; + + // And `VIRTUAL_ENV` is resolved relative to the project root so with relative paths we should + // warn from a child too + uv_snapshot!(context.filters(), context.sync().env("VIRTUAL_ENV", "foo").env("UV_PROJECT_ENVIRONMENT", "foo").current_dir(&child), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `VIRTUAL_ENV=foo` does not match the project environment path `[TEMP_DIR]/foo` and will be ignored + Resolved 2 packages in [TIME] + Audited 1 package in [TIME] + "###); + + // But, a matching absolute path shouldn't warn + uv_snapshot!(context.filters(), context.sync().env("VIRTUAL_ENV", context.temp_dir.join("foo")).env("UV_PROJECT_ENVIRONMENT", "foo").current_dir(&child), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Audited 1 package in [TIME] + "###); + + Ok(()) +} diff --git a/crates/uv/tests/workspace.rs b/crates/uv/tests/workspace.rs index 97e7c8cadbba..c9f4bca62600 100644 --- a/crates/uv/tests/workspace.rs +++ b/crates/uv/tests/workspace.rs @@ -377,6 +377,7 @@ fn test_uv_run_with_package_virtual_workspace() -> Result<()> { Success ----- stderr ----- + warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored Using Python 3.12.[X] interpreter at: [PYTHON] Creating virtualenv at: .venv Resolved 8 packages in [TIME] @@ -402,6 +403,7 @@ fn test_uv_run_with_package_virtual_workspace() -> Result<()> { Success ----- stderr ----- + warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored Resolved 8 packages in [TIME] Prepared 2 packages in [TIME] Installed 2 packages in [TIME] @@ -435,6 +437,7 @@ fn test_uv_run_virtual_workspace_root() -> Result<()> { Success ----- stderr ----- + warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored Using Python 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtualenv at: .venv Resolved 8 packages in [TIME] @@ -479,6 +482,7 @@ fn test_uv_run_with_package_root_workspace() -> Result<()> { Success ----- stderr ----- + warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored Using Python 3.12.[X] interpreter at: [PYTHON] Creating virtualenv at: .venv Resolved 8 packages in [TIME] @@ -504,6 +508,7 @@ fn test_uv_run_with_package_root_workspace() -> Result<()> { Success ----- stderr ----- + warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored Resolved 8 packages in [TIME] Prepared 2 packages in [TIME] Installed 2 packages in [TIME] @@ -542,6 +547,7 @@ fn test_uv_run_isolate() -> Result<()> { Success ----- stderr ----- + warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored Using Python 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtualenv at: .venv Resolved 8 packages in [TIME] @@ -572,6 +578,7 @@ fn test_uv_run_isolate() -> Result<()> { Success ----- stderr ----- + warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored Resolved 8 packages in [TIME] Audited 5 packages in [TIME] "###