diff --git a/crates/uv/src/commands/project/environment.rs b/crates/uv/src/commands/project/environment.rs index 2d4f8aaab946..d6789c5dcb8b 100644 --- a/crates/uv/src/commands/project/environment.rs +++ b/crates/uv/src/commands/project/environment.rs @@ -74,8 +74,10 @@ impl CachedEnvironment { // Hash the resolution by hashing the generated lockfile. // TODO(charlie): If the resolution contains any mutable metadata (like a path or URL // dependency), skip this step. - let distributions = resolution.distributions().collect::>(); - let resolution_hash = hash_digest(&distributions); + let resolution_hash = { + let distributions = resolution.distributions().collect::>(); + hash_digest(&distributions) + }; // Hash the interpreter based on its path. // TODO(charlie): Come up with a robust hash for the interpreter. @@ -84,24 +86,39 @@ impl CachedEnvironment { // Search in the content-addressed cache. let cache_entry = cache.entry(CacheBucket::Environments, interpreter_hash, resolution_hash); - // Lock the interpreter, to avoid concurrent modification across processes. + // Lock at the interpreter level, to avoid concurrent modification across processes. fs_err::tokio::create_dir_all(cache_entry.dir()).await?; let _lock = LockedFile::acquire( cache_entry.dir().join(".lock"), cache_entry.dir().user_display(), )?; - // If the receipt exists, return the environment. let ok = cache_entry.path().join(".ok"); - if ok.is_file() { - debug!( - "Found existing cached environment at: `{}`", - cache_entry.path().display() - ); - return Ok(Self(PythonEnvironment::from_root( - cache_entry.path(), - cache, - )?)); + + if settings.reinstall.is_none() { + // If the receipt exists, return the environment. + if ok.is_file() { + debug!( + "Reusing cached environment at: `{}`", + cache_entry.path().display() + ); + return Ok(Self(PythonEnvironment::from_root( + cache_entry.path(), + cache, + )?)); + } + } else { + // If the receipt exists, remove it. + match fs_err::tokio::remove_file(&ok).await { + Ok(()) => { + debug!( + "Removed receipt for environment at: `{}`", + cache_entry.path().display() + ); + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} + Err(err) => return Err(err.into()), + } } debug!( @@ -117,8 +134,6 @@ impl CachedEnvironment { false, )?; - // TODO(charlie): Rather than passing all the arguments to `sync_environment`, return a - // struct that lets us "continue" from `resolve_environment`. let venv = sync_environment( venv, &resolution, diff --git a/crates/uv/tests/tool_run.rs b/crates/uv/tests/tool_run.rs index e19e4b51e7cb..939ea1235b9a 100644 --- a/crates/uv/tests/tool_run.rs +++ b/crates/uv/tests/tool_run.rs @@ -508,6 +508,63 @@ fn tool_run_cache() { Resolved [N] packages in [TIME] "###); + // Verify that `--reinstall` reinstalls everything. + uv_snapshot!(context.filters(), context.tool_run() + .arg("-p") + .arg("3.12") + .arg("--reinstall") + .arg("black") + .arg("--version") + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + black, 24.3.0 (compiled: yes) + Python (CPython) 3.12.[X] + + ----- stderr ----- + warning: `uv tool run` is experimental and may change without warning + Resolved [N] packages in [TIME] + Prepared [N] packages in [TIME] + Installed [N] packages in [TIME] + + black==24.3.0 + + click==8.1.7 + + mypy-extensions==1.0.0 + + packaging==24.0 + + pathspec==0.12.1 + + platformdirs==4.2.0 + "###); + + // Verify that `--reinstall-package` reinstalls everything. We may want to change this. + uv_snapshot!(context.filters(), context.tool_run() + .arg("-p") + .arg("3.12") + .arg("--reinstall-package") + .arg("packaging") + .arg("black") + .arg("--version") + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + black, 24.3.0 (compiled: yes) + Python (CPython) 3.12.[X] + + ----- stderr ----- + warning: `uv tool run` is experimental and may change without warning + Resolved [N] packages in [TIME] + Prepared [N] packages in [TIME] + Installed [N] packages in [TIME] + + black==24.3.0 + + click==8.1.7 + + mypy-extensions==1.0.0 + + packaging==24.0 + + pathspec==0.12.1 + + platformdirs==4.2.0 + "###); + // Verify that varying the interpreter leads to a fresh environment. uv_snapshot!(context.filters(), context.tool_run() .arg("-p")