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
22 changes: 13 additions & 9 deletions crates/uv/src/commands/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1098,7 +1098,7 @@ fn discover_project_environment(
}

/// Return whether to use centralized project environments for this invocation.
fn centralized_environments_enabled(
pub(crate) fn centralized_environments_enabled(
selection: &ProjectEnvironmentSelection,
cache: &Cache,
) -> bool {
Expand All @@ -1115,7 +1115,7 @@ fn centralized_environments_enabled(
}

/// Return whether `path` is a link into the current cache's environment bucket.
fn is_centralized_environment_link(path: &Path, cache: &Cache) -> bool {
pub(crate) fn is_centralized_environment_link(path: &Path, cache: &Cache) -> bool {
let Ok(target) = fs_err::read_link(path) else {
return false;
};
Expand All @@ -1139,7 +1139,7 @@ fn is_centralized_environment_link(path: &Path, cache: &Cache) -> bool {
}

/// Return the centralized environment path for a given workspace and interpreter.
fn centralized_environment_root(
pub(crate) fn centralized_environment_root(
workspace: &Workspace,
interpreter: &Interpreter,
upgradeable: bool,
Expand Down Expand Up @@ -1203,11 +1203,11 @@ pub(crate) enum LinkErrorReporting {
}

/// Point the workspace's `.venv` to the centralized environment.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this needs an update mentioning the return value

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fn update_project_environment_link(
pub(crate) fn update_project_environment_link(
environment: &PythonEnvironment,
workspace: &Workspace,
link_error_reporting: LinkErrorReporting,
) {
) -> bool {
let link = workspace.install_path().join(".venv");
let report_error = |message: &str, err: &std::io::Error| match link_error_reporting {
LinkErrorReporting::User => {
Expand All @@ -1220,22 +1220,26 @@ fn update_project_environment_link(
if uv_fs::is_virtualenv_base(&link) {
if let Err(err) = uv_fs::remove_virtualenv(&link) {
report_error("Failed to remove existing local virtual environment", &err);
return;
return false;
}
} else {
// On Windows, copying a junction can produce an empty directory.
#[cfg(windows)]
if let Err(err) = fs_err::remove_dir(&link) {
report_error("Failed to create link to project environment", &err);
return;
return false;
}
}
}

// TODO(tk): When directory links are unavailable, write `.venv` as a file containing the
// environment path.
if let Err(err) = uv_fs::replace_symlink(environment.root(), &link) {
report_error("Failed to create link to project environment", &err);
match uv_fs::replace_symlink(environment.root(), &link) {
Ok(()) => true,
Err(err) => {
report_error("Failed to create link to project environment", &err);
false
}
}
}

Expand Down
134 changes: 80 additions & 54 deletions crates/uv/src/commands/venv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use uv_distribution_types::{
use uv_fs::Simplified;
use uv_install_wheel::LinkMode;
use uv_normalize::DefaultGroups;
use uv_preview::{Preview, PreviewFeature};
use uv_preview::Preview;
use uv_python::{
EnvironmentPreference, PythonDownloads, PythonInstallation, PythonPreference, PythonRequest,
};
Expand All @@ -32,18 +32,17 @@ use uv_shell::{Shell, shlex_posix, shlex_windows};
use uv_types::{
AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, HashStrategy, SourceTreeEditablePolicy,
};
use uv_virtualenv::OnExisting;
use uv_virtualenv::{OnExisting, RemovalReason};
use uv_warnings::warn_user;
use uv_workspace::{
DiscoveryOptions, ProjectEnvironmentSelection, VirtualProject, WorkspaceCache,
WorkspaceErrorKind,
};
use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceCache, WorkspaceErrorKind};

use crate::commands::ExitStatus;
use crate::commands::pip::loggers::{DefaultInstallLogger, InstallLogger};
use crate::commands::pip::operations::{Changelog, report_interpreter};
use crate::commands::project::{
WorkspacePython, lock_project_environment, validate_project_requires_python,
LinkErrorReporting, WorkspacePython, centralized_environment_root,
centralized_environments_enabled, is_centralized_environment_link, lock_project_environment,
update_project_environment_link, validate_project_requires_python,
};
use crate::commands::reporters::PythonDownloadReporter;
use crate::printer::Printer;
Expand Down Expand Up @@ -131,32 +130,13 @@ pub(crate) async fn venv(
let project_environment = project
.as_ref()
.map(VirtualProject::workspace)
.filter(|workspace| path.is_none() && workspace.install_path() == project_dir);

let project_environment_selection =
project_environment.map(|workspace| workspace.environment_selection(Some(false)));
.filter(|workspace| path.is_none() && workspace.install_path() == project_dir)
.map(|workspace| (workspace, workspace.environment_selection(Some(false))));

if project_environment_selection
let centralized_workspace = project_environment
.as_ref()
.is_some_and(ProjectEnvironmentSelection::is_default)
&& preview.is_enabled(PreviewFeature::CentralizedProjectEnvs)
{
warn_user!(
"The `centralized-project-envs` preview feature currently has no effect on `uv venv`"
);
}

// Determine the default path; either the virtual environment for the project or `.venv`.
let path = path.unwrap_or_else(|| {
project_environment_selection.map_or_else(
|| PathBuf::from(".venv"),
|selection| {
selection
.explicit_path()
.map_or_else(|| project_dir.join(".venv"), Path::to_path_buf)
},
)
});
.filter(|(_, selection)| centralized_environments_enabled(selection, cache))
.map(|(workspace, _)| *workspace);

let reporter = PythonDownloadReporter::single(printer);

Expand Down Expand Up @@ -199,6 +179,24 @@ pub(crate) async fn venv(
python.into_interpreter()
};

let upgradeable = python_request
.as_ref()
.is_none_or(|request| !request.includes_patch());

// Determine the default path.
let path = if let Some(workspace) = centralized_workspace {
centralized_environment_root(workspace, &interpreter, upgradeable, cache)
} else {
path.or_else(|| {
project_environment.as_ref().map(|(_, selection)| {
selection
.explicit_path()
.map_or_else(|| project_dir.join(".venv"), Path::to_path_buf)
})
})
.unwrap_or_else(|| PathBuf::from(".venv"))
};

// Check if the discovered Python version is incompatible with the current workspace
if let Some(requires_python) = requires_python {
match validate_project_requires_python(
Expand All @@ -215,19 +213,26 @@ pub(crate) async fn venv(
}
}

writeln!(
printer.stderr(),
"Creating virtual environment {}at: {}",
if seed { "with seed packages " } else { "" },
path.user_display().cyan()
)?;

let upgradeable = python_request
.as_ref()
.is_none_or(|request| !request.includes_patch());
let with_seed = if seed { " with seed packages" } else { "" };
if centralized_workspace.is_some() {
writeln!(
printer.stderr(),
"Creating virtual environment `{}`{with_seed}",
path.file_name()
.unwrap_or(path.as_os_str())
.to_string_lossy()
.cyan(),
)?;
} else {
writeln!(
printer.stderr(),
"Creating virtual environment{with_seed} at: {}",
path.user_display().cyan()
)?;
}

// Lock the project environment to avoid synchronization issues.
let _lock = if let Some(workspace) = project_environment {
let _lock = if let Some((workspace, _)) = project_environment.as_ref() {
lock_project_environment(workspace)
.await
.inspect_err(|err| {
Expand All @@ -238,6 +243,21 @@ pub(crate) async fn venv(
None
};

let on_existing = match on_existing {
OnExisting::Prompt | OnExisting::Remove(_) if centralized_workspace.is_some() => {
// Centralized environments are managed by uv, so replace them without prompting.
OnExisting::Remove(RemovalReason::ManagedEnvironment)
}
OnExisting::Prompt | OnExisting::Remove(_)
if is_centralized_environment_link(&path, cache) =>
{
// Remove `.venv` without following it into the cache.
uv_fs::remove_symlink(&path).map_err(|err| VenvError::Creation(err.into()))?;
on_existing
}
_ => on_existing,
};

// Create the virtual environment.
let venv = uv_virtualenv::create_venv(
&path,
Expand Down Expand Up @@ -355,30 +375,36 @@ pub(crate) async fn venv(
DefaultInstallLogger.on_complete(&changelog, printer, DryRun::Disabled)?;
}

// Determine the appropriate environment path.
let scripts = if let Some(workspace) = centralized_workspace
&& update_project_environment_link(&venv, workspace, LinkErrorReporting::User)
&& let Ok(suffix) = venv.scripts().strip_prefix(&path)
{
workspace.install_path().join(".venv").join(suffix)
} else {
venv.scripts().to_path_buf()
};

// Determine the appropriate activation command.
let activation = match Shell::from_env() {
None => None,
Some(Shell::Bash | Shell::Zsh | Shell::Ksh) => Some(format!(
"source {}",
shlex_posix(venv.scripts().join("activate"))
)),
Some(Shell::Bash | Shell::Zsh | Shell::Ksh) => {
Some(format!("source {}", shlex_posix(scripts.join("activate"))))
}
Some(Shell::Fish) => Some(format!(
"source {}",
shlex_posix(venv.scripts().join("activate.fish"))
shlex_posix(scripts.join("activate.fish"))
)),
Some(Shell::Nushell) => Some(format!(
"overlay use {}",
shlex_posix(venv.scripts().join("activate.nu"))
shlex_posix(scripts.join("activate.nu"))
)),
Some(Shell::Csh) => Some(format!(
"source {}",
shlex_posix(venv.scripts().join("activate.csh"))
)),
Some(Shell::Powershell) => Some(shlex_windows(
venv.scripts().join("activate"),
Shell::Powershell,
shlex_posix(scripts.join("activate.csh"))
)),
Some(Shell::Cmd) => Some(shlex_windows(venv.scripts().join("activate"), Shell::Cmd)),
Some(Shell::Powershell) => Some(shlex_windows(scripts.join("activate"), Shell::Powershell)),
Some(Shell::Cmd) => Some(shlex_windows(scripts.join("activate"), Shell::Cmd)),
};
if let Some(act) = activation {
writeln!(printer.stderr(), "Activate with: {}", act.green())?;
Expand Down
Loading
Loading