Add required environment marker example to hint (#16244)

## Summary
fixes issue #15938 
- show platform wheel hint with a concrete
`tool.uv.required-environments` example so users know how to configure
compatibility
- add `WheelTagHint::suggest_environment_marker` to pick a sensible
environment marker based on the available wheel tags
- update the `sync_required_environment_hint` integration snapshot to
expect the new multi-line hint

## Test Plan

cargo test --package uv --test it --
sync::sync_required_environment_hint
This commit is contained in:
Parham MohammadAlizadeh 2025-10-20 12:08:10 +01:00 committed by GitHub
parent 2b0407e277
commit ed3f99a119
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 79 additions and 25 deletions

View File

@ -1144,13 +1144,13 @@ impl<'lock> PylockToml {
kind: Box::new(PylockTomlErrorKind::IncompatibleWheelOnly( kind: Box::new(PylockTomlErrorKind::IncompatibleWheelOnly(
package.name.clone(), package.name.clone(),
)), )),
hint: package.tag_hint(tags), hint: package.tag_hint(tags, markers),
}), }),
(false, false) => Err(PylockTomlError { (false, false) => Err(PylockTomlError {
kind: Box::new(PylockTomlErrorKind::NeitherSourceDistNorWheel( kind: Box::new(PylockTomlErrorKind::NeitherSourceDistNorWheel(
package.name.clone(), package.name.clone(),
)), )),
hint: package.tag_hint(tags), hint: package.tag_hint(tags, markers),
}), }),
}; };
}; };
@ -1279,7 +1279,7 @@ impl PylockTomlPackage {
} }
/// Generate a [`WheelTagHint`] based on wheel-tag incompatibilities. /// Generate a [`WheelTagHint`] based on wheel-tag incompatibilities.
fn tag_hint(&self, tags: &Tags) -> Option<WheelTagHint> { fn tag_hint(&self, tags: &Tags, markers: &MarkerEnvironment) -> Option<WheelTagHint> {
let filenames = self let filenames = self
.wheels .wheels
.iter() .iter()
@ -1287,7 +1287,7 @@ impl PylockTomlPackage {
.filter_map(|wheel| wheel.filename(&self.name).ok()) .filter_map(|wheel| wheel.filename(&self.name).ok())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let filenames = filenames.iter().map(Cow::as_ref).collect::<Vec<_>>(); let filenames = filenames.iter().map(Cow::as_ref).collect::<Vec<_>>();
WheelTagHint::from_wheels(&self.name, self.version.as_ref(), &filenames, tags) WheelTagHint::from_wheels(&self.name, self.version.as_ref(), &filenames, tags, markers)
} }
/// Returns the [`ResolvedRepositoryReference`] for the package, if it is a Git source. /// Returns the [`ResolvedRepositoryReference`] for the package, if it is a Git source.

View File

@ -107,9 +107,9 @@ pub trait Installable<'lock> {
// Add the workspace package to the graph. // Add the workspace package to the graph.
let index = petgraph.add_node(if groups.prod() { let index = petgraph.add_node(if groups.prod() {
self.package_to_node(dist, tags, build_options, install_options)? self.package_to_node(dist, tags, build_options, install_options, marker_env)?
} else { } else {
self.non_installable_node(dist, tags)? self.non_installable_node(dist, tags, marker_env)?
}); });
inverse.insert(&dist.id, index); inverse.insert(&dist.id, index);
@ -162,6 +162,7 @@ pub trait Installable<'lock> {
tags, tags,
build_options, build_options,
install_options, install_options,
marker_env,
)?); )?);
entry.insert(index); entry.insert(index);
index index
@ -178,6 +179,7 @@ pub trait Installable<'lock> {
tags, tags,
build_options, build_options,
install_options, install_options,
marker_env,
)?; )?;
} }
index index
@ -226,9 +228,9 @@ pub trait Installable<'lock> {
// Add the package to the graph. // Add the package to the graph.
let index = petgraph.add_node(if groups.prod() { let index = petgraph.add_node(if groups.prod() {
self.package_to_node(dist, tags, build_options, install_options)? self.package_to_node(dist, tags, build_options, install_options, marker_env)?
} else { } else {
self.non_installable_node(dist, tags)? self.non_installable_node(dist, tags, marker_env)?
}); });
inverse.insert(&dist.id, index); inverse.insert(&dist.id, index);
@ -284,6 +286,7 @@ pub trait Installable<'lock> {
tags, tags,
build_options, build_options,
install_options, install_options,
marker_env,
)?); )?);
entry.insert(index); entry.insert(index);
index index
@ -295,7 +298,13 @@ pub trait Installable<'lock> {
let index = *entry.get(); let index = *entry.get();
let node = &mut petgraph[index]; let node = &mut petgraph[index];
if !groups.prod() { if !groups.prod() {
*node = self.package_to_node(dist, tags, build_options, install_options)?; *node = self.package_to_node(
dist,
tags,
build_options,
install_options,
marker_env,
)?;
} }
index index
} }
@ -475,6 +484,7 @@ pub trait Installable<'lock> {
tags, tags,
build_options, build_options,
install_options, install_options,
marker_env,
)?); )?);
entry.insert(index); entry.insert(index);
index index
@ -514,12 +524,14 @@ pub trait Installable<'lock> {
&self, &self,
package: &Package, package: &Package,
tags: &Tags, tags: &Tags,
marker_env: &ResolverMarkerEnvironment,
build_options: &BuildOptions, build_options: &BuildOptions,
) -> Result<Node, LockError> { ) -> Result<Node, LockError> {
let dist = package.to_dist( let dist = package.to_dist(
self.install_path(), self.install_path(),
TagPolicy::Required(tags), TagPolicy::Required(tags),
build_options, build_options,
marker_env,
)?; )?;
let version = package.version().cloned(); let version = package.version().cloned();
let dist = ResolvedDist::Installable { let dist = ResolvedDist::Installable {
@ -535,11 +547,17 @@ pub trait Installable<'lock> {
} }
/// Create a non-installable [`Node`] from a [`Package`]. /// Create a non-installable [`Node`] from a [`Package`].
fn non_installable_node(&self, package: &Package, tags: &Tags) -> Result<Node, LockError> { fn non_installable_node(
&self,
package: &Package,
tags: &Tags,
marker_env: &ResolverMarkerEnvironment,
) -> Result<Node, LockError> {
let dist = package.to_dist( let dist = package.to_dist(
self.install_path(), self.install_path(),
TagPolicy::Preferred(tags), TagPolicy::Preferred(tags),
&BuildOptions::default(), &BuildOptions::default(),
marker_env,
)?; )?;
let version = package.version().cloned(); let version = package.version().cloned();
let dist = ResolvedDist::Installable { let dist = ResolvedDist::Installable {
@ -561,15 +579,16 @@ pub trait Installable<'lock> {
tags: &Tags, tags: &Tags,
build_options: &BuildOptions, build_options: &BuildOptions,
install_options: &InstallOptions, install_options: &InstallOptions,
marker_env: &ResolverMarkerEnvironment,
) -> Result<Node, LockError> { ) -> Result<Node, LockError> {
if install_options.include_package( if install_options.include_package(
package.as_install_target(), package.as_install_target(),
self.project_name(), self.project_name(),
self.lock().members(), self.lock().members(),
) { ) {
self.installable_node(package, tags, build_options) self.installable_node(package, tags, marker_env, build_options)
} else { } else {
self.non_installable_node(package, tags) self.non_installable_node(package, tags, marker_env)
} }
} }
} }

View File

@ -1452,6 +1452,7 @@ impl Lock {
dependency_metadata: &DependencyMetadata, dependency_metadata: &DependencyMetadata,
indexes: Option<&IndexLocations>, indexes: Option<&IndexLocations>,
tags: &Tags, tags: &Tags,
markers: &MarkerEnvironment,
hasher: &HashStrategy, hasher: &HashStrategy,
index: &InMemoryIndex, index: &InMemoryIndex,
database: &DistributionDatabase<'_, Context>, database: &DistributionDatabase<'_, Context>,
@ -1724,8 +1725,12 @@ impl Lock {
if let Some(version) = package.id.version.as_ref() { if let Some(version) = package.id.version.as_ref() {
// For a non-dynamic package, fetch the metadata from the distribution database. // For a non-dynamic package, fetch the metadata from the distribution database.
let dist = let dist = package.to_dist(
package.to_dist(root, TagPolicy::Preferred(tags), &BuildOptions::default())?; root,
TagPolicy::Preferred(tags),
&BuildOptions::default(),
markers,
)?;
let metadata = { let metadata = {
let id = dist.version_id(); let id = dist.version_id();
@ -1875,6 +1880,7 @@ impl Lock {
root, root,
TagPolicy::Preferred(tags), TagPolicy::Preferred(tags),
&BuildOptions::default(), &BuildOptions::default(),
markers,
)?; )?;
let metadata = { let metadata = {
@ -2511,6 +2517,7 @@ impl Package {
workspace_root: &Path, workspace_root: &Path,
tag_policy: TagPolicy<'_>, tag_policy: TagPolicy<'_>,
build_options: &BuildOptions, build_options: &BuildOptions,
markers: &MarkerEnvironment,
) -> Result<Dist, LockError> { ) -> Result<Dist, LockError> {
let no_binary = build_options.no_binary_package(&self.id.name); let no_binary = build_options.no_binary_package(&self.id.name);
let no_build = build_options.no_build_package(&self.id.name); let no_build = build_options.no_build_package(&self.id.name);
@ -2613,19 +2620,23 @@ impl Package {
kind: Box::new(LockErrorKind::IncompatibleWheelOnly { kind: Box::new(LockErrorKind::IncompatibleWheelOnly {
id: self.id.clone(), id: self.id.clone(),
}), }),
hint: self.tag_hint(tag_policy), hint: self.tag_hint(tag_policy, markers),
}), }),
(false, false) => Err(LockError { (false, false) => Err(LockError {
kind: Box::new(LockErrorKind::NeitherSourceDistNorWheel { kind: Box::new(LockErrorKind::NeitherSourceDistNorWheel {
id: self.id.clone(), id: self.id.clone(),
}), }),
hint: self.tag_hint(tag_policy), hint: self.tag_hint(tag_policy, markers),
}), }),
} }
} }
/// Generate a [`WheelTagHint`] based on wheel-tag incompatibilities. /// Generate a [`WheelTagHint`] based on wheel-tag incompatibilities.
fn tag_hint(&self, tag_policy: TagPolicy<'_>) -> Option<WheelTagHint> { fn tag_hint(
&self,
tag_policy: TagPolicy<'_>,
markers: &MarkerEnvironment,
) -> Option<WheelTagHint> {
let filenames = self let filenames = self
.wheels .wheels
.iter() .iter()
@ -2636,6 +2647,7 @@ impl Package {
self.id.version.as_ref(), self.id.version.as_ref(),
&filenames, &filenames,
tag_policy.tags(), tag_policy.tags(),
markers,
) )
} }
@ -5260,6 +5272,7 @@ enum WheelTagHint {
version: Option<Version>, version: Option<Version>,
tags: BTreeSet<PlatformTag>, tags: BTreeSet<PlatformTag>,
best: Option<PlatformTag>, best: Option<PlatformTag>,
markers: MarkerEnvironment,
}, },
} }
@ -5270,6 +5283,7 @@ impl WheelTagHint {
version: Option<&Version>, version: Option<&Version>,
filenames: &[&WheelFilename], filenames: &[&WheelFilename],
tags: &Tags, tags: &Tags,
markers: &MarkerEnvironment,
) -> Option<Self> { ) -> Option<Self> {
let incompatibility = filenames let incompatibility = filenames
.iter() .iter()
@ -5322,17 +5336,18 @@ impl WheelTagHint {
} }
TagCompatibility::Incompatible(IncompatibleTag::Platform) => { TagCompatibility::Incompatible(IncompatibleTag::Platform) => {
let best = tags.platform_tag().cloned(); let best = tags.platform_tag().cloned();
let tags = Self::platform_tags(filenames.iter().copied(), tags) let incompatible_tags = Self::platform_tags(filenames.iter().copied(), tags)
.cloned() .cloned()
.collect::<BTreeSet<_>>(); .collect::<BTreeSet<_>>();
if tags.is_empty() { if incompatible_tags.is_empty() {
None None
} else { } else {
Some(Self::PlatformTags { Some(Self::PlatformTags {
package: name.clone(), package: name.clone(),
version: version.cloned(), version: version.cloned(),
tags, tags: incompatible_tags,
best, best,
markers: markers.clone(),
}) })
} }
} }
@ -5373,6 +5388,18 @@ impl WheelTagHint {
} }
}) })
} }
fn suggest_environment_marker(markers: &MarkerEnvironment) -> String {
let sys_platform = markers.sys_platform();
let platform_machine = markers.platform_machine();
// Generate the marker string based on actual environment values
if platform_machine.is_empty() {
format!("sys_platform == '{sys_platform}'")
} else {
format!("sys_platform == '{sys_platform}' and platform_machine == '{platform_machine}'")
}
}
} }
impl std::fmt::Display for WheelTagHint { impl std::fmt::Display for WheelTagHint {
@ -5517,9 +5544,11 @@ impl std::fmt::Display for WheelTagHint {
version, version,
tags, tags,
best, best,
markers,
} => { } => {
let s = if tags.len() == 1 { "" } else { "s" }; let s = if tags.len() == 1 { "" } else { "s" };
if let Some(best) = best { if let Some(best) = best {
let example_marker = Self::suggest_environment_marker(markers);
let best = if let Some(pretty) = best.pretty() { let best = if let Some(pretty) = best.pretty() {
format!("{} (`{}`)", pretty.cyan(), best.cyan()) format!("{} (`{}`)", pretty.cyan(), best.cyan())
} else { } else {
@ -5530,9 +5559,9 @@ impl std::fmt::Display for WheelTagHint {
} else { } else {
format!("`{}`", package.cyan()) format!("`{}`", package.cyan())
}; };
writeln!( write!(
f, f,
"{}{} You're on {}, but {} only has wheels for the following platform{s}: {}; consider adding your platform to `{}` to ensure uv resolves to a version with compatible wheels", "{}{} You're on {}, but {} only has wheels for the following platform{s}: {}; consider adding {} to `{}` to ensure uv resolves to a version with compatible wheels",
"hint".bold().cyan(), "hint".bold().cyan(),
":".bold(), ":".bold(),
best, best,
@ -5540,6 +5569,7 @@ impl std::fmt::Display for WheelTagHint {
tags.iter() tags.iter()
.map(|tag| format!("`{}`", tag.cyan())) .map(|tag| format!("`{}`", tag.cyan()))
.join(", "), .join(", "),
format!("\"{example_marker}\"").cyan(),
"tool.uv.required-environments".green() "tool.uv.required-environments".green()
) )
} else { } else {

View File

@ -1206,6 +1206,7 @@ impl ValidatedLock {
dependency_metadata, dependency_metadata,
indexes, indexes,
interpreter.tags()?, interpreter.tags()?,
interpreter.markers(),
hasher, hasher,
index, index,
database, database,

View File

@ -12077,8 +12077,12 @@ fn sync_required_environment_hint() -> Result<()> {
r"You're on [^ ]+ \(`.*`\)", r"You're on [^ ]+ \(`.*`\)",
"You're on [PLATFORM] (`[TAG]`)", "You're on [PLATFORM] (`[TAG]`)",
)); ));
filters.push((
r"sys_platform == '[^']+' and platform_machine == '[^']+'",
"sys_platform == '[PLATFORM]' and platform_machine == '[MACHINE]'",
));
uv_snapshot!(filters, context.sync().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r" uv_snapshot!(filters, context.sync().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r#"
success: false success: false
exit_code: 2 exit_code: 2
----- stdout ----- ----- stdout -----
@ -12087,8 +12091,8 @@ fn sync_required_environment_hint() -> Result<()> {
Resolved 2 packages in [TIME] Resolved 2 packages in [TIME]
error: Distribution `no-sdist-no-wheels-with-matching-platform-a==1.0.0 @ registry+https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/` can't be installed because it doesn't have a source distribution or wheel for the current platform error: Distribution `no-sdist-no-wheels-with-matching-platform-a==1.0.0 @ registry+https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/` can't be installed because it doesn't have a source distribution or wheel for the current platform
hint: You're on [PLATFORM] (`[TAG]`), but `no-sdist-no-wheels-with-matching-platform-a` (v1.0.0) only has wheels for the following platform: `macosx_10_0_ppc64`; consider adding your platform to `tool.uv.required-environments` to ensure uv resolves to a version with compatible wheels hint: You're on [PLATFORM] (`[TAG]`), but `no-sdist-no-wheels-with-matching-platform-a` (v1.0.0) only has wheels for the following platform: `macosx_10_0_ppc64`; consider adding "sys_platform == '[PLATFORM]' and platform_machine == '[MACHINE]'" to `tool.uv.required-environments` to ensure uv resolves to a version with compatible wheels
"); "#);
Ok(()) Ok(())
} }