Skip to content

Commit 4c05e8a

Browse files
committed
Hint at tool.uv.environments on resolution error
Users are not (yet) properly familiar with the concept of universal resolution and its implication that we need to resolve for all possible platforms and Python versions. Some projects only target a specific platform or Python version and experience resolution errors due to failures for other platforms. Indicated by the number of questions we get about it, `too.uv.environments` for restricting environments is not well discoverable. We add a special hint when resolution failed on a fork disjoint with the current environment, hinting the user to constraint `requires-python` and `tool.uv.environments` respectively. The hint has false positives for cases where the resolution failed on a different platform, but equally fails on the current platform, but the other fork was earlier. Given that conflicts can be based on `requires-python`, afaik be can't parse whether the current platform would also be affected from the derivation tree. Two cases not covered by this are build errors as well as install error that need `tool.uv.required-environments`.
1 parent 395039a commit 4c05e8a

File tree

14 files changed

+223
-8
lines changed

14 files changed

+223
-8
lines changed

crates/uv-bench/benches/uv.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ mod resolver {
206206
options,
207207
&python_requirement,
208208
markers,
209+
interpreter.markers(),
209210
conflicts,
210211
Some(&TAGS),
211212
&flat_index,

crates/uv-dispatch/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ impl BuildContext for BuildDispatch<'_> {
226226
.build(),
227227
&python_requirement,
228228
ResolverEnvironment::specific(marker_env),
229+
self.interpreter.markers(),
229230
// Conflicting groups only make sense when doing universal resolution.
230231
Conflicts::empty(),
231232
Some(tags),

crates/uv-resolver/src/error.rs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::fmt::Formatter;
33
use std::sync::Arc;
44

55
use indexmap::IndexSet;
6+
use owo_colors::OwoColorize;
67
use pubgrub::{
78
DefaultStringReporter, DerivationTree, Derived, External, Range, Ranges, Reporter, Term,
89
};
@@ -13,7 +14,8 @@ use uv_distribution_types::{
1314
DerivationChain, DistErrorKind, IndexCapabilities, IndexLocations, IndexUrl, RequestedDist,
1415
};
1516
use uv_normalize::{ExtraName, InvalidNameError, PackageName};
16-
use uv_pep440::{LocalVersionSlice, LowerBound, Version};
17+
use uv_pep440::{LocalVersionSlice, LowerBound, Version, VersionSpecifier};
18+
use uv_pep508::{MarkerEnvironment, MarkerExpression, MarkerTree, MarkerValueVersion};
1719
use uv_platform_tags::Tags;
1820
use uv_static::EnvVars;
1921

@@ -157,6 +159,7 @@ pub struct NoSolutionError {
157159
fork_urls: ForkUrls,
158160
fork_indexes: ForkIndexes,
159161
env: ResolverEnvironment,
162+
current_environment: MarkerEnvironment,
160163
tags: Option<Tags>,
161164
workspace_members: BTreeSet<PackageName>,
162165
options: Options,
@@ -178,6 +181,7 @@ impl NoSolutionError {
178181
fork_urls: ForkUrls,
179182
fork_indexes: ForkIndexes,
180183
env: ResolverEnvironment,
184+
current_environment: MarkerEnvironment,
181185
tags: Option<Tags>,
182186
workspace_members: BTreeSet<PackageName>,
183187
options: Options,
@@ -196,6 +200,7 @@ impl NoSolutionError {
196200
fork_urls,
197201
fork_indexes,
198202
env,
203+
current_environment,
199204
tags,
200205
workspace_members,
201206
options,
@@ -347,6 +352,42 @@ impl NoSolutionError {
347352
pub fn header(&self) -> NoSolutionHeader {
348353
NoSolutionHeader::new(self.env.clone())
349354
}
355+
356+
/// Hint at limiting the resolver environment if universal resolution failed for a target
357+
/// that is not the current platform or Python environment.
358+
fn hint_disjoint_targets(&self, f: &mut Formatter) -> std::fmt::Result {
359+
// Only applicable to universal resolution.
360+
let Some(markers) = self.env.fork_markers() else {
361+
return Ok(());
362+
};
363+
364+
// TODO(konsti): This is a crude approximation to telling the user the difference
365+
// between their Python version and the relevant Python version range from the marker.
366+
let current_python_version = MarkerTree::expression(MarkerExpression::Version {
367+
key: MarkerValueVersion::PythonVersion,
368+
specifier: VersionSpecifier::equals_version(
369+
self.current_environment.python_version().version.clone(),
370+
),
371+
});
372+
if markers.is_disjoint(current_python_version) {
373+
write!(
374+
f,
375+
"\n\n{}{} The resolution failed for a Python version range different than the current Python version, \
376+
consider limiting the Python version range using `requires-python`.",
377+
"hint".bold().cyan(),
378+
":".bold(),
379+
)?;
380+
} else if !markers.evaluate(&self.current_environment, &[]) {
381+
write!(
382+
f,
383+
"\n\n{}{} The resolution failed for an environment that is not the current one, \
384+
consider limiting the environments with `tool.uv.environments`.",
385+
"hint".bold().cyan(),
386+
":".bold(),
387+
)?;
388+
}
389+
Ok(())
390+
}
350391
}
351392

352393
impl std::fmt::Debug for NoSolutionError {
@@ -366,6 +407,7 @@ impl std::fmt::Debug for NoSolutionError {
366407
fork_urls,
367408
fork_indexes,
368409
env,
410+
current_environment,
369411
tags,
370412
workspace_members,
371413
options,
@@ -383,6 +425,7 @@ impl std::fmt::Debug for NoSolutionError {
383425
.field("fork_urls", fork_urls)
384426
.field("fork_indexes", fork_indexes)
385427
.field("env", env)
428+
.field("current_environment", current_environment)
386429
.field("tags", tags)
387430
.field("workspace_members", workspace_members)
388431
.field("options", options)
@@ -446,6 +489,7 @@ impl std::fmt::Display for NoSolutionError {
446489

447490
// Include any additional hints.
448491
let mut additional_hints = IndexSet::default();
492+
449493
formatter.generate_hints(
450494
&tree,
451495
&self.index,
@@ -467,6 +511,8 @@ impl std::fmt::Display for NoSolutionError {
467511
write!(f, "\n\n{hint}")?;
468512
}
469513

514+
self.hint_disjoint_targets(f)?;
515+
470516
Ok(())
471517
}
472518
}

crates/uv-resolver/src/resolver/environment.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,14 @@ impl ResolverEnvironment {
198198
crate::marker::requires_python(pep508_marker)
199199
}
200200

201+
/// For a universal resolution, return the markers of the current fork.
202+
pub(crate) fn fork_markers(&self) -> Option<MarkerTree> {
203+
match self.kind {
204+
Kind::Specific { .. } => None,
205+
Kind::Universal { markers, .. } => Some(markers),
206+
}
207+
}
208+
201209
/// Narrow this environment given the forking markers.
202210
///
203211
/// This effectively intersects any markers in this environment with the

crates/uv-resolver/src/resolver/mod.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ use uv_distribution_types::{
3131
use uv_git::GitResolver;
3232
use uv_normalize::{ExtraName, GroupName, PackageName};
3333
use uv_pep440::{release_specifiers_to_ranges, Version, VersionSpecifiers, MIN_VERSION};
34-
use uv_pep508::{MarkerExpression, MarkerOperator, MarkerTree, MarkerValueString};
34+
use uv_pep508::{
35+
MarkerEnvironment, MarkerExpression, MarkerOperator, MarkerTree, MarkerValueString,
36+
};
3537
use uv_platform_tags::Tags;
3638
use uv_pypi_types::{ConflictItem, ConflictItemRef, Conflicts, VerbatimParsedUrl};
3739
use uv_types::{BuildContext, HashStrategy, InstalledPackagesProvider};
@@ -115,6 +117,8 @@ struct ResolverState<InstalledPackages: InstalledPackagesProvider> {
115117
dependency_mode: DependencyMode,
116118
hasher: HashStrategy,
117119
env: ResolverEnvironment,
120+
// The environment of the current Python interpreter.
121+
current_environment: MarkerEnvironment,
118122
tags: Option<Tags>,
119123
python_requirement: PythonRequirement,
120124
conflicts: Conflicts,
@@ -158,6 +162,7 @@ impl<'a, Context: BuildContext, InstalledPackages: InstalledPackagesProvider>
158162
options: Options,
159163
python_requirement: &'a PythonRequirement,
160164
env: ResolverEnvironment,
165+
current_environment: &MarkerEnvironment,
161166
conflicts: Conflicts,
162167
tags: Option<&'a Tags>,
163168
flat_index: &'a FlatIndex,
@@ -184,6 +189,7 @@ impl<'a, Context: BuildContext, InstalledPackages: InstalledPackagesProvider>
184189
options,
185190
hasher,
186191
env,
192+
current_environment,
187193
tags.cloned(),
188194
python_requirement,
189195
conflicts,
@@ -206,6 +212,7 @@ impl<Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvider>
206212
options: Options,
207213
hasher: &HashStrategy,
208214
env: ResolverEnvironment,
215+
current_environment: &MarkerEnvironment,
209216
tags: Option<Tags>,
210217
python_requirement: &PythonRequirement,
211218
conflicts: Conflicts,
@@ -234,6 +241,7 @@ impl<Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvider>
234241
hasher: hasher.clone(),
235242
locations: locations.clone(),
236243
env,
244+
current_environment: current_environment.clone(),
237245
tags,
238246
python_requirement: python_requirement.clone(),
239247
conflicts,
@@ -354,6 +362,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
354362
state.fork_urls,
355363
state.fork_indexes,
356364
state.env,
365+
self.current_environment.clone(),
357366
&visited,
358367
));
359368
}
@@ -2502,6 +2511,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
25022511
fork_urls: ForkUrls,
25032512
fork_indexes: ForkIndexes,
25042513
env: ResolverEnvironment,
2514+
current_environment: MarkerEnvironment,
25052515
visited: &FxHashSet<PackageName>,
25062516
) -> ResolveError {
25072517
err = NoSolutionError::collapse_local_version_segments(NoSolutionError::collapse_proxies(
@@ -2587,6 +2597,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
25872597
fork_urls,
25882598
fork_indexes,
25892599
env,
2600+
current_environment,
25902601
self.tags.clone(),
25912602
self.workspace_members.clone(),
25922603
self.options.clone(),

crates/uv/src/commands/pip/compile.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,7 @@ pub(crate) async fn pip_compile(
517517
tags.as_deref(),
518518
resolver_env.clone(),
519519
python_requirement,
520+
interpreter.markers(),
520521
Conflicts::empty(),
521522
&client,
522523
&flat_index,

crates/uv/src/commands/pip/install.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,7 @@ pub(crate) async fn pip_install(
478478
Some(&tags),
479479
ResolverEnvironment::specific(marker_env.clone()),
480480
python_requirement,
481+
interpreter.markers(),
481482
Conflicts::empty(),
482483
&client,
483484
&flat_index,

crates/uv/src/commands/pip/operations.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use uv_fs::Simplified;
2929
use uv_install_wheel::LinkMode;
3030
use uv_installer::{Plan, Planner, Preparer, SitePackages};
3131
use uv_normalize::{GroupName, PackageName};
32+
use uv_pep508::MarkerEnvironment;
3233
use uv_platform_tags::Tags;
3334
use uv_pypi_types::{Conflicts, ResolverMarkerEnvironment};
3435
use uv_python::{PythonEnvironment, PythonInstallation};
@@ -119,6 +120,7 @@ pub(crate) async fn resolve<InstalledPackages: InstalledPackagesProvider>(
119120
tags: Option<&Tags>,
120121
resolver_env: ResolverEnvironment,
121122
python_requirement: PythonRequirement,
123+
current_environment: &MarkerEnvironment,
122124
conflicts: Conflicts,
123125
client: &RegistryClient,
124126
flat_index: &FlatIndex,
@@ -303,6 +305,7 @@ pub(crate) async fn resolve<InstalledPackages: InstalledPackagesProvider>(
303305
options,
304306
&python_requirement,
305307
resolver_env,
308+
current_environment,
306309
conflicts,
307310
tags,
308311
flat_index,

crates/uv/src/commands/pip/sync.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,7 @@ pub(crate) async fn pip_sync(
411411
Some(&tags),
412412
ResolverEnvironment::specific(marker_env.clone()),
413413
python_requirement,
414+
interpreter.markers(),
414415
Conflicts::empty(),
415416
&client,
416417
&flat_index,

crates/uv/src/commands/project/lock.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,7 @@ async fn do_lock(
817817
None,
818818
resolver_env,
819819
python_requirement,
820+
interpreter.markers(),
820821
conflicts.clone(),
821822
&client,
822823
&flat_index,

0 commit comments

Comments
 (0)