diff --git a/crates/uv-resolver/src/pubgrub/report.rs b/crates/uv-resolver/src/pubgrub/report.rs index d2c1b46b7..564b2fecd 100644 --- a/crates/uv-resolver/src/pubgrub/report.rs +++ b/crates/uv-resolver/src/pubgrub/report.rs @@ -544,10 +544,8 @@ impl PubGrubReportFormatter<'_> { DerivationTree::External(External::Custom(package, set, reason)) => { if let Some(name) = package.name_no_root() { // Check for no versions due to pre-release options. - if options.flexibility == Flexibility::Configurable { - if !fork_urls.contains_key(name) { - self.prerelease_available_hint(name, set, selector, env, output_hints); - } + if !fork_urls.contains_key(name) { + self.prerelease_hint(name, set, selector, env, options, output_hints); } // Check for no versions due to no `--find-links` flat index. @@ -604,10 +602,8 @@ impl PubGrubReportFormatter<'_> { DerivationTree::External(External::NoVersions(package, set)) => { if let Some(name) = package.name_no_root() { // Check for no versions due to pre-release options. - if options.flexibility == Flexibility::Configurable { - if !fork_urls.contains_key(name) { - self.prerelease_available_hint(name, set, selector, env, output_hints); - } + if !fork_urls.contains_key(name) { + self.prerelease_hint(name, set, selector, env, options, output_hints); } // Check for no versions due to no `--find-links` flat index. @@ -936,14 +932,19 @@ impl PubGrubReportFormatter<'_> { } } - fn prerelease_available_hint( + fn prerelease_hint( &self, name: &PackageName, set: &Range, selector: &CandidateSelector, env: &ResolverEnvironment, + options: &Options, hints: &mut IndexSet, ) { + if selector.prerelease_strategy().allows(name, env) == AllowPrerelease::Yes { + return; + } + let any_prerelease = set.iter().any(|(start, end)| { // Ignore, e.g., `>=2.4.dev0,<2.5.dev0`, which is the desugared form of `==2.4.*`. if PrefixMatch::from_range(start, end).is_some() { @@ -973,11 +974,19 @@ impl PubGrubReportFormatter<'_> { if any_prerelease { // A pre-release marker appeared in the version requirements. - if selector.prerelease_strategy().allows(name, env) != AllowPrerelease::Yes { - hints.insert(PubGrubHint::PrereleaseRequested { - name: name.clone(), - range: set.clone(), - }); + match options.flexibility { + Flexibility::Configurable => { + hints.insert(PubGrubHint::PrereleaseRequested { + name: name.clone(), + range: set.clone(), + }); + } + Flexibility::Fixed => { + hints.insert(PubGrubHint::BuildPrereleaseRequested { + name: name.clone(), + range: set.clone(), + }); + } } } else if let Some(version) = self.available_versions.get(name).and_then(|versions| { versions @@ -987,11 +996,19 @@ impl PubGrubReportFormatter<'_> { .find(|version| set.contains(version)) }) { // There are pre-release versions available for the package. - if selector.prerelease_strategy().allows(name, env) != AllowPrerelease::Yes { - hints.insert(PubGrubHint::PrereleaseAvailable { - package: name.clone(), - version: version.clone(), - }); + match options.flexibility { + Flexibility::Configurable => { + hints.insert(PubGrubHint::PrereleaseAvailable { + package: name.clone(), + version: version.clone(), + }); + } + Flexibility::Fixed => { + hints.insert(PubGrubHint::BuildPrereleaseAvailable { + package: name.clone(), + version: version.clone(), + }); + } } } } @@ -1007,6 +1024,13 @@ pub(crate) enum PubGrubHint { // excluded from `PartialEq` and `Hash` version: Version, }, + /// The resolver runs with fixed options (e.g., for build environments) and requires explicit + /// pre-release opt-in for a package that only has pre-releases available. + BuildPrereleaseAvailable { + package: PackageName, + // excluded from `PartialEq` and `Hash` + version: Version, + }, /// A requirement included a pre-release marker, but pre-releases weren't enabled for that /// package. PrereleaseRequested { @@ -1014,6 +1038,13 @@ pub(crate) enum PubGrubHint { // excluded from `PartialEq` and `Hash` range: Range, }, + /// A requirement included a pre-release marker, but the resolver runs with fixed options + /// (e.g., for build environments) and cannot enable pre-releases automatically. + BuildPrereleaseRequested { + name: PackageName, + // excluded from `PartialEq` and `Hash` + range: Range, + }, /// Requirements were unavailable due to lookups in the index being disabled and no extra /// index was provided via `--find-links` NoIndex, @@ -1159,9 +1190,15 @@ enum PubGrubHintCore { PrereleaseAvailable { package: PackageName, }, + BuildPrereleaseAvailable { + package: PackageName, + }, PrereleaseRequested { package: PackageName, }, + BuildPrereleaseRequested { + package: PackageName, + }, NoIndex, Offline, InvalidPackageMetadata { @@ -1228,9 +1265,15 @@ impl From for PubGrubHintCore { PubGrubHint::PrereleaseAvailable { package, .. } => { Self::PrereleaseAvailable { package } } + PubGrubHint::BuildPrereleaseAvailable { package, .. } => { + Self::BuildPrereleaseAvailable { package } + } PubGrubHint::PrereleaseRequested { name: package, .. } => { Self::PrereleaseRequested { package } } + PubGrubHint::BuildPrereleaseRequested { name: package, .. } => { + Self::BuildPrereleaseRequested { package } + } PubGrubHint::NoIndex => Self::NoIndex, PubGrubHint::Offline => Self::Offline, PubGrubHint::InvalidPackageMetadata { package, .. } => { @@ -1314,6 +1357,18 @@ impl std::fmt::Display for PubGrubHint { "--prerelease=allow".green(), ) } + Self::BuildPrereleaseAvailable { package, version } => { + let spec = format!("{package}>={version}"); + write!( + f, + "{}{} Only pre-releases of `{}` (e.g., {}) match these build requirements, and build environments can't enable pre-releases automatically. Add `{}` to `build-system.requires`, `[tool.uv.extra-build-dependencies]`, or supply it via `uv build --build-constraint`.", + "hint".bold().cyan(), + ":".bold(), + package.cyan(), + version.cyan(), + spec.cyan(), + ) + } Self::PrereleaseRequested { name, range } => { write!( f, @@ -1325,6 +1380,17 @@ impl std::fmt::Display for PubGrubHint { "--prerelease=allow".green(), ) } + Self::BuildPrereleaseRequested { name, range } => { + write!( + f, + "{}{} `{}` was requested with a pre-release marker (e.g., {}), but build environments can't opt into pre-releases automatically. Add `{}` to `build-system.requires`, `[tool.uv.extra-build-dependencies]`, or supply it via `uv build --build-constraint`.", + "hint".bold().cyan(), + ":".bold(), + name.cyan(), + PackageRange::compatibility(&PubGrubPackage::base(name), range, None).cyan(), + PackageRange::compatibility(&PubGrubPackage::base(name), range, None).cyan(), + ) + } Self::NoIndex => { write!( f, diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 825de07a0..2440f9acf 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -19,7 +19,7 @@ use wiremock::{ use crate::common::{self, decode_token}; use crate::common::{ DEFAULT_PYTHON_VERSION, TestContext, build_vendor_links_url, download_to_disk, get_bin, - uv_snapshot, venv_bin_path, + packse_index_url, uv_snapshot, venv_bin_path, }; use uv_fs::Simplified; use uv_static::EnvVars; @@ -3685,6 +3685,50 @@ fn install_git_source_respects_offline_mode() { ); } +/// Build requirements should explain how to opt into prereleases when they are the only solution. +#[test] +fn build_prerelease_hint() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + + [build-system] + requires = ["transitive-package-only-prereleases-in-range-a"] + build-backend = "setuptools.build_meta" + "#})?; + + let mut command = context.pip_install(); + command.arg("--index-url").arg(packse_index_url()).arg("."); + command.env_remove(EnvVars::UV_EXCLUDE_NEWER); + + uv_snapshot!( + context.filters(), + command, + @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + × Failed to build `project @ file://[TEMP_DIR]/` + ├─▶ Failed to resolve requirements from `build-system.requires` + ├─▶ No solution found when resolving: `transitive-package-only-prereleases-in-range-a` + ╰─▶ Because only transitive-package-only-prereleases-in-range-b<0.1 is available and transitive-package-only-prereleases-in-range-a==0.1.0 depends on transitive-package-only-prereleases-in-range-b>0.1, we can conclude that transitive-package-only-prereleases-in-range-a==0.1.0 cannot be used. + And because only transitive-package-only-prereleases-in-range-a==0.1.0 is available and you require transitive-package-only-prereleases-in-range-a, we can conclude that your requirements are unsatisfiable. + + hint: Only pre-releases of `transitive-package-only-prereleases-in-range-b` (e.g., 1.0.0a1) match these build requirements, and build environments can't enable pre-releases automatically. Add `transitive-package-only-prereleases-in-range-b>=1.0.0a1` to `build-system.requires`, `[tool.uv.extra-build-dependencies]`, or supply it via `uv build --build-constraint`. + " + ); + + Ok(()) +} + /// Test that constraint markers are respected when validating the current environment (i.e., we /// skip resolution entirely). #[test]