diff --git a/crates/uv-python/src/environment.rs b/crates/uv-python/src/environment.rs index f76f41182..07f3ddb54 100644 --- a/crates/uv-python/src/environment.rs +++ b/crates/uv-python/src/environment.rs @@ -174,7 +174,7 @@ impl PythonEnvironment { /// N.B. This function also works for system Python environments and users depend on this. pub fn from_root(root: impl AsRef, cache: &Cache) -> Result { debug!( - "Checking for Python environment at: {}", + "Checking for Python environment at: `{}`", root.as_ref().user_display() ); match root.as_ref().try_exists() { diff --git a/crates/uv/src/commands/project/environment.rs b/crates/uv/src/commands/project/environment.rs index 294b4c828..cf1add99a 100644 --- a/crates/uv/src/commands/project/environment.rs +++ b/crates/uv/src/commands/project/environment.rs @@ -107,15 +107,13 @@ impl CachedEnvironment { printer: Printer, preview: PreviewMode, ) -> Result { - // Resolve the "base" interpreter, which resolves to an underlying parent interpreter if the - // given interpreter is a virtual environment. - let base_interpreter = Self::base_interpreter(interpreter, cache)?; + let interpreter = Self::base_interpreter(interpreter, cache)?; // Resolve the requirements with the interpreter. let resolution = Resolution::from( resolve_environment( spec, - &base_interpreter, + &interpreter, build_constraints.clone(), &settings.resolver, network_settings, @@ -143,29 +141,20 @@ impl CachedEnvironment { // Use the canonicalized base interpreter path since that's the interpreter we performed the // resolution with and the interpreter the environment will be created with. // - // We also include the canonicalized `sys.prefix` of the non-base interpreter, that is, the - // virtual environment's path. Originally, we shared cached environments independent of the - // environment they'd be layered on top of. However, this causes collisions as the overlay - // `.pth` file can be overridden by another instance of uv. Including this element in the key - // avoids this problem at the cost of creating separate cached environments for identical - // `--with` invocations across projects. We use `sys.prefix` rather than `sys.executable` so - // we can canonicalize it without invalidating the purpose of the element — it'd probably be - // safe to just use the absolute `sys.executable` as well. - // - // TODO(zanieb): Since we're not sharing these environmments across projects, we should move - // [`CachedEvnvironment::set_overlay`] etc. here since the values there should be constant - // now. + // We cache environments independent of the environment they'd be layered on top of. The + // assumption is such that the environment will _not_ be modified by the user or uv; + // otherwise, we risk cache poisoning. For example, if we were to write a `.pth` file to + // the cached environment, it would be shared across all projects that use the same + // interpreter and the same cached dependencies. // // TODO(zanieb): We should include the version of the base interpreter in the hash, so if // the interpreter at the canonicalized path changes versions we construct a new // environment. - let environment_hash = cache_digest(&( - &canonicalize_executable(base_interpreter.sys_executable())?, - &interpreter.sys_prefix().canonicalize()?, - )); + let interpreter_hash = + cache_digest(&canonicalize_executable(interpreter.sys_executable())?); // Search in the content-addressed cache. - let cache_entry = cache.entry(CacheBucket::Environments, environment_hash, resolution_hash); + let cache_entry = cache.entry(CacheBucket::Environments, interpreter_hash, resolution_hash); if cache.refresh().is_none() { if let Ok(root) = cache.resolve_link(cache_entry.path()) { @@ -179,7 +168,7 @@ impl CachedEnvironment { let temp_dir = cache.venv_dir()?; let venv = uv_virtualenv::create_venv( temp_dir.path(), - base_interpreter, + interpreter, uv_virtualenv::Prompt::None, false, false, diff --git a/crates/uv/tests/it/run.rs b/crates/uv/tests/it/run.rs index 98c2adbfe..9cb92c80d 100644 --- a/crates/uv/tests/it/run.rs +++ b/crates/uv/tests/it/run.rs @@ -1302,7 +1302,6 @@ fn run_with_pyvenv_cfg_file() -> Result<()> { uv = [UV_VERSION] version_info = 3.12.[X] include-system-site-packages = false - relocatable = true extends-environment = [PARENT_VENV] @@ -4778,7 +4777,6 @@ fn run_groups_include_requires_python() -> Result<()> { baz = ["iniconfig"] dev = ["sniffio", {include-group = "foo"}, {include-group = "baz"}] - [tool.uv.dependency-groups] foo = {requires-python="<3.13"} bar = {requires-python=">=3.13"} @@ -4923,8 +4921,8 @@ fn run_repeated() -> Result<()> { Resolved 1 package in [TIME] "###); - // Re-running as a tool does require reinstalling `typing-extensions`, since the base venv is - // different. + // Re-running as a tool doesn't require reinstalling `typing-extensions`, since the environment + // is cached. uv_snapshot!( context.filters(), context.tool_run().arg("--with").arg("typing-extensions").arg("python").arg("-c").arg("import typing_extensions; import iniconfig"), @r#" @@ -4934,8 +4932,6 @@ fn run_repeated() -> Result<()> { ----- stderr ----- Resolved 1 package in [TIME] - Installed 1 package in [TIME] - + typing-extensions==4.10.0 Traceback (most recent call last): File "", line 1, in import typing_extensions; import iniconfig @@ -4982,8 +4978,7 @@ fn run_without_overlay() -> Result<()> { + typing-extensions==4.10.0 "###); - // Import `iniconfig` in the context of a `tool run` command, which should fail. Note that - // typing-extensions gets installed again, because the venv is not shared. + // Import `iniconfig` in the context of a `tool run` command, which should fail. uv_snapshot!( context.filters(), context.tool_run().arg("--with").arg("typing-extensions").arg("python").arg("-c").arg("import typing_extensions; import iniconfig"), @r#" @@ -4993,8 +4988,6 @@ fn run_without_overlay() -> Result<()> { ----- stderr ----- Resolved 1 package in [TIME] - Installed 1 package in [TIME] - + typing-extensions==4.10.0 Traceback (most recent call last): File "", line 1, in import typing_extensions; import iniconfig