diff --git a/crates/uv-installer/src/plan.rs b/crates/uv-installer/src/plan.rs index 616725fac591b..4659173f303a3 100644 --- a/crates/uv-installer/src/plan.rs +++ b/crates/uv-installer/src/plan.rs @@ -141,6 +141,24 @@ impl<'a> Planner<'a> { } RequirementSatisfaction::OutOfDate => { debug!("Requirement installed, but not fresh: {installed}"); + + // If we made it here, something went wrong in the resolver, because it returned an + // already-installed distribution that we "shouldn't" use. Typically, this means the + // distribution was considered out-of-date, but in a way that the resolver didn't + // detect, and is indicative of drift between the resolver's candidate selector and + // the install plan. For example, at present, the resolver doesn't check that an + // installed distribution was built with the expected build settings. Treat it as + // up-to-date for now; it's just means we may not rebuild a package when we otherwise + // should. This is a known issue, but should only affect the `uv pip` CLI, as the + // project APIs never return installed distributions during resolution (i.e., the + // resolver is stateless). + // TODO(charlie): Incorporate these checks into the resolver. + if matches!(dist, ResolvedDist::Installed { .. }) { + warn!( + "Installed distribution was considered out-of-date, but returned by the resolver: {dist}" + ); + continue; + } } RequirementSatisfaction::CacheInvalid => { // Already logged diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index fa33574068a70..af6c4aee8ea9f 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -12583,3 +12583,70 @@ fn overlapping_packages_warning() -> Result<()> { Ok(()) } + +/// See: +#[test] +fn transitive_dependency_config_settings_invalidation() -> Result<()> { + let context = TestContext::new("3.12"); + + // Create a local package named `idna`. + context + .temp_dir + .child("idna") + .child("pyproject.toml") + .write_str(indoc! { + r#" + [project] + name = "idna" + version = "3.6" + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + "# + })?; + context + .temp_dir + .child("idna") + .child("src") + .child("idna") + .child("__init__.py") + .touch()?; + + // Install the local `idna` package. + uv_snapshot!(context.filters(), context.pip_install() + .arg(context.temp_dir.child("idna").path()), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + idna==3.6 (from file://[TEMP_DIR]/idna) + " + ); + + // Install a package that depends on `idna`, with a `--config-settings` value. + // + // This "should" rebuild `idna`, but for now, we reuse the "stale" distribution. Prior to + // https://github.com/astral-sh/uv/pull/15389, this would panic. + uv_snapshot!(context.filters(), context.pip_install() + .arg("anyio") + .arg("--config-settings=foo=bar"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + anyio==4.3.0 + + sniffio==1.3.1 + " + ); + + Ok(()) +}