mirror of https://github.com/astral-sh/uv
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:
parent
45c1907ede
commit
857827da14
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Reference in New Issue