diff --git a/crates/uv-configuration/src/preview.rs b/crates/uv-configuration/src/preview.rs index c8d67be5b..6079f3169 100644 --- a/crates/uv-configuration/src/preview.rs +++ b/crates/uv-configuration/src/preview.rs @@ -14,6 +14,7 @@ bitflags::bitflags! { const JSON_OUTPUT = 1 << 2; const PYLOCK = 1 << 3; const ADD_BOUNDS = 1 << 4; + const TEMPLATE_PROJECT_ENVIRONMENT = 1 << 5; } } diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index 09f2b692a..73c5f8821 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -8,7 +8,8 @@ use glob::{GlobError, PatternError, glob}; use rustc_hash::{FxHashMap, FxHashSet}; use tracing::{debug, trace, warn}; -use uv_configuration::DependencyGroupsWithDefaults; +use uv_cache_key::cache_digest; +use uv_configuration::{DependencyGroupsWithDefaults, Preview, PreviewFeatures}; use uv_distribution_types::{Index, Requirement, RequirementSource}; use uv_fs::{CWD, Simplified}; use uv_normalize::{DEV_DEPENDENCIES, GroupName, PackageName}; @@ -631,16 +632,46 @@ impl Workspace { /// If `active` is `true`, the `VIRTUAL_ENV` variable will be preferred. If it is `false`, any /// warnings about mismatch between the active environment and the project environment will be /// silenced. - pub fn venv(&self, active: Option) -> PathBuf { + pub fn venv(&self, active: Option, preview: Preview) -> PathBuf { /// Resolve the `UV_PROJECT_ENVIRONMENT` value, if any. - fn from_project_environment_variable(workspace: &Workspace) -> Option { + fn from_project_environment_variable( + workspace: &Workspace, + preview: Preview, + ) -> Option { let value = std::env::var_os(EnvVars::UV_PROJECT_ENVIRONMENT)?; if value.is_empty() { return None; } - let path = PathBuf::from(value); + let templated = value + .to_string_lossy() + .replace( + "{project_path_hash}", + &cache_digest(&workspace.install_path), + ) + .replace( + "{project_name}", + &workspace + .pyproject_toml + .project + .as_ref() + .map(|project| project.name.to_string()) + .unwrap_or("none".to_string()), + ); + + let path = if templated != value.to_string_lossy() { + if !preview.is_enabled(PreviewFeatures::TEMPLATE_PROJECT_ENVIRONMENT) { + warn_user_once!( + "Templating the `UV_PROJECT_ENVIRONMENT` setting is in preview and may change without warning; use `--preview-features {}` to disable this warning", + PreviewFeatures::TEMPLATE_PROJECT_ENVIRONMENT + ); + } + PathBuf::from(templated) + } else { + PathBuf::from(value) + }; + if path.is_absolute() { return Some(path); } @@ -668,7 +699,7 @@ impl Workspace { } // Determine the default value - let project_env = from_project_environment_variable(self) + let project_env = from_project_environment_variable(self, preview) .unwrap_or_else(|| self.install_path.join(".venv")); // Warn if it conflicts with `VIRTUAL_ENV` diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index a5953cd76..fbc523283 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -904,7 +904,7 @@ impl ProjectInterpreter { .await?; // Read from the virtual environment first. - let root = workspace.venv(active); + let root = workspace.venv(active, preview); match PythonEnvironment::from_root(&root, cache) { Ok(venv) => { match environment_is_usable( @@ -1307,7 +1307,7 @@ impl ProjectEnvironment { // Otherwise, create a virtual environment with the discovered interpreter. ProjectInterpreter::Interpreter(interpreter) => { - let root = workspace.venv(active); + let root = workspace.venv(active, preview); // Avoid removing things that are not virtual environments let replace = match (root.try_exists(), root.join("pyvenv.cfg").try_exists()) { diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index 1f2ce3dfb..9daec9939 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -119,7 +119,7 @@ pub(crate) async fn venv( // This isn't strictly necessary and we may want to change it later, but this // avoids a breaking change when adding project environment support to `uv venv`. (project.workspace().install_path() == project_dir) - .then(|| project.workspace().venv(Some(false))) + .then(|| project.workspace().venv(Some(false), preview)) }) .unwrap_or(PathBuf::from(".venv")), );