diff --git a/crates/uv-python/src/lib.rs b/crates/uv-python/src/lib.rs index 51ecbda8c..173b10135 100644 --- a/crates/uv-python/src/lib.rs +++ b/crates/uv-python/src/lib.rs @@ -1208,6 +1208,7 @@ mod tests { &[ ("CONDA_PREFIX", Some(baseenv.as_os_str())), ("CONDA_DEFAULT_ENV", Some(&OsString::from("base"))), + (EnvVars::CONDA_ROOT, None), ], || { find_python_installation( @@ -1231,6 +1232,7 @@ mod tests { &[ ("CONDA_PREFIX", Some(baseenv.as_os_str())), ("CONDA_DEFAULT_ENV", Some(&OsString::from("base"))), + (EnvVars::CONDA_ROOT, None), ], || { find_python_installation( @@ -1275,6 +1277,66 @@ mod tests { "We should find the conda environment" ); + // Test _CONDA_ROOT detection of base environment + let conda_root_env = context.tempdir.child("conda-root"); + TestContext::mock_conda_prefix(&conda_root_env, "3.12.2")?; + + // When _CONDA_ROOT matches CONDA_PREFIX, it should be treated as a base environment + let result = context.run_with_vars( + &[ + (EnvVars::CONDA_PREFIX, Some(conda_root_env.as_os_str())), + (EnvVars::CONDA_ROOT, Some(conda_root_env.as_os_str())), + ( + EnvVars::CONDA_DEFAULT_ENV, + Some(&OsString::from("custom-name")), + ), + ], + || { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::OnlyVirtual, + PythonPreference::OnlySystem, + &context.cache, + Preview::default(), + ) + }, + )?; + + assert!( + matches!(result, Err(PythonNotFound { .. })), + "Base environment detected via _CONDA_ROOT should be excluded from virtual environments; got {result:?}" + ); + + // When _CONDA_ROOT doesn't match CONDA_PREFIX, it should be treated as a regular conda environment + let other_conda_env = context.tempdir.child("other-conda"); + TestContext::mock_conda_prefix(&other_conda_env, "3.12.3")?; + + let python = context.run_with_vars( + &[ + (EnvVars::CONDA_PREFIX, Some(other_conda_env.as_os_str())), + (EnvVars::CONDA_ROOT, Some(conda_root_env.as_os_str())), + ( + EnvVars::CONDA_DEFAULT_ENV, + Some(&OsString::from("custom-env")), + ), + ], + || { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::OnlyVirtual, + PythonPreference::OnlySystem, + &context.cache, + Preview::default(), + ) + }, + )??; + + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.12.3", + "Non-base conda environment should be available for virtual environment preference" + ); + Ok(()) } diff --git a/crates/uv-python/src/virtualenv.rs b/crates/uv-python/src/virtualenv.rs index a4e7d5509..7cdde7ba9 100644 --- a/crates/uv-python/src/virtualenv.rs +++ b/crates/uv-python/src/virtualenv.rs @@ -79,19 +79,27 @@ pub(crate) enum CondaEnvironmentKind { impl CondaEnvironmentKind { /// Whether the given `CONDA_PREFIX` path is the base Conda environment. /// - /// When the base environment is used, `CONDA_DEFAULT_ENV` will be set to a name, i.e., `base` or - /// `root` which does not match the prefix, e.g. `/usr/local` instead of - /// `/usr/local/conda/envs/`. + /// The base environment is typically stored in a location matching the `_CONDA_ROOT` path. /// - /// Note the name `CONDA_DEFAULT_ENV` is misleading, it's the current environment, not the base - /// environment name. + /// Additionally, when the base environment is active, `CONDA_DEFAULT_ENV` will be set to a + /// name, e.g., `base`, which does not match the `CONDA_PREFIX`, e.g., `/usr/local` instead of + /// `/usr/local/conda/envs/`. Note the name `CONDA_DEFAULT_ENV` is misleading, it's the + /// active environment name, not a constant base environment name. fn from_prefix_path(path: &Path) -> Self { - // If we cannot read `CONDA_DEFAULT_ENV`, there's no way to know if the base environment + // If `_CONDA_ROOT` is set and matches `CONDA_PREFIX`, it's the base environment. + if let Ok(conda_root) = env::var(EnvVars::CONDA_ROOT) { + if path == Path::new(&conda_root) { + return Self::Base; + } + } + + // Next, we'll use a heuristic based on `CONDA_DEFAULT_ENV` let Ok(current_env) = env::var(EnvVars::CONDA_DEFAULT_ENV) else { return Self::Child; }; - // These are the expected names for the base environment + // These are the expected names for the base environment; we may want to remove this + // restriction in the future as it's not strictly necessary. if current_env != "base" && current_env != "root" { return Self::Child; } diff --git a/crates/uv-static/src/env_vars.rs b/crates/uv-static/src/env_vars.rs index 36648f17d..799b73566 100644 --- a/crates/uv-static/src/env_vars.rs +++ b/crates/uv-static/src/env_vars.rs @@ -497,12 +497,15 @@ impl EnvVars { /// Used to detect an activated virtual environment. pub const VIRTUAL_ENV: &'static str = "VIRTUAL_ENV"; - /// Used to detect an activated Conda environment. + /// Used to detect the path of an active Conda environment. pub const CONDA_PREFIX: &'static str = "CONDA_PREFIX"; - /// Used to determine if an active Conda environment is the base environment or not. + /// Used to determine the name of the active Conda environment. pub const CONDA_DEFAULT_ENV: &'static str = "CONDA_DEFAULT_ENV"; + /// Used to determine the root install path of Conda. + pub const CONDA_ROOT: &'static str = "_CONDA_ROOT"; + /// If set to `1` before a virtual environment is activated, then the /// virtual environment name will not be prepended to the terminal prompt. pub const VIRTUAL_ENV_DISABLE_PROMPT: &'static str = "VIRTUAL_ENV_DISABLE_PROMPT"; diff --git a/docs/reference/environment.md b/docs/reference/environment.md index 7537af535..82f9b797e 100644 --- a/docs/reference/environment.md +++ b/docs/reference/environment.md @@ -584,11 +584,15 @@ This is a quasi-standard variable, described, e.g., in `ncurses(3x)`. ### `CONDA_DEFAULT_ENV` -Used to determine if an active Conda environment is the base environment or not. +Used to determine the name of the active Conda environment. ### `CONDA_PREFIX` -Used to detect an activated Conda environment. +Used to detect the path of an active Conda environment. + +### `CONDA_ROOT` + +Used to determine the root install path of Conda. ### `FISH_VERSION`