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
39 changes: 31 additions & 8 deletions crates/uv/src/commands/project/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,15 @@ impl CachedEnvironment {
printer: Printer,
preview: PreviewMode,
) -> Result<Self, ProjectError> {
let interpreter = Self::base_interpreter(interpreter, cache)?;
// 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)?;

// Resolve the requirements with the interpreter.
let resolution = Resolution::from(
resolve_environment(
spec,
&interpreter,
&base_interpreter,
build_constraints.clone(),
&settings.resolver,
network_settings,
Expand All @@ -73,13 +75,34 @@ impl CachedEnvironment {
hash_digest(&distributions)
};

// Hash the interpreter based on its path.
// TODO(charlie): Come up with a robust hash for the interpreter.
let interpreter_hash =
cache_digest(&canonicalize_executable(interpreter.sys_executable())?);
// Construct a hash for the environment.
//
// 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.
//
// 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()?,
));

// Search in the content-addressed cache.
let cache_entry = cache.entry(CacheBucket::Environments, interpreter_hash, resolution_hash);
let cache_entry = cache.entry(CacheBucket::Environments, environment_hash, resolution_hash);

if cache.refresh().is_none() {
if let Ok(root) = cache.resolve_link(cache_entry.path()) {
Expand All @@ -93,7 +116,7 @@ impl CachedEnvironment {
let temp_dir = cache.venv_dir()?;
let venv = uv_virtualenv::create_venv(
temp_dir.path(),
interpreter,
base_interpreter,
uv_virtualenv::Prompt::None,
false,
false,
Expand Down
22 changes: 14 additions & 8 deletions crates/uv/tests/it/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4777,7 +4777,7 @@ fn run_groups_include_requires_python() -> Result<()> {
bar = ["iniconfig"]
baz = ["iniconfig"]
dev = ["sniffio", {include-group = "foo"}, {include-group = "baz"}]


[tool.uv.dependency-groups]
foo = {requires-python="<3.13"}
Expand Down Expand Up @@ -4876,7 +4876,7 @@ fn exit_status_signal() -> Result<()> {

#[test]
fn run_repeated() -> Result<()> {
let context = TestContext::new_with_versions(&["3.13"]);
let context = TestContext::new_with_versions(&["3.13", "3.12"]);

let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
Expand Down Expand Up @@ -4923,22 +4923,25 @@ fn run_repeated() -> Result<()> {
Resolved 1 package in [TIME]
"###);

// Re-running as a tool shouldn't require reinstalling `typing-extensions`, since the environment is cached.
// Re-running as a tool does require reinstalling `typing-extensions`, since the base venv is
// different.
uv_snapshot!(
context.filters(),
context.tool_run().arg("--with").arg("typing-extensions").arg("python").arg("-c").arg("import typing_extensions; import iniconfig"), @r###"
context.tool_run().arg("--with").arg("typing-extensions").arg("python").arg("-c").arg("import typing_extensions; import iniconfig"), @r#"
success: false
exit_code: 1
----- stdout -----

----- stderr -----
Resolved 1 package in [TIME]
Installed 1 package in [TIME]
+ typing-extensions==4.10.0
Traceback (most recent call last):
File "<string>", line 1, in <module>
import typing_extensions; import iniconfig
^^^^^^^^^^^^^^^^
ModuleNotFoundError: No module named 'iniconfig'
"###);
"#);

Ok(())
}
Expand Down Expand Up @@ -4979,22 +4982,25 @@ fn run_without_overlay() -> Result<()> {
+ typing-extensions==4.10.0
"###);

// Import `iniconfig` in the context of a `tool run` command, which should fail.
// 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.
uv_snapshot!(
context.filters(),
context.tool_run().arg("--with").arg("typing-extensions").arg("python").arg("-c").arg("import typing_extensions; import iniconfig"), @r###"
context.tool_run().arg("--with").arg("typing-extensions").arg("python").arg("-c").arg("import typing_extensions; import iniconfig"), @r#"
success: false
exit_code: 1
----- stdout -----

----- stderr -----
Resolved 1 package in [TIME]
Installed 1 package in [TIME]
+ typing-extensions==4.10.0
Traceback (most recent call last):
File "<string>", line 1, in <module>
import typing_extensions; import iniconfig
^^^^^^^^^^^^^^^^
ModuleNotFoundError: No module named 'iniconfig'
"###);
"#);

// Re-running in the context of the project should reset the overlay.
uv_snapshot!(
Expand Down