Add prerelease guidance for build-system resolution failures (#16550)

Resolves https://github.com/astral-sh/uv/issues/16496

This PR updates the resolver so `build-system` dependency failures
surface prerelease hints even when prerelease selection is fixed. When a
build dependency only has prerelease candidates, or the requested
version explicitly includes a prerelease marker, we now emit a tailored
hint explaining that build environments can’t auto-enable prereleases
and describing how to opt in.

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
This commit is contained in:
liam 2025-11-02 13:38:09 -05:00 committed by GitHub
parent 45c1907ede
commit 857827da14
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 130 additions and 20 deletions

View File

@ -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<Version>,
selector: &CandidateSelector,
env: &ResolverEnvironment,
options: &Options,
hints: &mut IndexSet<PubGrubHint>,
) {
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<Version>,
},
/// 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<Version>,
},
/// 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<PubGrubHint> 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,

View File

@ -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]