mirror of
https://github.com/astral-sh/uv
synced 2026-06-21 13:47:25 +00:00
Add centralised environments in preview
This commit is contained in:
@@ -614,8 +614,8 @@ impl Cache {
|
||||
}
|
||||
}
|
||||
|
||||
// Second, remove any cached environments. These are never referenced by symlinks, so we can
|
||||
// remove them directly.
|
||||
// Second, remove all cached environments. Centralized project environments can be
|
||||
// referenced by `.venv` links, but are recreated on demand when their project is next used.
|
||||
match fs_err::read_dir(self.bucket(CacheBucket::Environments)) {
|
||||
Ok(entries) => {
|
||||
for entry in entries {
|
||||
|
||||
@@ -258,6 +258,7 @@ pub enum PreviewFeature {
|
||||
VenvSafeClear = 1 << 32,
|
||||
Check = 1 << 33,
|
||||
PackagedInit = 1 << 34,
|
||||
CentralizedEnvs = 1 << 35,
|
||||
}
|
||||
|
||||
impl PreviewFeature {
|
||||
@@ -299,6 +300,7 @@ impl PreviewFeature {
|
||||
Self::VenvSafeClear => "venv-safe-clear",
|
||||
Self::Check => "check-command",
|
||||
Self::PackagedInit => "packaged-init",
|
||||
Self::CentralizedEnvs => "centralized-envs",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -353,6 +355,7 @@ impl FromStr for PreviewFeature {
|
||||
"venv-safe-clear" => Self::VenvSafeClear,
|
||||
"check" | "check-command" => Self::Check,
|
||||
"packaged-init" => Self::PackagedInit,
|
||||
"centralized-envs" => Self::CentralizedEnvs,
|
||||
_ => return Err(PreviewFeatureParseError),
|
||||
})
|
||||
}
|
||||
@@ -644,6 +647,7 @@ mod tests {
|
||||
assert_eq!(PreviewFeature::VenvSafeClear.as_str(), "venv-safe-clear");
|
||||
assert_eq!(PreviewFeature::Audit.as_str(), "audit-command");
|
||||
assert_eq!(PreviewFeature::Check.as_str(), "check-command");
|
||||
assert_eq!(PreviewFeature::CentralizedEnvs.as_str(), "centralized-envs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -11,6 +11,7 @@ use uv_fs::Simplified;
|
||||
use uv_warnings::warn_user;
|
||||
|
||||
use uv_cache::Cache;
|
||||
use uv_cache_key::{CacheKey, CacheKeyHasher};
|
||||
use uv_client::{BaseClient, BaseClientBuilder};
|
||||
use uv_pep440::{Prerelease, Version};
|
||||
use uv_platform::{Arch, Libc, Os, Platform};
|
||||
@@ -715,6 +716,12 @@ impl fmt::Display for PythonInstallationKey {
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheKey for PythonInstallationKey {
|
||||
fn cache_key(&self, state: &mut CacheKeyHasher) {
|
||||
self.to_string().cache_key(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for PythonInstallationKey {
|
||||
type Err = PythonInstallationKeyError;
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ impl Interpreter {
|
||||
}
|
||||
|
||||
/// Returns the [`PythonInstallationKey`] for this interpreter.
|
||||
pub(crate) fn key(&self) -> PythonInstallationKey {
|
||||
pub fn key(&self) -> PythonInstallationKey {
|
||||
PythonInstallationKey::new(
|
||||
LenientImplementationName::from(self.implementation_name()),
|
||||
self.python_major(),
|
||||
|
||||
@@ -1055,9 +1055,13 @@ impl TestContext {
|
||||
));
|
||||
// Filter script environment hashes
|
||||
filters.push((
|
||||
r"environments-v(\d+)[\\/](\w+)-[a-z0-9]+".to_string(),
|
||||
r"environments-v(\d+)[\\/]([\w.\[\]-]+)-[a-f0-9]{16}".to_string(),
|
||||
"environments-v$1/$2-[HASH]".to_string(),
|
||||
));
|
||||
filters.push((
|
||||
r"`([\w.\[\]-]+)-[a-f0-9]{16}`".to_string(),
|
||||
"`$1-[HASH]`".to_string(),
|
||||
));
|
||||
// Filter archive hashes
|
||||
filters.push((
|
||||
r"archive-v(\d+)[\\/][A-Za-z0-9\-\_]+".to_string(),
|
||||
|
||||
@@ -861,7 +861,7 @@ 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_path(&self, active: Option<bool>) -> ProjectEnvironmentTarget {
|
||||
pub fn venv(&self, active: Option<bool>) -> ProjectEnvironmentTarget {
|
||||
/// Resolve the `UV_PROJECT_ENVIRONMENT` value, if any.
|
||||
fn from_project_environment_variable(workspace: &Workspace) -> Option<PathBuf> {
|
||||
let value = std::env::var_os(EnvVars::UV_PROJECT_ENVIRONMENT)?;
|
||||
@@ -935,13 +935,6 @@ impl Workspace {
|
||||
project_env
|
||||
}
|
||||
|
||||
/// The path to the workspace virtual environment.
|
||||
pub fn venv(&self, active: Option<bool>) -> PathBuf {
|
||||
self.venv_path(active)
|
||||
.project_path(&self.install_path)
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
/// The members of the workspace.
|
||||
pub fn packages(&self) -> &BTreeMap<PackageName, WorkspaceMember> {
|
||||
&self.packages
|
||||
|
||||
@@ -319,6 +319,7 @@ pub(crate) async fn add(
|
||||
cache,
|
||||
DryRun::Disabled,
|
||||
printer,
|
||||
true,
|
||||
)
|
||||
.await?
|
||||
.into_environment()?;
|
||||
|
||||
@@ -225,6 +225,7 @@ pub(crate) async fn check(
|
||||
cache,
|
||||
DryRun::Disabled,
|
||||
printer,
|
||||
true,
|
||||
)
|
||||
.await?
|
||||
.into_environment()?
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fmt::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -22,13 +23,13 @@ use uv_distribution_types::{
|
||||
ExtraBuildRequirement, ExtraBuildRequires, Index, IndexCredentialsError, Requirement,
|
||||
RequiresPython, Resolution, UnresolvedRequirement, UnresolvedRequirementSpecification,
|
||||
};
|
||||
use uv_fs::{CWD, LockedFile, LockedFileError, LockedFileMode, Simplified};
|
||||
use uv_fs::{CWD, LockedFile, LockedFileError, LockedFileMode, Simplified, verbatim_path};
|
||||
use uv_git::ResolvedRepositoryReference;
|
||||
use uv_installer::{InstallationStrategy, SatisfiesResult, SitePackages};
|
||||
use uv_normalize::{DEV_DEPENDENCIES, DefaultGroups, ExtraName, GroupName, PackageName};
|
||||
use uv_pep440::{TildeVersionSpecifier, Version, VersionSpecifiers};
|
||||
use uv_pep508::MarkerTreeContents;
|
||||
use uv_preview::Preview;
|
||||
use uv_preview::{Preview, PreviewFeature};
|
||||
use uv_pypi_types::{ConflictItem, ConflictKind, ConflictSet, Conflicts};
|
||||
use uv_python::{
|
||||
BrokenLink, EnvironmentPreference, Interpreter, InvalidEnvironmentKind, PythonDownloads,
|
||||
@@ -51,7 +52,7 @@ use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy, SourceTreeE
|
||||
use uv_warnings::{warn_user, warn_user_once};
|
||||
use uv_workspace::dependency_groups::DependencyGroupError;
|
||||
use uv_workspace::pyproject::{ExtraBuildDependency, PyProjectToml};
|
||||
use uv_workspace::{RequiresPythonSources, Workspace, WorkspaceCache};
|
||||
use uv_workspace::{ProjectEnvironmentTarget, RequiresPythonSources, Workspace, WorkspaceCache};
|
||||
|
||||
use crate::commands::pip::loggers::{InstallLogger, ResolveLogger};
|
||||
use crate::commands::pip::operations::{Changelog, Modifications};
|
||||
@@ -260,6 +261,9 @@ pub(crate) enum ProjectError {
|
||||
#[error("Project virtual environment directory `{0}` cannot be used because {1}")]
|
||||
InvalidProjectEnvironmentDir(PathBuf, String),
|
||||
|
||||
#[error("`--no-cache` is unsupported with the `centralized-envs` feature enabled")]
|
||||
CentralizedEnvironmentNoCache,
|
||||
|
||||
#[error("Failed to parse `uv.lock`")]
|
||||
UvLockParse(#[source] toml::de::Error),
|
||||
|
||||
@@ -1003,6 +1007,7 @@ fn usable_project_environment(
|
||||
python_preference: PythonPreference,
|
||||
requires_python: Option<&RequiresPython>,
|
||||
keep_incompatible: bool,
|
||||
centralized: bool,
|
||||
cache: &Cache,
|
||||
) -> Option<PythonEnvironment> {
|
||||
match environment_is_usable(
|
||||
@@ -1015,10 +1020,21 @@ fn usable_project_environment(
|
||||
) {
|
||||
Ok(()) => Some(environment),
|
||||
Err(err) if keep_incompatible => {
|
||||
warn_user!(
|
||||
"Using incompatible environment (`{}`) due to `--no-sync` ({err})",
|
||||
environment.root().user_display().cyan(),
|
||||
);
|
||||
if centralized {
|
||||
let root = environment.root();
|
||||
warn_user!(
|
||||
"Using incompatible environment (`{}`) due to `--no-sync` ({err})",
|
||||
root.file_name()
|
||||
.unwrap_or(root.as_os_str())
|
||||
.to_string_lossy()
|
||||
.cyan(),
|
||||
);
|
||||
} else {
|
||||
warn_user!(
|
||||
"Using incompatible environment (`{}`) due to `--no-sync` ({err})",
|
||||
environment.root().user_display().cyan(),
|
||||
);
|
||||
}
|
||||
Some(environment)
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -1034,6 +1050,7 @@ fn discover_project_environment(
|
||||
python_preference: PythonPreference,
|
||||
requires_python: Option<&RequiresPython>,
|
||||
keep_incompatible: bool,
|
||||
centralized: bool,
|
||||
cache: &Cache,
|
||||
) -> Result<Option<PythonEnvironment>, ProjectError> {
|
||||
match PythonEnvironment::from_root(root, cache) {
|
||||
@@ -1043,6 +1060,7 @@ fn discover_project_environment(
|
||||
python_preference,
|
||||
requires_python,
|
||||
keep_incompatible,
|
||||
centralized,
|
||||
cache,
|
||||
)),
|
||||
Err(uv_python::Error::MissingEnvironment(_)) => Ok(None),
|
||||
@@ -1055,7 +1073,9 @@ fn discover_project_environment(
|
||||
));
|
||||
}
|
||||
InvalidEnvironmentKind::MissingExecutable(_) => {
|
||||
if fs_err::read_dir(root).is_ok_and(|mut dir| dir.next().is_some()) {
|
||||
if !centralized
|
||||
&& fs_err::read_dir(root).is_ok_and(|mut dir| dir.next().is_some())
|
||||
{
|
||||
if !root.join("pyvenv.cfg").try_exists().unwrap_or_default() {
|
||||
return Err(ProjectError::InvalidProjectEnvironmentDir(
|
||||
root.to_path_buf(),
|
||||
@@ -1094,6 +1114,108 @@ fn discover_project_environment(
|
||||
}
|
||||
}
|
||||
|
||||
fn centralized_environments_enabled(target: &ProjectEnvironmentTarget) -> bool {
|
||||
target.is_default() && uv_preview::is_enabled(PreviewFeature::CentralizedEnvs)
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_centralized_environment_uses_persistent_cache(
|
||||
target: &ProjectEnvironmentTarget,
|
||||
cache: &Cache,
|
||||
) -> Result<(), ProjectError> {
|
||||
if centralized_environments_enabled(target) && cache.is_temporary() {
|
||||
return Err(ProjectError::CentralizedEnvironmentNoCache);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return whether `path` is a directory link to a centralized environment.
|
||||
fn is_centralized_environment_link(path: &Path, cache: &Cache) -> bool {
|
||||
let Ok(target) = fs_err::read_link(path) else {
|
||||
return false;
|
||||
};
|
||||
let environments = cache.bucket(CacheBucket::Environments);
|
||||
// Compare Windows paths in the verbatim namespace so long targets returned with `\\?\` match
|
||||
// the cache root.
|
||||
let starts_with =
|
||||
|path: &Path, base: &Path| verbatim_path(path).starts_with(verbatim_path(base).as_ref());
|
||||
if starts_with(&target, &environments) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Resolve existing indirect links. The lexical check above also recognizes dangling direct
|
||||
// links into the cache.
|
||||
path.simple_canonicalize().is_ok_and(|target| {
|
||||
environments
|
||||
.simple_canonicalize()
|
||||
.is_ok_and(|environments| starts_with(&target, &environments))
|
||||
})
|
||||
}
|
||||
|
||||
fn centralized_environment_root(
|
||||
workspace: &Workspace,
|
||||
interpreter: &Interpreter,
|
||||
cache: &Cache,
|
||||
) -> PathBuf {
|
||||
let digest = cache_digest(&(workspace.install_path(), interpreter.key()));
|
||||
let name = workspace
|
||||
.pyproject_toml()
|
||||
.project
|
||||
.as_ref()
|
||||
.and_then(|project| cache_name(project.name.as_ref(), Some(100)))
|
||||
.or_else(|| {
|
||||
workspace
|
||||
.install_path()
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.and_then(|name| cache_name(name, Some(100)))
|
||||
});
|
||||
let python_version = interpreter.python_version();
|
||||
let entry = name.map_or_else(
|
||||
// A virtual workspace can be nameless if its directory has no cache-safe characters.
|
||||
|| format!("py{python_version}-{digest}"),
|
||||
|name| format!("{name}-py{python_version}-{digest}"),
|
||||
);
|
||||
cache
|
||||
.shard(CacheBucket::Environments, entry)
|
||||
.into_path_buf()
|
||||
}
|
||||
|
||||
fn update_venv_link(
|
||||
environment: &PythonEnvironment,
|
||||
workspace: &Workspace,
|
||||
warn_on_link_errors: bool,
|
||||
) {
|
||||
let link = workspace.install_path().join(".venv");
|
||||
let report_error = |message: &str, err: &std::io::Error| {
|
||||
if warn_on_link_errors {
|
||||
warn_user_once!("{message}: {err}");
|
||||
} else {
|
||||
warn!("{message}: {err}");
|
||||
}
|
||||
};
|
||||
|
||||
if fs_err::symlink_metadata(&link).is_ok_and(|metadata| metadata.is_dir()) {
|
||||
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;
|
||||
}
|
||||
} 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 project environment link at `.venv`", &err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(tk): Fall back to a path file when directory links are unavailable.
|
||||
if let Err(err) = uv_fs::replace_symlink(environment.root(), &link) {
|
||||
report_error("Failed to create project environment link at `.venv`", &err);
|
||||
}
|
||||
}
|
||||
|
||||
/// An interpreter suitable for the project.
|
||||
#[derive(Debug)]
|
||||
#[expect(clippy::large_enum_variant)]
|
||||
@@ -1125,14 +1247,37 @@ impl ProjectInterpreter {
|
||||
requires_python,
|
||||
} = workspace_python;
|
||||
|
||||
// Read from the virtual environment first.
|
||||
let root = workspace.venv(active);
|
||||
if let Some(environment) = discover_project_environment(
|
||||
&root,
|
||||
let environment_target = workspace.venv(active);
|
||||
let centralized = centralized_environments_enabled(&environment_target);
|
||||
|
||||
// For centralized environments, `.venv` identifies the interpreter. The cache path is
|
||||
// derived from the workspace and interpreter instead of trusting the link target.
|
||||
if centralized {
|
||||
let project_environment_path =
|
||||
environment_target.project_path(workspace.install_path());
|
||||
if let Ok(candidate) = PythonEnvironment::from_root(&project_environment_path, cache) {
|
||||
let root = centralized_environment_root(workspace, candidate.interpreter(), cache);
|
||||
if let Some(environment) = discover_project_environment(
|
||||
&root,
|
||||
python_request.as_ref(),
|
||||
python_preference,
|
||||
requires_python.as_ref(),
|
||||
keep_incompatible,
|
||||
centralized,
|
||||
cache,
|
||||
)? {
|
||||
return Ok(Self::Environment(environment));
|
||||
}
|
||||
}
|
||||
} else if let Some(environment) = discover_project_environment(
|
||||
environment_target
|
||||
.project_path(workspace.install_path())
|
||||
.as_ref(),
|
||||
python_request.as_ref(),
|
||||
python_preference,
|
||||
requires_python.as_ref(),
|
||||
keep_incompatible,
|
||||
centralized,
|
||||
cache,
|
||||
)? {
|
||||
return Ok(Self::Environment(environment));
|
||||
@@ -1155,6 +1300,21 @@ impl ProjectInterpreter {
|
||||
)
|
||||
.await?;
|
||||
|
||||
if centralized {
|
||||
let root = centralized_environment_root(workspace, python.interpreter(), cache);
|
||||
if let Some(environment) = discover_project_environment(
|
||||
&root,
|
||||
python_request.as_ref(),
|
||||
python_preference,
|
||||
requires_python.as_ref(),
|
||||
keep_incompatible,
|
||||
centralized,
|
||||
cache,
|
||||
)? {
|
||||
return Ok(Self::Environment(environment));
|
||||
}
|
||||
}
|
||||
|
||||
let managed = python.source().is_managed();
|
||||
let implementation = python.implementation();
|
||||
let interpreter = python.into_interpreter();
|
||||
@@ -1195,7 +1355,7 @@ impl ProjectInterpreter {
|
||||
pub(crate) fn into_interpreter(self) -> Interpreter {
|
||||
match self {
|
||||
Self::Interpreter(interpreter) => interpreter,
|
||||
Self::Environment(venv) => venv.into_interpreter(),
|
||||
Self::Environment(environment) => environment.into_interpreter(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1466,6 +1626,9 @@ pub(crate) enum ProjectEnvironment {
|
||||
|
||||
impl ProjectEnvironment {
|
||||
/// Initialize a virtual environment for the current project.
|
||||
///
|
||||
/// If `warn_on_link_errors` is `true`, failures to update `.venv` are reported to the user.
|
||||
/// Otherwise, they are logged at warning level.
|
||||
pub(crate) async fn get_or_init(
|
||||
workspace: &Workspace,
|
||||
groups: &DependencyGroupsWithDefaults,
|
||||
@@ -1480,7 +1643,12 @@ impl ProjectEnvironment {
|
||||
cache: &Cache,
|
||||
dry_run: DryRun,
|
||||
printer: Printer,
|
||||
warn_on_link_errors: bool,
|
||||
) -> Result<Self, ProjectError> {
|
||||
let environment_target = workspace.venv(active);
|
||||
let centralized = centralized_environments_enabled(&environment_target);
|
||||
ensure_centralized_environment_uses_persistent_cache(&environment_target, cache)?;
|
||||
|
||||
// Lock the project environment to avoid synchronization issues.
|
||||
let _lock = lock_project_environment(workspace)
|
||||
.await
|
||||
@@ -1519,39 +1687,58 @@ impl ProjectEnvironment {
|
||||
.await?
|
||||
{
|
||||
// If we found an existing, compatible environment, use it.
|
||||
ProjectInterpreter::Environment(environment) => Ok(Self::Existing(environment)),
|
||||
ProjectInterpreter::Environment(environment) => {
|
||||
if centralized && !dry_run.enabled() {
|
||||
update_venv_link(&environment, workspace, warn_on_link_errors);
|
||||
}
|
||||
Ok(Self::Existing(environment))
|
||||
}
|
||||
|
||||
// Otherwise, create a virtual environment with the discovered interpreter.
|
||||
ProjectInterpreter::Interpreter(interpreter) => {
|
||||
let root = workspace.venv(active);
|
||||
let root: Cow<'_, Path> = match &environment_target {
|
||||
ProjectEnvironmentTarget::Default if centralized => {
|
||||
Cow::Owned(centralized_environment_root(workspace, &interpreter, cache))
|
||||
}
|
||||
_ => environment_target.project_path(workspace.install_path()),
|
||||
};
|
||||
let centralized_environment_link =
|
||||
!centralized && is_centralized_environment_link(&root, cache);
|
||||
|
||||
// Avoid removing things that are not virtual environments
|
||||
let replace = match (root.try_exists(), root.join("pyvenv.cfg").try_exists()) {
|
||||
// It's a virtual environment we can remove it
|
||||
(_, Ok(true)) => true,
|
||||
// It doesn't exist at all, we should use it without deleting it to avoid TOCTOU bugs
|
||||
(Ok(false), Ok(false)) => false,
|
||||
// If it's not a virtual environment, bail
|
||||
(Ok(true), Ok(false)) => {
|
||||
// Unless it's empty, in which case we just ignore it
|
||||
if root.read_dir().is_ok_and(|mut dir| dir.next().is_none()) {
|
||||
false
|
||||
} else {
|
||||
let replace_environment = if centralized_environment_link {
|
||||
true
|
||||
} else {
|
||||
match (root.try_exists(), root.join("pyvenv.cfg").try_exists()) {
|
||||
// It's a virtual environment we can remove it
|
||||
(_, Ok(true)) => true,
|
||||
// It doesn't exist at all, we should use it without deleting it to avoid TOCTOU bugs
|
||||
(Ok(false), Ok(false)) => false,
|
||||
// If it's not a virtual environment, bail
|
||||
(Ok(true), Ok(false)) => {
|
||||
// Unless it's empty, in which case we just ignore it
|
||||
if root.read_dir().is_ok_and(|mut dir| dir.next().is_none()) {
|
||||
false
|
||||
} else if centralized {
|
||||
// We maintain the cache, eagerly recover a mangled env.
|
||||
true
|
||||
} else {
|
||||
return Err(ProjectError::InvalidProjectEnvironmentDir(
|
||||
root.into_owned(),
|
||||
"it is not a compatible environment but cannot be recreated because it is not a virtual environment".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
// Similarly, if we can't _tell_ if it exists we should bail
|
||||
(_, Err(err)) | (Err(err), _) => {
|
||||
return Err(ProjectError::InvalidProjectEnvironmentDir(
|
||||
root,
|
||||
"it is not a compatible environment but cannot be recreated because it is not a virtual environment".to_string(),
|
||||
root.into_owned(),
|
||||
format!(
|
||||
"it is not a compatible environment but cannot be recreated because uv cannot determine if it is a virtual environment: {err}"
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
// Similarly, if we can't _tell_ if it exists we should bail
|
||||
(_, Err(err)) | (Err(err), _) => {
|
||||
return Err(ProjectError::InvalidProjectEnvironmentDir(
|
||||
root,
|
||||
format!(
|
||||
"it is not a compatible environment but cannot be recreated because uv cannot determine if it is a virtual environment: {err}"
|
||||
),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Determine a prompt for the environment, in order of preference:
|
||||
@@ -1588,33 +1775,50 @@ impl ProjectEnvironment {
|
||||
false,
|
||||
upgradeable,
|
||||
)?;
|
||||
return Ok(if replace {
|
||||
Self::WouldReplace(root, environment, temp_dir)
|
||||
return Ok(if replace_environment {
|
||||
Self::WouldReplace(root.into_owned(), environment, temp_dir)
|
||||
} else {
|
||||
Self::WouldCreate(root, environment, temp_dir)
|
||||
Self::WouldCreate(root.into_owned(), environment, temp_dir)
|
||||
});
|
||||
}
|
||||
|
||||
// Remove the existing virtual environment if it doesn't meet the requirements.
|
||||
if replace {
|
||||
match uv_fs::remove_virtualenv(&root) {
|
||||
Ok(()) => {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"Removed virtual environment at: {}",
|
||||
root.user_display().cyan()
|
||||
)?;
|
||||
if replace_environment {
|
||||
// `clear_virtualenv` follows directory links, so unlink centralized links
|
||||
// directly to preserve their cached targets.
|
||||
let removed = if centralized_environment_link {
|
||||
match uv_fs::remove_virtualenv(&root) {
|
||||
Ok(()) => true,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => false,
|
||||
Err(err) => return Err(uv_virtualenv::Error::from(err).into()),
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(err) => return Err(uv_virtualenv::Error::from(err).into()),
|
||||
} else {
|
||||
uv_fs::clear_virtualenv(&root).map_err(uv_virtualenv::Error::from)?
|
||||
};
|
||||
if removed {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"Removed virtual environment at: {}",
|
||||
root.user_display().cyan()
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"Creating virtual environment at: {}",
|
||||
root.user_display().cyan()
|
||||
)?;
|
||||
if centralized {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"Creating virtual environment `{}` in the centralized store",
|
||||
root.file_name()
|
||||
.unwrap_or(root.as_os_str())
|
||||
.to_string_lossy()
|
||||
.cyan(),
|
||||
)?;
|
||||
} else {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"Creating virtual environment at: {}",
|
||||
root.user_display().cyan()
|
||||
)?;
|
||||
}
|
||||
|
||||
let environment = uv_virtualenv::create_venv(
|
||||
&root,
|
||||
@@ -1629,7 +1833,11 @@ impl ProjectEnvironment {
|
||||
upgradeable,
|
||||
)?;
|
||||
|
||||
if replace {
|
||||
if centralized {
|
||||
update_venv_link(&environment, workspace, warn_on_link_errors);
|
||||
}
|
||||
|
||||
if replace_environment {
|
||||
Ok(Self::Replaced(environment))
|
||||
} else {
|
||||
Ok(Self::Created(environment))
|
||||
|
||||
@@ -33,6 +33,7 @@ use crate::commands::project::lock_target::LockTarget;
|
||||
use crate::commands::project::{
|
||||
ProjectEnvironment, ProjectError, ProjectInterpreter, ScriptInterpreter, UniversalState,
|
||||
WorkspacePython, default_dependency_groups,
|
||||
ensure_centralized_environment_uses_persistent_cache,
|
||||
};
|
||||
use crate::commands::{ExitStatus, diagnostics, project};
|
||||
use crate::printer::Printer;
|
||||
@@ -188,6 +189,16 @@ pub(crate) async fn remove(
|
||||
|
||||
let content = toml.to_string();
|
||||
|
||||
if frozen.is_none()
|
||||
&& !no_sync
|
||||
&& let RemoveTarget::Project(project) = &target
|
||||
{
|
||||
ensure_centralized_environment_uses_persistent_cache(
|
||||
&project.workspace().venv(active),
|
||||
cache,
|
||||
)?;
|
||||
}
|
||||
|
||||
// Save the modified `pyproject.toml` or script.
|
||||
target.write(&content)?;
|
||||
|
||||
@@ -266,6 +277,7 @@ pub(crate) async fn remove(
|
||||
cache,
|
||||
DryRun::Disabled,
|
||||
printer,
|
||||
true,
|
||||
)
|
||||
.await?
|
||||
.into_environment()?;
|
||||
|
||||
@@ -713,6 +713,7 @@ pub(crate) async fn run(
|
||||
&cache,
|
||||
DryRun::Disabled,
|
||||
printer,
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.into_environment()?
|
||||
|
||||
@@ -171,6 +171,7 @@ pub(crate) async fn sync(
|
||||
cache,
|
||||
dry_run,
|
||||
printer,
|
||||
true,
|
||||
)
|
||||
.await?,
|
||||
),
|
||||
|
||||
@@ -37,7 +37,7 @@ use crate::commands::project::install_target::InstallTarget;
|
||||
use crate::commands::project::lock::LockMode;
|
||||
use crate::commands::project::{
|
||||
ProjectEnvironment, ProjectError, ProjectInterpreter, UniversalState, WorkspacePython,
|
||||
default_dependency_groups,
|
||||
default_dependency_groups, ensure_centralized_environment_uses_persistent_cache,
|
||||
};
|
||||
use crate::commands::{ExitStatus, diagnostics, project};
|
||||
use crate::printer::Printer;
|
||||
@@ -338,6 +338,12 @@ pub(crate) async fn project_version(
|
||||
let status = if dry_run {
|
||||
ExitStatus::Success
|
||||
} else if let Some(new_version) = &new_version {
|
||||
if frozen.is_none() && !no_sync {
|
||||
ensure_centralized_environment_uses_persistent_cache(
|
||||
&project.workspace().venv(active),
|
||||
cache,
|
||||
)?;
|
||||
}
|
||||
let project = update_project(
|
||||
project,
|
||||
new_version,
|
||||
@@ -653,6 +659,7 @@ async fn lock_and_sync(
|
||||
cache,
|
||||
DryRun::Disabled,
|
||||
printer,
|
||||
true,
|
||||
)
|
||||
.await?
|
||||
.into_environment()?;
|
||||
|
||||
@@ -134,7 +134,12 @@ pub(crate) async fn venv(
|
||||
let path = path.unwrap_or_else(|| {
|
||||
project_environment.map_or_else(
|
||||
|| PathBuf::from(".venv"),
|
||||
|workspace| workspace.venv(Some(false)),
|
||||
|workspace| {
|
||||
workspace
|
||||
.venv(Some(false))
|
||||
.project_path(workspace.install_path())
|
||||
.into_owned()
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ use crate::commands::project::lock::{LockMode, LockOperation};
|
||||
use crate::commands::project::lock_target::LockTarget;
|
||||
use crate::commands::project::{
|
||||
ProjectEnvironment, ProjectError, ProjectInterpreter, UniversalState, WorkspacePython,
|
||||
ensure_centralized_environment_uses_persistent_cache,
|
||||
};
|
||||
use crate::commands::{ExitStatus, diagnostics};
|
||||
use crate::printer::Printer;
|
||||
@@ -65,6 +66,14 @@ pub(crate) async fn metadata(
|
||||
workspace_cache,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if sync {
|
||||
ensure_centralized_environment_uses_persistent_cache(
|
||||
&virtual_project.workspace().venv(Some(false)),
|
||||
cache,
|
||||
)?;
|
||||
}
|
||||
|
||||
let target = LockTarget::Workspace(virtual_project.workspace());
|
||||
|
||||
// Don't enable any groups' requires-python for interpreter discovery.
|
||||
@@ -148,6 +157,7 @@ pub(crate) async fn metadata(
|
||||
cache,
|
||||
DryRun::Disabled,
|
||||
printer,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
let module_owners = collect_module_owners(
|
||||
|
||||
@@ -169,6 +169,62 @@ requires-python = ">=3.12"
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_centralized_environment_no_cache_does_not_modify_project() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
fs_err::remove_dir_all(&context.venv)?;
|
||||
let pyproject = indoc! {r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
"#};
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(pyproject)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.version()
|
||||
.arg("0.2.0")
|
||||
.arg("--no-cache")
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs"), @r#"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: `--no-cache` is unsupported with the `centralized-envs` feature enabled
|
||||
"#);
|
||||
|
||||
assert_eq!(fs_err::read_to_string(&pyproject_toml)?, pyproject);
|
||||
|
||||
uv_snapshot!(context.filters(), context.version()
|
||||
.arg("0.2.0")
|
||||
.arg("--no-sync")
|
||||
.arg("--no-cache")
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs"), @"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
project 0.1.0 => 0.2.0
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||||
Resolved 1 package in [TIME]
|
||||
");
|
||||
|
||||
assert!(!context.venv.exists());
|
||||
assert_snapshot!(fs_err::read_to_string(pyproject_toml)?, @r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.2.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
"#);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Set the version (--short)
|
||||
#[test]
|
||||
fn version_set_value_short() -> Result<()> {
|
||||
|
||||
@@ -146,6 +146,61 @@ fn add_registry() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_centralized_environment_no_cache_does_not_modify_project() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
fs_err::remove_dir_all(&context.venv)?;
|
||||
let pyproject = indoc! {r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["iniconfig"]
|
||||
"#};
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(pyproject)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.remove()
|
||||
.arg("iniconfig")
|
||||
.arg("--no-cache")
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs"), @r#"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: `--no-cache` is unsupported with the `centralized-envs` feature enabled
|
||||
"#);
|
||||
|
||||
assert_eq!(fs_err::read_to_string(pyproject_toml)?, pyproject);
|
||||
|
||||
uv_snapshot!(context.filters(), context.remove()
|
||||
.arg("iniconfig")
|
||||
.arg("--no-sync")
|
||||
.arg("--no-cache")
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs"), @"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||||
Resolved 1 package in [TIME]
|
||||
");
|
||||
|
||||
assert!(!context.venv.exists());
|
||||
assert_snapshot!(context.read("pyproject.toml"), @r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
"#);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a Git requirement.
|
||||
#[test]
|
||||
#[cfg(feature = "test-git")]
|
||||
|
||||
@@ -7088,3 +7088,78 @@ async fn run_malware_detected() {
|
||||
error: Malware detected in one or more dependencies that would be installed; aborting sync. Set `UV_MALWARE_CHECK=0` to bypass this check.
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_centralized_environment_no_cache_is_rejected() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
context
|
||||
.temp_dir
|
||||
.child("pyproject.toml")
|
||||
.write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
"#})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.run()
|
||||
.arg("--no-cache")
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs")
|
||||
.arg("python")
|
||||
.arg("-c")
|
||||
.arg("pass"), @r#"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: `--no-cache` is unsupported with the `centralized-envs` feature enabled
|
||||
"#);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_centralized_environment_no_sync_uses_incompatible_python() -> Result<()> {
|
||||
let context = uv_test::test_context_with_versions!(&["3.11", "3.12"]);
|
||||
context
|
||||
.temp_dir
|
||||
.child("pyproject.toml")
|
||||
.write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = []
|
||||
"#})?;
|
||||
context
|
||||
.sync()
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs")
|
||||
.arg("--python")
|
||||
.arg("3.12")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
// `--no-sync` reuses the existing environment despite the Python request.
|
||||
uv_snapshot!(context.filters(), context.run()
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs")
|
||||
.arg("--no-sync")
|
||||
.arg("--python")
|
||||
.arg("3.11")
|
||||
.arg("python")
|
||||
.arg("-c")
|
||||
.arg("import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"), @r#"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
3.12
|
||||
|
||||
----- stderr -----
|
||||
warning: Using incompatible environment (`project-py3.12.[X]-[HASH]`) due to `--no-sync` (The project environment's Python version does not satisfy the request: `Python 3.11`)
|
||||
"#);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -8,6 +8,39 @@ use url::Url;
|
||||
use uv_static::EnvVars;
|
||||
use uv_test::uv_snapshot;
|
||||
|
||||
#[test]
|
||||
fn tree_centralized_environment_no_cache() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
fs_err::remove_dir_all(&context.venv)?;
|
||||
context
|
||||
.temp_dir
|
||||
.child("pyproject.toml")
|
||||
.write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
"#})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.tree()
|
||||
.arg("--no-cache")
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs"), @"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
project v0.1.0
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||||
Resolved 1 package in [TIME]
|
||||
");
|
||||
|
||||
assert!(!context.venv.exists());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_dependencies() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
|
||||
@@ -0,0 +1,653 @@
|
||||
use anyhow::Result;
|
||||
use assert_cmd::prelude::*;
|
||||
use assert_fs::prelude::*;
|
||||
use insta::assert_snapshot;
|
||||
use serde_json::json;
|
||||
|
||||
use uv_fs::Simplified;
|
||||
use uv_static::EnvVars;
|
||||
#[cfg(unix)]
|
||||
use uv_test::ReadOnlyDirectoryGuard;
|
||||
use uv_test::{TestContext, uv_snapshot};
|
||||
|
||||
fn write_project(
|
||||
context: &TestContext,
|
||||
requires_python: &str,
|
||||
dependencies: &[&str],
|
||||
) -> Result<()> {
|
||||
let pyproject_toml = toml::to_string(&json!({
|
||||
"project": {
|
||||
"name": "project",
|
||||
"version": "0.1.0",
|
||||
"requires-python": requires_python,
|
||||
"dependencies": dependencies,
|
||||
}
|
||||
}))?;
|
||||
context
|
||||
.temp_dir
|
||||
.child("pyproject.toml")
|
||||
.write_str(&pyproject_toml)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_centralized_env() -> Result<()> {
|
||||
let context = uv_test::test_context_with_versions!(&["3.12"]);
|
||||
write_project(&context, ">=3.12", &["iniconfig"])?;
|
||||
|
||||
// Creates the environment in the centralized store.
|
||||
uv_snapshot!(context.filters(), context.sync()
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs"), @r#"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||||
Creating virtual environment `project-py3.12.[X]-[HASH]` in the centralized store
|
||||
Resolved 2 packages in [TIME]
|
||||
Prepared 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ iniconfig==2.0.0
|
||||
"#);
|
||||
|
||||
let link = context.temp_dir.child(".venv");
|
||||
let target = fs_err::read_link(link.path())?;
|
||||
// The project link points into the cache.
|
||||
insta::with_settings!({ filters => context.filters() }, {
|
||||
assert_snapshot!(target.portable_display(), @"[CACHE_DIR]/environments-v2/project-py3.12.[X]-[HASH]");
|
||||
});
|
||||
|
||||
fs_err::remove_dir_all(link.path())?;
|
||||
|
||||
// Reuses the cache entry without recreating `.venv`.
|
||||
uv_snapshot!(context.filters(), context.sync()
|
||||
.arg("--dry-run")
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs"), @r#"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Would use project environment at: [CACHE_DIR]/environments-v2/project-py3.12.[X]-[HASH]
|
||||
Resolved 2 packages in [TIME]
|
||||
Found up-to-date lockfile at: uv.lock
|
||||
Checked 1 package in [TIME]
|
||||
Would make no changes
|
||||
"#);
|
||||
// Only the cached environment remains.
|
||||
assert!(!link.exists());
|
||||
assert!(target.is_dir());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_centralized_env_switch_python() -> Result<()> {
|
||||
let context = uv_test::test_context_with_versions!(&["3.11", "3.12"]);
|
||||
write_project(&context, ">=3.11", &[])?;
|
||||
|
||||
context
|
||||
.sync()
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs")
|
||||
.arg("--python")
|
||||
.arg("3.12")
|
||||
.assert()
|
||||
.success();
|
||||
let link_312 = fs_err::read_link(context.temp_dir.child(".venv").path())?;
|
||||
insta::with_settings!({ filters => context.filters() }, {
|
||||
assert_snapshot!(link_312.portable_display(), @"[CACHE_DIR]/environments-v2/project-py3.12.[X]-[HASH]");
|
||||
});
|
||||
let reuse_marker = context.temp_dir.child(".venv").child("reuse-marker");
|
||||
reuse_marker.touch()?;
|
||||
|
||||
context
|
||||
.sync()
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs")
|
||||
.arg("--python")
|
||||
.arg("3.11")
|
||||
.assert()
|
||||
.success();
|
||||
let link_311 = fs_err::read_link(context.temp_dir.child(".venv").path())?;
|
||||
insta::with_settings!({ filters => context.filters() }, {
|
||||
assert_snapshot!(link_311.portable_display(), @"[CACHE_DIR]/environments-v2/project-py3.11.[X]-[HASH]");
|
||||
});
|
||||
|
||||
context
|
||||
.sync()
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs")
|
||||
.arg("--python")
|
||||
.arg("3.12")
|
||||
.assert()
|
||||
.success();
|
||||
// The original environment is reused, not recreated.
|
||||
assert!(reuse_marker.exists());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "test-python-managed")]
|
||||
fn sync_centralized_env_distinguishes_python_patch() -> Result<()> {
|
||||
let context = uv_test::test_context_with_versions!(&[])
|
||||
.with_managed_python_dirs()
|
||||
.with_python_download_cache();
|
||||
context
|
||||
.python_install()
|
||||
.arg("3.12.9")
|
||||
.arg("3.12.11")
|
||||
.assert()
|
||||
.success();
|
||||
write_project(&context, ">=3.12", &[])?;
|
||||
|
||||
context
|
||||
.sync()
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs")
|
||||
.arg("--python")
|
||||
.arg("3.12.9")
|
||||
.assert()
|
||||
.success();
|
||||
let first = fs_err::read_link(context.temp_dir.child(".venv").path())?;
|
||||
insta::with_settings!({ filters => context.filters() }, {
|
||||
assert_snapshot!(first.portable_display(), @"[CACHE_DIR]/environments-v2/project-py3.12.9-[HASH]");
|
||||
});
|
||||
|
||||
context
|
||||
.sync()
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs")
|
||||
.arg("--python")
|
||||
.arg("3.12.11")
|
||||
.assert()
|
||||
.success();
|
||||
let second = fs_err::read_link(context.temp_dir.child(".venv").path())?;
|
||||
insta::with_settings!({ filters => context.filters() }, {
|
||||
assert_snapshot!(second.portable_display(), @"[CACHE_DIR]/environments-v2/project-py3.12.11-[HASH]");
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_centralized_env_avoids_project_name_collisions() -> Result<()> {
|
||||
let context = uv_test::test_context_with_versions!(&["3.12"]);
|
||||
let project_a = context.temp_dir.child("project-a");
|
||||
let project_b = context.temp_dir.child("project-b");
|
||||
for project in [&project_a, &project_b] {
|
||||
project.create_dir_all()?;
|
||||
project.child("pyproject.toml").write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
"#,
|
||||
)?;
|
||||
}
|
||||
|
||||
for project in [&project_a, &project_b] {
|
||||
context
|
||||
.sync()
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs")
|
||||
.arg("--project")
|
||||
.arg(project.path())
|
||||
.assert()
|
||||
.success();
|
||||
}
|
||||
|
||||
let target_a = fs_err::read_link(project_a.child(".venv").path())?;
|
||||
let target_b = fs_err::read_link(project_b.child(".venv").path())?;
|
||||
// Projects with the same name use different environments.
|
||||
assert_ne!(target_a, target_b);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_centralized_env_respects_explicit_environments() -> Result<()> {
|
||||
let context = uv_test::test_context_with_versions!(&["3.12"]);
|
||||
write_project(&context, ">=3.12", &[])?;
|
||||
|
||||
context
|
||||
.sync()
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs")
|
||||
.env(EnvVars::UV_PROJECT_ENVIRONMENT, "override")
|
||||
.assert()
|
||||
.success();
|
||||
// `UV_PROJECT_ENVIRONMENT` bypasses centralized environments.
|
||||
assert!(context.temp_dir.child("override").is_dir());
|
||||
assert!(!context.temp_dir.child(".venv").exists());
|
||||
|
||||
let active = context.temp_dir.child("active");
|
||||
context.venv().arg(active.path()).assert().success();
|
||||
context
|
||||
.sync()
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs")
|
||||
.arg("--active")
|
||||
.env_remove(EnvVars::UV_PROJECT_ENVIRONMENT)
|
||||
.env(EnvVars::VIRTUAL_ENV, active.path())
|
||||
.assert()
|
||||
.success();
|
||||
// `--active` uses `VIRTUAL_ENV` directly.
|
||||
assert!(active.is_dir());
|
||||
assert!(!context.temp_dir.child(".venv").exists());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_centralized_env_respects_active_default_environment() -> Result<()> {
|
||||
let context = uv_test::test_context_with_versions!(&["3.12"]);
|
||||
write_project(&context, ">=3.12", &[])?;
|
||||
let environment = context.temp_dir.child(".venv");
|
||||
context.venv().arg(environment.path()).assert().success();
|
||||
environment.child("user-marker").touch()?;
|
||||
|
||||
context
|
||||
.sync()
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs")
|
||||
.arg("--active")
|
||||
.env(EnvVars::VIRTUAL_ENV, environment.path())
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
assert!(environment.child("user-marker").is_file());
|
||||
assert!(!context.cache_dir.child("environments-v2").exists());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_centralized_env_virtual_workspace() -> Result<()> {
|
||||
let context = uv_test::test_context_with_versions!(&["3.12"]);
|
||||
context.temp_dir.child("pyproject.toml").write_str(
|
||||
r#"
|
||||
[tool.uv.workspace]
|
||||
members = ["member"]
|
||||
"#,
|
||||
)?;
|
||||
let member = context.temp_dir.child("member");
|
||||
member.create_dir_all()?;
|
||||
member.child("pyproject.toml").write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "member"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
"#,
|
||||
)?;
|
||||
|
||||
context
|
||||
.sync()
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let target = fs_err::read_link(context.temp_dir.child(".venv").path())?;
|
||||
// The workspace root owns the centralized environment.
|
||||
insta::with_settings!({ filters => context.filters() }, {
|
||||
assert_snapshot!(target.portable_display(), @"[CACHE_DIR]/environments-v2/temp-py3.12.[X]-[HASH]");
|
||||
});
|
||||
// The workspace member does not get its own environment.
|
||||
assert!(!member.child(".venv").exists());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_centralized_env_dry_run() -> Result<()> {
|
||||
let context = uv_test::test_context_with_versions!(&["3.12"]);
|
||||
write_project(&context, ">=3.12", &["iniconfig"])?;
|
||||
|
||||
// Reports the environment without creating it.
|
||||
uv_snapshot!(context.filters(), context.sync()
|
||||
.arg("--dry-run")
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs"), @r#"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||||
Would create project environment at: [CACHE_DIR]/environments-v2/project-py3.12.[X]-[HASH]
|
||||
Resolved 2 packages in [TIME]
|
||||
Would create lockfile at: uv.lock
|
||||
Would download 1 package
|
||||
Would install 1 package
|
||||
+ iniconfig==2.0.0
|
||||
"#);
|
||||
// Dry-run leaves the filesystem unchanged.
|
||||
assert!(!context.temp_dir.child(".venv").exists());
|
||||
assert!(!context.cache_dir.child("environments-v2").exists());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_prune_removes_and_recreates_centralized_environment() -> Result<()> {
|
||||
let context = uv_test::test_context_with_versions!(&["3.12"]);
|
||||
write_project(&context, ">=3.12", &[])?;
|
||||
|
||||
context
|
||||
.sync()
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs")
|
||||
.assert()
|
||||
.success();
|
||||
let link = context.temp_dir.child(".venv");
|
||||
let target = fs_err::read_link(link.path())?;
|
||||
|
||||
context.prune().assert().success();
|
||||
assert!(!target.exists());
|
||||
assert_eq!(target, fs_err::read_link(link.path())?);
|
||||
|
||||
context
|
||||
.sync()
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs")
|
||||
.assert()
|
||||
.success();
|
||||
// The recreated environment uses the same cache entry.
|
||||
assert_eq!(target, fs_err::read_link(link.path())?);
|
||||
// The dangling target is recreated.
|
||||
assert!(target.is_dir());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_recovers_incomplete_centralized_environment() -> Result<()> {
|
||||
let context = uv_test::test_context_with_versions!(&["3.12"]);
|
||||
write_project(&context, ">=3.12", &[])?;
|
||||
context
|
||||
.sync()
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let link = context.temp_dir.child(".venv");
|
||||
let target = fs_err::read_link(link.path())?;
|
||||
|
||||
// Simulate a mangled environment (e.g., due to interruption).
|
||||
uv_fs::remove_virtualenv(link.path())?;
|
||||
uv_fs::remove_virtualenv(&target)?;
|
||||
fs_err::create_dir(&target)?;
|
||||
uv_fs::cachedir::ensure_tag(&target)?;
|
||||
fs_err::write(target.join(".gitignore"), "*")?;
|
||||
|
||||
context
|
||||
.sync()
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
assert_eq!(target, fs_err::read_link(link.path())?);
|
||||
assert!(target.join("pyvenv.cfg").is_file());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_centralized_env_replaces_existing_local_environment() -> Result<()> {
|
||||
let context = uv_test::test_context_with_versions!(&["3.12"]);
|
||||
write_project(&context, ">=3.12", &[])?;
|
||||
context.sync().assert().success();
|
||||
|
||||
context
|
||||
.sync()
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let target = fs_err::read_link(context.temp_dir.child(".venv").path())?;
|
||||
// The local environment is replaced by a link into the cache.
|
||||
insta::with_settings!({ filters => context.filters() }, {
|
||||
assert_snapshot!(target.portable_display(), @"[CACHE_DIR]/environments-v2/project-py3.12.[X]-[HASH]");
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_centralized_env_replaces_existing_directory_link() -> Result<()> {
|
||||
let context = uv_test::test_context_with_versions!(&["3.12"]);
|
||||
write_project(&context, ">=3.12", &[])?;
|
||||
let environment = context.temp_dir.child(".venv");
|
||||
let previous_target = context.temp_dir.child("previous-target");
|
||||
previous_target.create_dir_all()?;
|
||||
let marker = previous_target.child("marker");
|
||||
marker.touch()?;
|
||||
uv_fs::create_symlink(previous_target.path(), environment.path())?;
|
||||
|
||||
context
|
||||
.sync()
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let target = fs_err::read_link(environment.path())?;
|
||||
insta::with_settings!({ filters => context.filters() }, {
|
||||
assert_snapshot!(target.portable_display(), @"[CACHE_DIR]/environments-v2/project-py3.12.[X]-[HASH]");
|
||||
});
|
||||
assert!(marker.is_file());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_centralized_env_with_existing_file() -> Result<()> {
|
||||
let context = uv_test::test_context_with_versions!(&["3.12"]);
|
||||
write_project(&context, ">=3.12", &[])?;
|
||||
let environment = context.temp_dir.child(".venv");
|
||||
environment.write_str("user-data")?;
|
||||
|
||||
context
|
||||
.sync()
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let target = fs_err::read_link(environment.path())?;
|
||||
insta::with_settings!({ filters => context.filters() }, {
|
||||
assert_snapshot!(target.portable_display(), @"[CACHE_DIR]/environments-v2/project-py3.12.[X]-[HASH]");
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// TODO(tk): This behavior will change once path files are added.
|
||||
assert_eq!(fs_err::read_to_string(environment.path())?, "user-data");
|
||||
assert!(context.cache_dir.child("environments-v2").is_dir());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn sync_centralized_env_replaces_existing_empty_directory() -> Result<()> {
|
||||
let context = uv_test::test_context_with_versions!(&["3.12"]);
|
||||
write_project(&context, ">=3.12", &[])?;
|
||||
context.temp_dir.child(".venv").create_dir_all()?;
|
||||
|
||||
context
|
||||
.sync()
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let target = fs_err::read_link(context.temp_dir.child(".venv").path())?;
|
||||
insta::with_settings!({ filters => context.filters() }, {
|
||||
assert_snapshot!(target.portable_display(), @"[CACHE_DIR]/environments-v2/project-py3.12.[X]-[HASH]");
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_and_sync_link_failure_reporting() -> Result<()> {
|
||||
let context = uv_test::test_context_with_versions!(&["3.12"]).with_filter((
|
||||
r"(?m)^warning: Failed to create project environment link at `.venv`: .*$",
|
||||
"warning: Failed to create project environment link at `.venv`: [ERR]",
|
||||
));
|
||||
write_project(&context, ">=3.12", &["iniconfig"])?;
|
||||
let environment = context.temp_dir.child(".venv");
|
||||
environment.create_dir_all()?;
|
||||
environment.child("keep").touch()?;
|
||||
|
||||
// `uv run` uses the centralized environment without reporting a link update failure.
|
||||
uv_snapshot!(context.filters(), context.run()
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs")
|
||||
.arg("python")
|
||||
.arg("-c")
|
||||
.arg("import iniconfig"), @r#"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||||
Creating virtual environment `project-py3.12.[X]-[HASH]` in the centralized store
|
||||
Resolved 2 packages in [TIME]
|
||||
Prepared 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ iniconfig==2.0.0
|
||||
"#);
|
||||
|
||||
assert!(environment.child("keep").is_file());
|
||||
|
||||
// `uv sync` reports the same link update failure to the user.
|
||||
uv_snapshot!(context.filters(), context.sync()
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs"), @r#"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: Failed to create project environment link at `.venv`: [ERR]
|
||||
Resolved 2 packages in [TIME]
|
||||
Checked 1 package in [TIME]
|
||||
"#);
|
||||
|
||||
assert!(environment.child("keep").is_file());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn sync_centralized_env_link_creation_failure_preserves_cached_target() -> Result<()> {
|
||||
let context = uv_test::test_context_with_versions!(&["3.12"])
|
||||
.with_filter((r"\.tmp[a-zA-Z0-9]+", "[TMP]"));
|
||||
write_project(&context, ">=3.12", &[])?;
|
||||
context
|
||||
.sync()
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let environment = context.temp_dir.child(".venv");
|
||||
let target = fs_err::read_link(environment.path())?;
|
||||
let marker = target.join("marker");
|
||||
fs_err::write(&marker, "")?;
|
||||
|
||||
let _guard = ReadOnlyDirectoryGuard::new(context.temp_dir.path())?;
|
||||
uv_snapshot!(context.filters(), context.sync()
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs"), @r#"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: Failed to create project environment link at `.venv`: Permission denied (os error 13) at path "[TEMP_DIR]/[TMP]"
|
||||
Resolved 1 package in [TIME]
|
||||
Checked in [TIME]
|
||||
"#);
|
||||
|
||||
assert_eq!(target, fs_err::read_link(environment.path())?);
|
||||
assert!(marker.is_file());
|
||||
assert!(target.join("pyvenv.cfg").is_file());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_replaces_environment_links_without_removing_cached_targets() -> Result<()> {
|
||||
let context = uv_test::test_context_with_versions!(&["3.11", "3.12"]);
|
||||
write_project(&context, ">=3.11", &[])?;
|
||||
context
|
||||
.sync()
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs")
|
||||
.arg("--python")
|
||||
.arg("3.12")
|
||||
.assert()
|
||||
.success();
|
||||
let cache_target = fs_err::read_link(context.temp_dir.child(".venv").path())?;
|
||||
let cache_marker = cache_target.join("marker");
|
||||
fs_err::write(&cache_marker, "")?;
|
||||
|
||||
let override_environment = context.temp_dir.child("override");
|
||||
uv_fs::create_symlink(&cache_target, override_environment.path())?;
|
||||
context
|
||||
.sync()
|
||||
.arg("--preview-features")
|
||||
.arg("centralized-envs")
|
||||
.arg("--python")
|
||||
.arg("3.11")
|
||||
.env(EnvVars::UV_PROJECT_ENVIRONMENT, "override")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
// An explicit environment path is local, but replacing it does not remove the cached target.
|
||||
assert!(override_environment.is_dir());
|
||||
assert!(fs_err::read_link(override_environment.path()).is_err());
|
||||
assert!(cache_marker.is_file());
|
||||
|
||||
let environment = context.temp_dir.child(".venv");
|
||||
let intermediate = context.temp_dir.child("intermediate");
|
||||
uv_fs::create_symlink(&cache_target, intermediate.path())?;
|
||||
uv_fs::replace_symlink(intermediate.path(), environment.path())?;
|
||||
|
||||
context
|
||||
.sync()
|
||||
.arg("--python")
|
||||
.arg("3.11")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
// Without the preview, the indirect cache link is replaced by a local environment.
|
||||
assert!(environment.is_dir());
|
||||
assert!(fs_err::read_link(environment.path()).is_err());
|
||||
// The cached environment remains available for later reuse.
|
||||
assert!(cache_marker.is_file());
|
||||
|
||||
// Replacing an incompatible user-managed `.venv` link rebuilds its target in place.
|
||||
let target = context.temp_dir.child("environment");
|
||||
fs_err::rename(environment.path(), target.path())?;
|
||||
uv_fs::create_symlink(target.path(), environment.path())?;
|
||||
let marker = target.child("marker");
|
||||
marker.touch()?;
|
||||
context
|
||||
.sync()
|
||||
.arg("--python")
|
||||
.arg("3.12")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
assert!(fs_err::read_link(environment.path()).is_ok());
|
||||
// The link remains, but its target is a fresh Python 3.12 environment.
|
||||
assert!(!marker.exists());
|
||||
assert!(target.child("pyvenv.cfg").is_file());
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
//! Integration tests for uv synchronization and settings.
|
||||
|
||||
#[cfg(all(feature = "test-python", feature = "test-pypi"))]
|
||||
mod centralized_envs;
|
||||
|
||||
#[cfg(all(feature = "test-python", feature = "test-pypi"))]
|
||||
mod show_settings;
|
||||
|
||||
|
||||
@@ -3053,6 +3053,7 @@ fn preview_features() {
|
||||
+ VenvSafeClear,
|
||||
+ Check,
|
||||
+ PackagedInit,
|
||||
+ CentralizedEnvs,
|
||||
+ ],
|
||||
},
|
||||
python_preference: Managed,
|
||||
|
||||
@@ -102,6 +102,59 @@ fn workspace_metadata_simple() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_metadata_sync_centralized_environment() -> Result<()> {
|
||||
let context = uv_test::test_context_with_versions!(&["3.12"]);
|
||||
|
||||
context.temp_dir.child("pyproject.toml").write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Rejects `--no-cache` without modifying the project.
|
||||
uv_snapshot!(context.filters(), context.workspace_metadata()
|
||||
.arg("--sync")
|
||||
.arg("--no-cache")
|
||||
.arg("--preview-features")
|
||||
.arg("workspace-metadata,centralized-envs"), @r#"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: `--no-cache` is unsupported with the `centralized-envs` feature enabled
|
||||
"#);
|
||||
|
||||
assert!(!context.temp_dir.child("uv.lock").exists());
|
||||
assert!(!context.temp_dir.child(".venv").exists());
|
||||
|
||||
// A subsequent sync creates and reports the centralized environment.
|
||||
let assert = context
|
||||
.workspace_metadata()
|
||||
.arg("--sync")
|
||||
.arg("--preview-features")
|
||||
.arg("workspace-metadata,centralized-envs")
|
||||
.assert()
|
||||
.success();
|
||||
let metadata: serde_json::Value = serde_json::from_slice(&assert.get_output().stdout)?;
|
||||
let target = fs_err::read_link(context.temp_dir.child(".venv").path())?;
|
||||
|
||||
assert_eq!(
|
||||
metadata["environment"]["root"].as_str().map(Path::new),
|
||||
Some(target.as_path())
|
||||
);
|
||||
assert_eq!(
|
||||
target.parent(),
|
||||
Some(context.cache_dir.child("environments-v2").path())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_metadata_module_owners_from_locked_wheels() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
|
||||
@@ -70,6 +70,9 @@ The following preview features are available:
|
||||
|
||||
- `add-bounds`: Allows configuring the
|
||||
[default bounds for `uv add`](../reference/settings.md#add-bounds) invocations.
|
||||
- `centralized-envs`: Stores
|
||||
[project virtual environments](../reference/storage.md#project-virtual-environments) in the uv
|
||||
cache.
|
||||
- `json-output`: Allows `--output-format json` for various uv commands.
|
||||
- `package-conflicts`: Allows defining workspace conflicts at the package level.
|
||||
- `pylock`: Allows installing from `pylock.toml` files.
|
||||
|
||||
@@ -194,6 +194,17 @@ Use the `UV_PROJECT_ENVIRONMENT` environment variable to override this location.
|
||||
see the
|
||||
[projects environment documentation](../concepts/projects/config.md#project-environment-path).
|
||||
|
||||
With the [`centralized-envs` preview feature](../concepts/preview.md), uv stores the default project
|
||||
environment in its cache. uv attempts to maintain a `.venv` directory link to the cached environment
|
||||
so existing activation and editor workflows can continue to use the usual path. If link creation
|
||||
fails, uv continues using the cached environment directly, but tools relying on `.venv` may not
|
||||
discover it. Switching interpreters can reuse their respective cached environments.
|
||||
|
||||
Explicit project environment paths, including `UV_PROJECT_ENVIRONMENT` and environments selected
|
||||
with `--active`, are not centralized. Centralized environments are not supported with `--no-cache`.
|
||||
Like other cache data, they can be removed by `uv cache clean` or `uv cache prune` and are recreated
|
||||
when next needed.
|
||||
|
||||
### Script virtual environments
|
||||
|
||||
When running [scripts with inline metadata](../guides/scripts.md), uv creates a dedicated virtual
|
||||
|
||||
Generated
+2
-1
@@ -1673,7 +1673,8 @@
|
||||
"malware-check",
|
||||
"venv-safe-clear",
|
||||
"check-command",
|
||||
"packaged-init"
|
||||
"packaged-init",
|
||||
"centralized-envs"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user