diff --git a/crates/uv-tool/src/lib.rs b/crates/uv-tool/src/lib.rs index e7235ec3df37..923dc2514a9b 100644 --- a/crates/uv-tool/src/lib.rs +++ b/crates/uv-tool/src/lib.rs @@ -204,7 +204,7 @@ impl InstalledTools { match PythonEnvironment::from_root(&environment_path, cache) { Ok(venv) => { debug!( - "Using existing environment for tool `{name}`: {}", + "Found existing environment for tool `{name}`: {}", environment_path.user_display() ); Ok(Some(venv)) diff --git a/crates/uv/src/commands/tool/install.rs b/crates/uv/src/commands/tool/install.rs index cba65fc909fc..33d9a73f6c47 100644 --- a/crates/uv/src/commands/tool/install.rs +++ b/crates/uv/src/commands/tool/install.rs @@ -276,19 +276,27 @@ pub(crate) async fn install( installed_tools .get_environment(&from.name, &cache)? .filter(|environment| { - python_request.as_ref().map_or(true, |python_request| { - if python_request.satisfied(environment.interpreter(), &cache) { - debug!("Found existing environment for `{from}`", from = from.name.cyan()); - true - } else { - let _ = writeln!( - printer.stderr(), - "Existing environment for `{from}` does not satisfy the requested Python interpreter", - from = from.name.cyan(), - ); - false - } - }) + // NOTE(lucab): this compares `base_prefix` paths as a proxy for + // detecting interpreters mismatches. Directly comparing interpreters + // (by paths or binaries on-disk) would result in several false + // positives on Windows due to file-copying and shims. + let old_base_prefix = environment.interpreter().sys_base_prefix(); + let selected_base_prefix = interpreter.sys_base_prefix(); + if old_base_prefix == selected_base_prefix { + debug!( + "Found existing interpreter for tool `{}`: {}", + from.name, + environment.interpreter().sys_executable().display() + ); + true + } else { + let _ = writeln!( + printer.stderr(), + "Ignored existing environment for `{from}` due to stale Python interpreter", + from = from.name.cyan(), + ); + false + } }); // If the requested and receipt requirements are the same... diff --git a/crates/uv/tests/tool_install.rs b/crates/uv/tests/tool_install.rs index f350c1df1754..6a3bb07ba8e3 100644 --- a/crates/uv/tests/tool_install.rs +++ b/crates/uv/tests/tool_install.rs @@ -2072,9 +2072,10 @@ fn tool_install_upgrade() { }); } -/// Test reinstalling tools with varying `--python` requests. +/// Test reinstalling tools with varying `--python` and +/// `--python-preference` parameters. #[test] -fn tool_install_python_request() { +fn tool_install_python_params() { let context = TestContext::new_with_versions(&["3.11", "3.12"]) .with_filtered_counts() .with_filtered_exe_suffix(); @@ -2122,10 +2123,12 @@ fn tool_install_python_request() { `black` is already installed "###); - // Install with Python 3.11 (incompatible). + // Install with system Python 3.11 (different version, incompatible). uv_snapshot!(context.filters(), context.tool_install() .arg("-p") .arg("3.11") + .arg("--python-preference") + .arg("only-system") .arg("black") .env("UV_TOOL_DIR", tool_dir.as_os_str()) .env("XDG_BIN_HOME", bin_dir.as_os_str()) @@ -2135,7 +2138,7 @@ fn tool_install_python_request() { ----- stdout ----- ----- stderr ----- - Existing environment for `black` does not satisfy the requested Python interpreter + Ignored existing environment for `black` due to stale Python interpreter Resolved [N] packages in [TIME] Prepared [N] packages in [TIME] Installed [N] packages in [TIME] @@ -2147,6 +2150,69 @@ fn tool_install_python_request() { + platformdirs==4.2.0 Installed 2 executables: black, blackd "###); + + // Install with system Python 3.11 (compatible). + uv_snapshot!(context.filters(), context.tool_install() + .arg("-p") + .arg("3.11") + .arg("--python-preference") + .arg("only-system") + .arg("black") + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()) + .env("PATH", bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + `black` is already installed + "###); + + // Install with managed Python 3.11 (different source, incompatible). + uv_snapshot!(context.filters(), context.tool_install() + .arg("-p") + .arg("3.11") + .arg("--python-preference") + .arg("only-managed") + .arg("black") + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()) + .env("PATH", bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Ignored existing environment for `black` due to stale Python interpreter + Resolved [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 + Installed 2 executables: black, blackd + "###); + + // Install with managed Python 3.11 (compatible). + uv_snapshot!(context.filters(), context.tool_install() + .arg("-p") + .arg("3.11") + .arg("--python-preference") + .arg("only-managed") + .arg("black") + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()) + .env("PATH", bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + `black` is already installed + "###); } /// Test preserving a tool environment when new but incompatible requirements are requested.