mirror of
https://github.com/astral-sh/uv
synced 2026-06-21 13:47:25 +00:00
Take project environment lock when using uv venv in a project (#19837)
## Summary We added a global workspace lock for project-oriented uv commands creating/re-creating venvs but the same was not done for `uv venv` when it operates on a project environment. This PR remedies this. ## Test Plan There's a new test, not sure how good of an idea it is to set RUST_LOG there....
This commit is contained in:
committed by
GitHub
parent
1ecdc2b6c0
commit
b08e2151b2
@@ -1167,19 +1167,21 @@ impl ProjectInterpreter {
|
||||
Self::Environment(venv) => venv.into_interpreter(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Grab a file lock for the environment to prevent concurrent writes across processes.
|
||||
async fn lock(workspace: &Workspace) -> Result<LockedFile, LockedFileError> {
|
||||
LockedFile::acquire(
|
||||
std::env::temp_dir().join(format!(
|
||||
"uv-{}.lock",
|
||||
cache_digest(workspace.install_path())
|
||||
)),
|
||||
LockedFileMode::Exclusive,
|
||||
workspace.install_path().simplified_display(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
/// Grab a file lock for the project environment to prevent concurrent writes across processes.
|
||||
pub(crate) async fn lock_project_environment(
|
||||
workspace: &Workspace,
|
||||
) -> Result<LockedFile, LockedFileError> {
|
||||
LockedFile::acquire(
|
||||
std::env::temp_dir().join(format!(
|
||||
"uv-{}.lock",
|
||||
cache_digest(workspace.install_path())
|
||||
)),
|
||||
LockedFileMode::Exclusive,
|
||||
workspace.install_path().simplified_display(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// The source of a `Requires-Python` specifier.
|
||||
@@ -1449,7 +1451,7 @@ impl ProjectEnvironment {
|
||||
printer: Printer,
|
||||
) -> Result<Self, ProjectError> {
|
||||
// Lock the project environment to avoid synchronization issues.
|
||||
let _lock = ProjectInterpreter::lock(workspace)
|
||||
let _lock = lock_project_environment(workspace)
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
warn!("Failed to acquire project environment lock: {err}");
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::vec;
|
||||
use anyhow::Result;
|
||||
use owo_colors::OwoColorize;
|
||||
use thiserror::Error;
|
||||
use tracing::warn;
|
||||
|
||||
use uv_cache::Cache;
|
||||
use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
|
||||
@@ -38,7 +39,9 @@ use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceCache, WorkspaceEr
|
||||
use crate::commands::ExitStatus;
|
||||
use crate::commands::pip::loggers::{DefaultInstallLogger, InstallLogger};
|
||||
use crate::commands::pip::operations::{Changelog, report_interpreter};
|
||||
use crate::commands::project::{WorkspacePython, validate_project_requires_python};
|
||||
use crate::commands::project::{
|
||||
WorkspacePython, lock_project_environment, validate_project_requires_python,
|
||||
};
|
||||
use crate::commands::reporters::PythonDownloadReporter;
|
||||
use crate::printer::Printer;
|
||||
|
||||
@@ -119,19 +122,21 @@ pub(crate) async fn venv(
|
||||
}
|
||||
};
|
||||
|
||||
// Determine the default path; either the virtual environment for the project or `.venv`
|
||||
let path = path.unwrap_or(
|
||||
project
|
||||
.as_ref()
|
||||
.and_then(|project| {
|
||||
// Only use the project environment path if we're invoked from the root
|
||||
// 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)))
|
||||
})
|
||||
.unwrap_or(PathBuf::from(".venv")),
|
||||
);
|
||||
// Only use the project environment path if we're invoked from the root with no explicit path.
|
||||
// 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`.
|
||||
let project_environment = project
|
||||
.as_ref()
|
||||
.map(VirtualProject::workspace)
|
||||
.filter(|workspace| path.is_none() && workspace.install_path() == project_dir);
|
||||
|
||||
// Determine the default path; either the virtual environment for the project or `.venv`.
|
||||
let path = path.unwrap_or_else(|| {
|
||||
project_environment.map_or_else(
|
||||
|| PathBuf::from(".venv"),
|
||||
|workspace| workspace.venv(Some(false)),
|
||||
)
|
||||
});
|
||||
|
||||
let reporter = PythonDownloadReporter::single(printer);
|
||||
|
||||
@@ -201,6 +206,18 @@ pub(crate) async fn venv(
|
||||
.as_ref()
|
||||
.is_none_or(|request| !request.includes_patch());
|
||||
|
||||
// Lock the project environment to avoid synchronization issues.
|
||||
let _lock = if let Some(workspace) = project_environment {
|
||||
lock_project_environment(workspace)
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
warn!("Failed to acquire project environment lock: {err}");
|
||||
})
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Create the virtual environment.
|
||||
let venv = uv_virtualenv::create_venv(
|
||||
&path,
|
||||
|
||||
@@ -3,6 +3,8 @@ use assert_cmd::prelude::*;
|
||||
use assert_fs::prelude::*;
|
||||
use indoc::indoc;
|
||||
use predicates::prelude::*;
|
||||
use uv_cache_key::cache_digest;
|
||||
use uv_fs::{LockedFile, LockedFileMode};
|
||||
use uv_python::{PYTHON_VERSION_FILENAME, PYTHON_VERSIONS_FILENAME};
|
||||
use uv_static::EnvVars;
|
||||
|
||||
@@ -212,6 +214,56 @@ fn create_venv_project_environment() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_venv_project_environment_lock() -> 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"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Simulate another project command holding the environment lock.
|
||||
let install_path = dunce::canonicalize(context.temp_dir.path())?;
|
||||
let lock_path = std::env::temp_dir().join(format!("uv-{}.lock", cache_digest(&install_path)));
|
||||
let lock = LockedFile::acquire(
|
||||
&lock_path,
|
||||
LockedFileMode::Exclusive,
|
||||
context.temp_dir.path().display(),
|
||||
)
|
||||
.await?;
|
||||
let context = context.with_filtered_path(&lock_path, "PROJECT_ENVIRONMENT_LOCK");
|
||||
|
||||
// A pathless invocation from the project root uses the workspace-derived lock, including when
|
||||
// `UV_PROJECT_ENVIRONMENT` changes the environment path. Lock errors warn and continue.
|
||||
uv_snapshot!(context.filters(), context.venv()
|
||||
.env(EnvVars::UV_PROJECT_ENVIRONMENT, "foo")
|
||||
.env(EnvVars::RUST_LOG, "warn")
|
||||
.env(EnvVars::UV_LOCK_TIMEOUT, "1"), @"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||||
Creating virtual environment at: foo
|
||||
WARN Failed to acquire project environment lock: Timeout ([TIME]) when waiting for lock on `[TEMP_DIR]/` at `[PROJECT_ENVIRONMENT_LOCK]/`, is another uv process running? You can set `UV_LOCK_TIMEOUT` to increase the timeout.
|
||||
Activate with: source foo/[BIN]/activate
|
||||
");
|
||||
drop(lock);
|
||||
|
||||
context
|
||||
.temp_dir
|
||||
.child("foo")
|
||||
.assert(predicates::path::is_dir());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn virtual_empty() -> Result<()> {
|
||||
// testing how `uv venv` reacts to a pyproject with no `[project]` and nothing useful to it
|
||||
|
||||
Reference in New Issue
Block a user