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(
package.name.clone(),
)),
hint: package.tag_hint(tags),
hint: package.tag_hint(tags, markers),
}),
(false, false) => Err(PylockTomlError {
kind: Box::new(PylockTomlErrorKind::NeitherSourceDistNorWheel(
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.
fn tag_hint(&self, tags: &Tags) -> Option<WheelTagHint> {
fn tag_hint(&self, tags: &Tags, markers: &MarkerEnvironment) -> Option<WheelTagHint> {
let filenames = self
.wheels
.iter()
@ -1287,7 +1287,7 @@ impl PylockTomlPackage {
.filter_map(|wheel| wheel.filename(&self.name).ok())
.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.

View File

@ -107,9 +107,9 @@ pub trait Installable<'lock> {
// Add the workspace package to the graph.
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 {
self.non_installable_node(dist, tags)?
self.non_installable_node(dist, tags, marker_env)?
});
inverse.insert(&dist.id, index);
@ -162,6 +162,7 @@ pub trait Installable<'lock> {
tags,
build_options,
install_options,
marker_env,
)?);
entry.insert(index);
index
@ -178,6 +179,7 @@ pub trait Installable<'lock> {
tags,
build_options,
install_options,
marker_env,
)?;
}
index
@ -226,9 +228,9 @@ pub trait Installable<'lock> {
// Add the package to the graph.
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 {
self.non_installable_node(dist, tags)?
self.non_installable_node(dist, tags, marker_env)?
});
inverse.insert(&dist.id, index);
@ -284,6 +286,7 @@ pub trait Installable<'lock> {
tags,
build_options,
install_options,
marker_env,
)?);
entry.insert(index);
index
@ -295,7 +298,13 @@ pub trait Installable<'lock> {
let index = *entry.get();
let node = &mut petgraph[index];
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
}
@ -475,6 +484,7 @@ pub trait Installable<'lock> {
tags,
build_options,
install_options,
marker_env,
)?);
entry.insert(index);
index
@ -514,12 +524,14 @@ pub trait Installable<'lock> {
&self,
package: &Package,
tags: &Tags,
marker_env: &ResolverMarkerEnvironment,
build_options: &BuildOptions,
) -> Result<Node, LockError> {
let dist = package.to_dist(
self.install_path(),
TagPolicy::Required(tags),
build_options,
marker_env,
)?;
let version = package.version().cloned();
let dist = ResolvedDist::Installable {
@ -535,11 +547,17 @@ pub trait Installable<'lock> {
}
/// 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(
self.install_path(),
TagPolicy::Preferred(tags),
&BuildOptions::default(),
marker_env,
)?;
let version = package.version().cloned();
let dist = ResolvedDist::Installable {
@ -561,15 +579,16 @@ pub trait Installable<'lock> {
tags: &Tags,
build_options: &BuildOptions,
install_options: &InstallOptions,
marker_env: &ResolverMarkerEnvironment,
) -> Result<Node, LockError> {
if install_options.include_package(
package.as_install_target(),
self.project_name(),
self.lock().members(),
) {
self.installable_node(package, tags, build_options)
self.installable_node(package, tags, marker_env, build_options)
} 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,
indexes: Option<&IndexLocations>,
tags: &Tags,
markers: &MarkerEnvironment,
hasher: &HashStrategy,
index: &InMemoryIndex,
database: &DistributionDatabase<'_, Context>,
@ -1724,8 +1725,12 @@ impl Lock {
if let Some(version) = package.id.version.as_ref() {
// For a non-dynamic package, fetch the metadata from the distribution database.
let dist =
package.to_dist(root, TagPolicy::Preferred(tags), &BuildOptions::default())?;
let dist = package.to_dist(
root,
TagPolicy::Preferred(tags),
&BuildOptions::default(),
markers,
)?;
let metadata = {
let id = dist.version_id();
@ -1875,6 +1880,7 @@ impl Lock {
root,
TagPolicy::Preferred(tags),
&BuildOptions::default(),
markers,
)?;
let metadata = {
@ -2511,6 +2517,7 @@ impl Package {
workspace_root: &Path,
tag_policy: TagPolicy<'_>,
build_options: &BuildOptions,
markers: &MarkerEnvironment,
) -> Result<Dist, LockError> {
let no_binary = build_options.no_binary_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 {
id: self.id.clone(),
}),
hint: self.tag_hint(tag_policy),
hint: self.tag_hint(tag_policy, markers),
}),
(false, false) => Err(LockError {
kind: Box::new(LockErrorKind::NeitherSourceDistNorWheel {
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.
fn tag_hint(&self, tag_policy: TagPolicy<'_>) -> Option<WheelTagHint> {
fn tag_hint(
&self,
tag_policy: TagPolicy<'_>,
markers: &MarkerEnvironment,
) -> Option<WheelTagHint> {
let filenames = self
.wheels
.iter()
@ -2636,6 +2647,7 @@ impl Package {
self.id.version.as_ref(),
&filenames,
tag_policy.tags(),
markers,
)
}
@ -5260,6 +5272,7 @@ enum WheelTagHint {
version: Option<Version>,
tags: BTreeSet<PlatformTag>,
best: Option<PlatformTag>,
markers: MarkerEnvironment,
},
}
@ -5270,6 +5283,7 @@ impl WheelTagHint {
version: Option<&Version>,
filenames: &[&WheelFilename],
tags: &Tags,
markers: &MarkerEnvironment,
) -> Option<Self> {
let incompatibility = filenames
.iter()
@ -5322,17 +5336,18 @@ impl WheelTagHint {
}
TagCompatibility::Incompatible(IncompatibleTag::Platform) => {
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()
.collect::<BTreeSet<_>>();
if tags.is_empty() {
if incompatible_tags.is_empty() {
None
} else {
Some(Self::PlatformTags {
package: name.clone(),
version: version.cloned(),
tags,
tags: incompatible_tags,
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 {
@ -5517,9 +5544,11 @@ impl std::fmt::Display for WheelTagHint {
version,
tags,
best,
markers,
} => {
let s = if tags.len() == 1 { "" } else { "s" };
if let Some(best) = best {
let example_marker = Self::suggest_environment_marker(markers);
let best = if let Some(pretty) = best.pretty() {
format!("{} (`{}`)", pretty.cyan(), best.cyan())
} else {
@ -5530,9 +5559,9 @@ impl std::fmt::Display for WheelTagHint {
} else {
format!("`{}`", package.cyan())
};
writeln!(
write!(
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(),
":".bold(),
best,
@ -5540,6 +5569,7 @@ impl std::fmt::Display for WheelTagHint {
tags.iter()
.map(|tag| format!("`{}`", tag.cyan()))
.join(", "),
format!("\"{example_marker}\"").cyan(),
"tool.uv.required-environments".green()
)
} else {

View File

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

View File

@ -12077,8 +12077,12 @@ fn sync_required_environment_hint() -> Result<()> {
r"You're on [^ ]+ \(`.*`\)",
"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
exit_code: 2
----- stdout -----
@ -12087,8 +12091,8 @@ fn sync_required_environment_hint() -> Result<()> {
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
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(())
}