Handle universal vs. fork markers with ResolverMarkers (#5099)

* Use a dedicated `ResolverMarkers` check in the fork state. This is
better than the `MarkerTree::And(Vec::new())` check.
* Report the timing correct naming universal resolution instead of two
spaces around an empty string when there are no markers.
* On resolution error, show the split that we're in. I'm not sure how to
word this, since we're doing a universal resolution until we fork, so
the trace may contain information from requirements that are not part of
this fork.
This commit is contained in:
konsti 2024-07-17 18:59:33 +02:00 committed by GitHub
parent fe403576c5
commit a6dfd3953a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 220 additions and 133 deletions

View file

@ -90,7 +90,7 @@ mod resolver {
use uv_python::PythonEnvironment;
use uv_resolver::{
FlatIndex, InMemoryIndex, Manifest, OptionsBuilder, PythonRequirement, ResolutionGraph,
Resolver,
Resolver, ResolverMarkers,
};
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight};
@ -175,7 +175,7 @@ mod resolver {
manifest,
options,
&python_requirement,
Some(&MARKERS),
ResolverMarkers::SpecificEnvironment(MARKERS.clone()),
Some(&TAGS),
&flat_index,
&index,

View file

@ -26,6 +26,7 @@ use uv_installer::{Installer, Plan, Planner, Preparer, SitePackages};
use uv_python::{Interpreter, PythonEnvironment};
use uv_resolver::{
ExcludeNewer, FlatIndex, InMemoryIndex, Manifest, OptionsBuilder, PythonRequirement, Resolver,
ResolverMarkers,
};
use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight};
@ -146,7 +147,7 @@ impl<'a> BuildContext for BuildDispatch<'a> {
.index_strategy(self.index_strategy)
.build(),
&python_requirement,
Some(markers),
ResolverMarkers::SpecificEnvironment(markers.clone()),
Some(tags),
self.flat_index,
self.index,

View file

@ -7,13 +7,12 @@ use thiserror::Error;
use tracing::trace;
use distribution_types::{BuiltDist, Dist, DistributionMetadata, GitSourceDist, SourceDist};
use pep508_rs::MarkerEnvironment;
use pypi_types::{Requirement, RequirementSource};
use uv_configuration::{Constraints, Overrides};
use uv_distribution::{DistributionDatabase, Reporter};
use uv_git::GitUrl;
use uv_normalize::GroupName;
use uv_resolver::{InMemoryIndex, MetadataResponse};
use uv_resolver::{InMemoryIndex, MetadataResponse, ResolverMarkers};
use uv_types::{BuildContext, HashStrategy, RequestedRequirements};
#[derive(Debug, Error)]
@ -98,7 +97,7 @@ impl<'a, Context: BuildContext> LookaheadResolver<'a, Context> {
/// to "only evaluate marker expressions that reference an extra name.")
pub async fn resolve(
self,
markers: Option<&MarkerEnvironment>,
markers: &ResolverMarkers,
) -> Result<Vec<RequestedRequirements>, LookaheadError> {
let mut results = Vec::new();
let mut futures = FuturesUnordered::new();
@ -108,7 +107,7 @@ impl<'a, Context: BuildContext> LookaheadResolver<'a, Context> {
let mut queue: VecDeque<_> = self
.constraints
.apply(self.overrides.apply(self.requirements))
.filter(|requirement| requirement.evaluate_markers(markers, &[]))
.filter(|requirement| requirement.evaluate_markers(markers.marker_environment(), &[]))
.map(|requirement| (*requirement).clone())
.collect();
@ -127,7 +126,9 @@ impl<'a, Context: BuildContext> LookaheadResolver<'a, Context> {
.constraints
.apply(self.overrides.apply(lookahead.requirements()))
{
if requirement.evaluate_markers(markers, lookahead.extras()) {
if requirement
.evaluate_markers(markers.marker_environment(), lookahead.extras())
{
queue.push_back((*requirement).clone());
}
}

View file

@ -18,7 +18,7 @@ use crate::pubgrub::{
PubGrubPackage, PubGrubPackageInner, PubGrubReportFormatter, PubGrubSpecifierError,
};
use crate::python_requirement::PythonRequirement;
use crate::resolver::{IncompletePackage, UnavailablePackage, UnavailableReason};
use crate::resolver::{IncompletePackage, ResolverMarkers, UnavailablePackage, UnavailableReason};
#[derive(Debug, thiserror::Error)]
pub enum ResolveError {
@ -50,10 +50,10 @@ pub enum ResolveError {
ConflictingOverrideUrls(PackageName, String, String),
#[error("Requirements contain conflicting URLs for package `{0}`:\n- {}", _1.join("\n- "))]
ConflictingUrls(PackageName, Vec<String>),
ConflictingUrlsUniversal(PackageName, Vec<String>),
#[error("Requirements contain conflicting URLs for package `{package_name}` in split `{fork_markers}`:\n- {}", urls.join("\n- "))]
ConflictingUrlsInFork {
ConflictingUrlsFork {
package_name: PackageName,
urls: Vec<String>,
fork_markers: MarkerTree,
@ -125,9 +125,21 @@ pub struct NoSolutionError {
unavailable_packages: FxHashMap<PackageName, UnavailablePackage>,
incomplete_packages: FxHashMap<PackageName, BTreeMap<Version, IncompletePackage>>,
fork_urls: ForkUrls,
markers: ResolverMarkers,
}
impl NoSolutionError {
pub fn header(&self) -> String {
match &self.markers {
ResolverMarkers::Universal | ResolverMarkers::SpecificEnvironment(_) => {
"No solution found when resolving dependencies:".to_string()
}
ResolverMarkers::Fork(markers) => {
format!("No solution found when resolving dependencies for split ({markers}):")
}
}
}
pub(crate) fn new(
error: pubgrub::error::NoSolutionError<UvDependencyProvider>,
available_versions: FxHashMap<PubGrubPackage, BTreeSet<Version>>,
@ -137,6 +149,7 @@ impl NoSolutionError {
unavailable_packages: FxHashMap<PackageName, UnavailablePackage>,
incomplete_packages: FxHashMap<PackageName, BTreeMap<Version, IncompletePackage>>,
fork_urls: ForkUrls,
markers: ResolverMarkers,
) -> Self {
Self {
error,
@ -147,6 +160,7 @@ impl NoSolutionError {
unavailable_packages,
incomplete_packages,
fork_urls,
markers,
}
}

View file

@ -3,10 +3,10 @@ use std::collections::hash_map::Entry;
use rustc_hash::FxHashMap;
use distribution_types::Verbatim;
use pep508_rs::MarkerTree;
use pypi_types::VerbatimParsedUrl;
use uv_normalize::PackageName;
use crate::resolver::ResolverMarkers;
use crate::ResolveError;
/// See [`crate::resolver::SolveState`].
@ -29,7 +29,7 @@ impl ForkUrls {
&mut self,
package_name: &PackageName,
url: &VerbatimParsedUrl,
fork_markers: &MarkerTree,
fork_markers: &ResolverMarkers,
) -> Result<(), ResolveError> {
match self.0.entry(package_name.clone()) {
Entry::Occupied(previous) => {
@ -39,17 +39,20 @@ impl ForkUrls {
url.verbatim.verbatim().to_string(),
];
conflicting_url.sort();
return if fork_markers.is_universal() {
Err(ResolveError::ConflictingUrls(
package_name.clone(),
conflicting_url,
))
} else {
Err(ResolveError::ConflictingUrlsInFork {
package_name: package_name.clone(),
urls: conflicting_url,
fork_markers: fork_markers.clone(),
})
return match fork_markers {
ResolverMarkers::Universal | ResolverMarkers::SpecificEnvironment(_) => {
Err(ResolveError::ConflictingUrlsUniversal(
package_name.clone(),
conflicting_url,
))
}
ResolverMarkers::Fork(fork_markers) => {
Err(ResolveError::ConflictingUrlsFork {
package_name: package_name.clone(),
urls: conflicting_url,
fork_markers: fork_markers.clone(),
})
}
};
}
}

View file

@ -15,7 +15,7 @@ pub use resolution::{AnnotationStyle, DisplayResolutionGraph, ResolutionGraph};
pub use resolution_mode::ResolutionMode;
pub use resolver::{
BuildId, DefaultResolverProvider, InMemoryIndex, MetadataResponse, PackageVersionsResult,
Reporter as ResolverReporter, Resolver, ResolverProvider, VersionsResponse,
Reporter as ResolverReporter, Resolver, ResolverMarkers, ResolverProvider, VersionsResponse,
WheelMetadataResult,
};
pub use version_map::VersionMap;

View file

@ -7,12 +7,13 @@ use petgraph::Direction;
use rustc_hash::{FxBuildHasher, FxHashMap};
use distribution_types::{DistributionMetadata, Name, SourceAnnotation, SourceAnnotations};
use pep508_rs::MarkerEnvironment;
use pep508_rs::MarkerTree;
use uv_normalize::PackageName;
use crate::resolution::{RequirementsTxtDist, ResolutionGraphNode};
use crate::{marker, ResolutionGraph};
use crate::{marker, ResolutionGraph, ResolverMarkers};
static UNIVERSAL_MARKERS: ResolverMarkers = ResolverMarkers::Universal;
/// A [`std::fmt::Display`] implementation for the resolution graph.
#[derive(Debug)]
@ -21,7 +22,7 @@ pub struct DisplayResolutionGraph<'a> {
/// The underlying graph.
resolution: &'a ResolutionGraph,
/// The marker environment, used to determine the markers that apply to each package.
marker_env: Option<&'a MarkerEnvironment>,
marker_env: &'a ResolverMarkers,
/// The packages to exclude from the output.
no_emit_packages: &'a [PackageName],
/// Whether to include hashes in the output.
@ -50,7 +51,7 @@ impl<'a> From<&'a ResolutionGraph> for DisplayResolutionGraph<'a> {
fn from(resolution: &'a ResolutionGraph) -> Self {
Self::new(
resolution,
None,
&UNIVERSAL_MARKERS,
&[],
false,
false,
@ -67,7 +68,7 @@ impl<'a> DisplayResolutionGraph<'a> {
#[allow(clippy::fn_params_excessive_bools)]
pub fn new(
underlying: &'a ResolutionGraph,
marker_env: Option<&'a MarkerEnvironment>,
marker_env: &'a ResolverMarkers,
no_emit_packages: &'a [PackageName],
show_hashes: bool,
include_extras: bool,
@ -97,12 +98,9 @@ impl std::fmt::Display for DisplayResolutionGraph<'_> {
let sources = if self.include_annotations {
let mut sources = SourceAnnotations::default();
for requirement in self
.resolution
.requirements
.iter()
.filter(|requirement| requirement.evaluate_markers(self.marker_env, &[]))
{
for requirement in self.resolution.requirements.iter().filter(|requirement| {
requirement.evaluate_markers(self.marker_env.marker_environment(), &[])
}) {
if let Some(origin) = &requirement.origin {
sources.add(
&requirement.name,
@ -115,7 +113,9 @@ impl std::fmt::Display for DisplayResolutionGraph<'_> {
.resolution
.constraints
.requirements()
.filter(|requirement| requirement.evaluate_markers(self.marker_env, &[]))
.filter(|requirement| {
requirement.evaluate_markers(self.marker_env.marker_environment(), &[])
})
{
if let Some(origin) = &requirement.origin {
sources.add(
@ -129,7 +129,9 @@ impl std::fmt::Display for DisplayResolutionGraph<'_> {
.resolution
.overrides
.requirements()
.filter(|requirement| requirement.evaluate_markers(self.marker_env, &[]))
.filter(|requirement| {
requirement.evaluate_markers(self.marker_env.marker_environment(), &[])
})
{
if let Some(origin) = &requirement.origin {
sources.add(

View file

@ -28,9 +28,10 @@ use distribution_types::{
};
pub(crate) use locals::ForkLocals;
use pep440_rs::{Version, MIN_VERSION};
use pep508_rs::{MarkerEnvironment, MarkerTree};
use pep508_rs::MarkerTree;
use platform_tags::Tags;
use pypi_types::{Metadata23, Requirement, VerbatimParsedUrl};
pub use resolver_markers::ResolverMarkers;
pub(crate) use urls::Urls;
use uv_configuration::{Constraints, Overrides};
use uv_distribution::{ArchiveMetadata, DistributionDatabase};
@ -73,6 +74,7 @@ mod index;
mod locals;
mod provider;
mod reporter;
mod resolver_markers;
mod urls;
pub struct Resolver<Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvider> {
@ -94,8 +96,7 @@ struct ResolverState<InstalledPackages: InstalledPackagesProvider> {
urls: Urls,
dependency_mode: DependencyMode,
hasher: HashStrategy,
/// When not set, the resolver is in "universal" mode.
markers: Option<MarkerEnvironment>,
markers: ResolverMarkers,
python_requirement: PythonRequirement,
/// This is derived from `PythonRequirement` once at initialization
/// time. It's used in universal mode to filter our dependencies with
@ -140,7 +141,7 @@ impl<'a, Context: BuildContext, InstalledPackages: InstalledPackagesProvider>
manifest: Manifest,
options: Options,
python_requirement: &'a PythonRequirement,
markers: Option<&'a MarkerEnvironment>,
markers: ResolverMarkers,
tags: Option<&'a Tags>,
flat_index: &'a FlatIndex,
index: &'a InMemoryIndex,
@ -156,7 +157,11 @@ impl<'a, Context: BuildContext, InstalledPackages: InstalledPackagesProvider>
python_requirement
.target()
.and_then(|target| target.as_requires_python()),
AllowedYanks::from_manifest(&manifest, markers, options.dependency_mode),
AllowedYanks::from_manifest(
&manifest,
markers.marker_environment(),
options.dependency_mode,
),
hasher,
options.exclude_newer,
build_context.build_options(),
@ -184,7 +189,7 @@ impl<Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvider>
manifest: Manifest,
options: Options,
hasher: &HashStrategy,
markers: Option<&MarkerEnvironment>,
markers: ResolverMarkers,
python_requirement: &PythonRequirement,
index: &InMemoryIndex,
git: &GitResolver,
@ -196,9 +201,18 @@ impl<Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvider>
git: git.clone(),
unavailable_packages: DashMap::default(),
incomplete_packages: DashMap::default(),
selector: CandidateSelector::for_resolution(options, &manifest, markers),
selector: CandidateSelector::for_resolution(
options,
&manifest,
markers.marker_environment(),
),
dependency_mode: options.dependency_mode,
urls: Urls::from_manifest(&manifest, markers, git, options.dependency_mode)?,
urls: Urls::from_manifest(
&manifest,
markers.marker_environment(),
git,
options.dependency_mode,
)?,
project: manifest.project,
requirements: manifest.requirements,
constraints: manifest.constraints,
@ -207,12 +221,12 @@ impl<Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvider>
preferences: manifest.preferences,
exclusions: manifest.exclusions,
hasher: hasher.clone(),
markers: markers.cloned(),
requires_python: if markers.is_some() {
requires_python: if markers.marker_environment().is_some() {
None
} else {
python_requirement.to_marker_tree()
},
markers,
python_requirement: python_requirement.clone(),
reporter: None,
installed_packages,
@ -296,7 +310,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
fork_locals: ForkLocals::default(),
priorities: PubGrubPriorities::default(),
added_dependencies: FxHashMap::default(),
markers: MarkerTree::And(vec![]),
markers: self.markers.clone(),
python_requirement: self.python_requirement.clone(),
requires_python: self.requires_python.clone(),
};
@ -305,14 +319,14 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
let mut resolutions = vec![];
'FORK: while let Some(mut state) = forked_states.pop() {
if !state.markers.is_universal() {
if let ResolverMarkers::Fork(markers) = &state.markers {
if let Some(requires_python) = state.requires_python.as_ref() {
debug!(
"Solving split {} (requires-python: {})",
state.markers, requires_python
markers, requires_python
);
} else {
debug!("Solving split {}", state.markers);
debug!("Solving split {}", markers);
}
}
let start = Instant::now();
@ -321,7 +335,8 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
if let Err(err) = state.pubgrub.unit_propagation(state.next.clone()) {
return Err(self.convert_no_solution_err(
err,
state.fork_urls.clone(),
state.fork_urls,
state.markers,
&visited,
&index_locations,
));
@ -348,7 +363,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
prefetcher.log_tried_versions();
}
debug!(
"Split {} took {:.3}s",
"Split {} resolution took {:.3}s",
state.markers,
start.elapsed().as_secs_f32()
);
@ -540,15 +555,15 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
cur_state = Some(forked_state.clone());
}
forked_state.markers.and(fork.markers);
forked_state.markers = normalize(forked_state.markers, None)
.unwrap_or(MarkerTree::And(Vec::new()));
let combined_markers = forked_state.markers.and(fork.markers);
let combined_markers = normalize(combined_markers, None)
.expect("Fork markers are universal");
// If the fork contains a narrowed Python requirement, apply it.
let python_requirement = requires_python_marker(
&forked_state.markers,
)
.and_then(|marker| forked_state.python_requirement.narrow(&marker));
let python_requirement = requires_python_marker(&combined_markers)
.and_then(|marker| {
forked_state.python_requirement.narrow(&marker)
});
if let Some(python_requirement) = python_requirement {
if let Some(target) = python_requirement.target() {
debug!("Narrowed `requires-python` bound to: {target}");
@ -562,6 +577,8 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
forked_state.python_requirement = python_requirement;
}
forked_state.markers = ResolverMarkers::Fork(combined_markers);
forked_state.add_package_version_dependencies(
for_package.as_deref(),
&version,
@ -584,18 +601,11 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
}
forked_states.push(forked_state);
}
if markers.is_universal() {
debug!(
"Pre-fork split universal took {:.3}s",
start.elapsed().as_secs_f32()
);
} else {
debug!(
"Pre-fork split {} took {:.3}s",
markers,
start.elapsed().as_secs_f32()
);
}
debug!(
"Pre-fork split {} took {:.3}s",
markers,
start.elapsed().as_secs_f32()
);
continue 'FORK;
}
}
@ -1083,7 +1093,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
version: &Version,
fork_urls: &ForkUrls,
fork_locals: &ForkLocals,
markers: &MarkerTree,
markers: &ResolverMarkers,
requires_python: Option<&MarkerTree>,
) -> Result<ForkedDependencies, ResolveError> {
let result = self.get_dependencies(
@ -1094,13 +1104,13 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
markers,
requires_python,
);
if self.markers.is_some() {
return result.map(|deps| match deps {
match markers {
ResolverMarkers::SpecificEnvironment(_) => result.map(|deps| match deps {
Dependencies::Available(deps) => ForkedDependencies::Unforked(deps),
Dependencies::Unavailable(err) => ForkedDependencies::Unavailable(err),
});
}),
ResolverMarkers::Universal | ResolverMarkers::Fork(_) => Ok(result?.fork()),
}
Ok(result?.fork())
}
/// Given a candidate package and version, return its dependencies.
@ -1111,7 +1121,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
version: &Version,
fork_urls: &ForkUrls,
fork_locals: &ForkLocals,
markers: &MarkerTree,
markers: &ResolverMarkers,
requires_python: Option<&MarkerTree>,
) -> Result<Dependencies, ResolveError> {
let url = package.name().and_then(|name| fork_urls.get(name));
@ -1383,7 +1393,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
extra: Option<&'a ExtraName>,
dev: Option<&'a GroupName>,
name: Option<&PackageName>,
markers: &'a MarkerTree,
markers: &'a ResolverMarkers,
requires_python: Option<&'a MarkerTree>,
) -> Vec<Cow<'a, Requirement>> {
// Start with the requirements for the current extra of the package (for an extra
@ -1448,7 +1458,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
&'data self,
dependencies: impl IntoIterator<Item = &'data Requirement> + 'parameters,
extra: Option<&'parameters ExtraName>,
markers: &'parameters MarkerTree,
markers: &'parameters ResolverMarkers,
requires_python: Option<&'parameters MarkerTree>,
) -> impl Iterator<Item = Cow<'data, Requirement>> + 'parameters
where
@ -1469,31 +1479,31 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
return false;
}
// If we're in universal mode, `fork_markers` might correspond to a
// non-trivial marker expression that provoked the resolver to fork.
// In that case, we should ignore any dependency that cannot possibly
// satisfy the markers that provoked the fork.
if !possible_to_satisfy_markers(markers, requirement) {
trace!("skipping {requirement} because of context resolver markers {markers}");
return false;
// If we're in a fork in universal mode, ignore any dependency that isn't part of
// this fork (but will be part of another fork).
if let ResolverMarkers::Fork(markers) = markers {
if !possible_to_satisfy_markers(markers, requirement) {
trace!("skipping {requirement} because of context resolver markers {markers}");
return false;
}
}
// If the requirement isn't relevant for the current platform, skip it.
match extra {
Some(source_extra) => {
// Only include requirements that are relevant for the current extra.
if requirement.evaluate_markers(self.markers.as_ref(), &[]) {
if requirement.evaluate_markers(markers.marker_environment(), &[]) {
return false;
}
if !requirement.evaluate_markers(
self.markers.as_ref(),
markers.marker_environment(),
std::slice::from_ref(source_extra),
) {
return false;
}
}
None => {
if !requirement.evaluate_markers(self.markers.as_ref(), &[]) {
if !requirement.evaluate_markers(markers.marker_environment(), &[]) {
return false;
}
}
@ -1516,23 +1526,27 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
return false;
}
if !possible_to_satisfy_markers(markers, constraint) {
trace!("skipping {constraint} because of context resolver markers {markers}");
return false;
// If we're in a fork in universal mode, ignore any dependency that isn't part of
// this fork (but will be part of another fork).
if let ResolverMarkers::Fork(markers) = markers {
if !possible_to_satisfy_markers(markers, constraint) {
trace!("skipping {constraint} because of context resolver markers {markers}");
return false;
}
}
// If the constraint isn't relevant for the current platform, skip it.
match extra {
Some(source_extra) => {
if !constraint.evaluate_markers(
self.markers.as_ref(),
markers.marker_environment(),
std::slice::from_ref(source_extra),
) {
return false;
}
}
None => {
if !constraint.evaluate_markers(self.markers.as_ref(), &[]) {
if !constraint.evaluate_markers(markers.marker_environment(), &[]) {
return false;
}
}
@ -1835,6 +1849,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
&self,
mut err: pubgrub::error::NoSolutionError<UvDependencyProvider>,
fork_urls: ForkUrls,
markers: ResolverMarkers,
visited: &FxHashSet<PackageName>,
index_locations: &IndexLocations,
) -> ResolveError {
@ -1897,6 +1912,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
unavailable_packages,
incomplete_packages,
fork_urls,
markers,
))
}
@ -1976,7 +1992,7 @@ struct ForkState {
/// completely disjoint marker expression (i.e., it can never be true given
/// that the marker expression that provoked the fork is true), then that
/// dependency is completely ignored.
markers: MarkerTree,
markers: ResolverMarkers,
/// The Python requirement for this fork. Defaults to the Python requirement for
/// the resolution, but may be narrowed if a `python_version` marker is present
/// in a given fork.

View file

@ -0,0 +1,50 @@
use pep508_rs::{MarkerEnvironment, MarkerTree};
use std::fmt::{Display, Formatter};
#[derive(Debug, Clone)]
/// Whether we're solving for a specific environment, universally or for a specific fork.
pub enum ResolverMarkers {
/// We're solving for this specific environment only.
SpecificEnvironment(MarkerEnvironment),
/// We're doing a universal resolution for all environments (a python version
/// constraint is expressed separately).
Universal,
/// We're in a fork of the universal resolution solving only for specific markers.
Fork(MarkerTree),
}
impl ResolverMarkers {
/// Add the markers of an initial or subsequent fork to the current markers.
pub(crate) fn and(self, other: MarkerTree) -> MarkerTree {
match self {
ResolverMarkers::Universal => other,
ResolverMarkers::Fork(mut current) => {
current.and(other);
current
}
ResolverMarkers::SpecificEnvironment(_) => {
unreachable!("Specific environment mode must not fork")
}
}
}
/// If solving for a specific environment, return this environment
pub fn marker_environment(&self) -> Option<&MarkerEnvironment> {
match self {
ResolverMarkers::Universal | ResolverMarkers::Fork(_) => None,
ResolverMarkers::SpecificEnvironment(env) => Some(env),
}
}
}
impl Display for ResolverMarkers {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ResolverMarkers::Universal => f.write_str("universal"),
ResolverMarkers::SpecificEnvironment(_) => f.write_str("specific environment"),
ResolverMarkers::Fork(markers) => {
write!(f, "({markers})")
}
}
}
}

View file

@ -174,7 +174,7 @@ impl Urls {
.chain(iter::once(verbatim_url.verbatim().to_string()))
.collect();
conflicting_urls.sort();
return Err(ResolveError::ConflictingUrls(
return Err(ResolveError::ConflictingUrlsUniversal(
package_name.clone(),
conflicting_urls,
));

View file

@ -33,7 +33,7 @@ use uv_requirements::{
use uv_resolver::{
AnnotationStyle, DependencyMode, DisplayResolutionGraph, ExcludeNewer, FlatIndex,
InMemoryIndex, OptionsBuilder, PreReleaseMode, PythonRequirement, RequiresPython,
ResolutionMode,
ResolutionMode, ResolverMarkers,
};
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight};
use uv_warnings::warn_user;
@ -230,11 +230,14 @@ pub(crate) async fn pip_compile(
// Determine the environment for the resolution.
let (tags, markers) = if universal {
(None, None)
(None, ResolverMarkers::Universal)
} else {
let (tags, markers) =
resolution_environment(python_version, python_platform, &interpreter)?;
(Some(tags), Some(markers))
(
Some(tags),
ResolverMarkers::SpecificEnvironment((*markers).clone()),
)
};
// Generate, but don't enforce hashes for the requirements.
@ -335,7 +338,7 @@ pub(crate) async fn pip_compile(
&Reinstall::None,
&upgrade,
tags.as_deref(),
markers.as_deref(),
markers.clone(),
python_requirement,
&client,
&flat_index,
@ -351,8 +354,7 @@ pub(crate) async fn pip_compile(
{
Ok(resolution) => resolution,
Err(operations::Error::Resolve(uv_resolver::ResolveError::NoSolution(err))) => {
let report = miette::Report::msg(format!("{err}"))
.context("No solution found when resolving dependencies:");
let report = miette::Report::msg(format!("{err}")).context(err.header());
eprint!("{report:?}");
return Ok(ExitStatus::Failure);
}
@ -384,7 +386,7 @@ pub(crate) async fn pip_compile(
}
if include_marker_expression {
if let Some(markers) = markers.as_deref() {
if let ResolverMarkers::SpecificEnvironment(markers) = &markers {
let relevant_markers = resolution.marker_tree(&top_level_index, markers)?;
writeln!(
writer,
@ -457,7 +459,7 @@ pub(crate) async fn pip_compile(
"{}",
DisplayResolutionGraph::new(
&resolution,
markers.as_deref(),
&markers,
&no_emit_packages,
generate_hashes,
include_extras,

View file

@ -25,7 +25,7 @@ use uv_python::{
use uv_requirements::{RequirementsSource, RequirementsSpecification};
use uv_resolver::{
DependencyMode, ExcludeNewer, FlatIndex, OptionsBuilder, PreReleaseMode, PythonRequirement,
ResolutionMode,
ResolutionMode, ResolverMarkers,
};
use uv_types::{BuildIsolation, HashStrategy};
@ -326,7 +326,7 @@ pub(crate) async fn pip_install(
&reinstall,
&upgrade,
Some(&tags),
Some(&markers),
ResolverMarkers::SpecificEnvironment((*markers).clone()),
python_requirement,
&client,
&flat_index,
@ -342,8 +342,7 @@ pub(crate) async fn pip_install(
{
Ok(resolution) => Resolution::from(resolution),
Err(operations::Error::Resolve(uv_resolver::ResolveError::NoSolution(err))) => {
let report = miette::Report::msg(format!("{err}"))
.context("No solution found when resolving dependencies:");
let report = miette::Report::msg(format!("{err}")).context(err.header());
eprint!("{report:?}");
return Ok(ExitStatus::Failure);
}

View file

@ -16,7 +16,6 @@ use distribution_types::{
DistributionMetadata, IndexLocations, InstalledMetadata, LocalDist, Name, Resolution,
};
use install_wheel_rs::linker::LinkMode;
use pep508_rs::MarkerEnvironment;
use platform_tags::Tags;
use pypi_types::Requirement;
use uv_cache::Cache;
@ -37,7 +36,7 @@ use uv_requirements::{
};
use uv_resolver::{
DependencyMode, Exclusions, FlatIndex, InMemoryIndex, Manifest, Options, Preference,
Preferences, PythonRequirement, ResolutionGraph, Resolver,
Preferences, PythonRequirement, ResolutionGraph, Resolver, ResolverMarkers,
};
use uv_types::{HashStrategy, InFlight, InstalledPackagesProvider};
use uv_warnings::warn_user;
@ -88,7 +87,7 @@ pub(crate) async fn resolve<InstalledPackages: InstalledPackagesProvider>(
reinstall: &Reinstall,
upgrade: &Upgrade,
tags: Option<&Tags>,
markers: Option<&MarkerEnvironment>,
markers: ResolverMarkers,
python_requirement: PythonRequirement,
client: &RegistryClient,
flat_index: &FlatIndex,
@ -188,7 +187,7 @@ pub(crate) async fn resolve<InstalledPackages: InstalledPackagesProvider>(
.chain(upgrade.constraints().cloned()),
);
let overrides = Overrides::from_requirements(overrides);
let preferences = Preferences::from_iter(preferences, markers);
let preferences = Preferences::from_iter(preferences, markers.marker_environment());
// Determine any lookahead requirements.
let lookaheads = match options.dependency_mode {
@ -203,7 +202,7 @@ pub(crate) async fn resolve<InstalledPackages: InstalledPackagesProvider>(
DistributionDatabase::new(client, build_dispatch, concurrency.downloads, preview),
)
.with_reporter(ResolverReporter::from(printer))
.resolve(markers)
.resolve(&markers)
.await?
}
DependencyMode::Direct => Vec::new(),

View file

@ -24,7 +24,7 @@ use uv_python::{
use uv_requirements::{RequirementsSource, RequirementsSpecification};
use uv_resolver::{
DependencyMode, ExcludeNewer, FlatIndex, OptionsBuilder, PreReleaseMode, PythonRequirement,
ResolutionMode,
ResolutionMode, ResolverMarkers,
};
use uv_types::{BuildIsolation, HashStrategy};
@ -281,7 +281,7 @@ pub(crate) async fn pip_sync(
&reinstall,
&upgrade,
Some(&tags),
Some(&markers),
ResolverMarkers::SpecificEnvironment((*markers).clone()),
python_requirement,
&client,
&flat_index,
@ -297,8 +297,7 @@ pub(crate) async fn pip_sync(
{
Ok(resolution) => Resolution::from(resolution),
Err(operations::Error::Resolve(uv_resolver::ResolveError::NoSolution(err))) => {
let report = miette::Report::msg(format!("{err}"))
.context("No solution found when resolving dependencies:");
let report = miette::Report::msg(format!("{err}")).context(err.header());
eprint!("{report:?}");
return Ok(ExitStatus::Failure);
}

View file

@ -17,7 +17,9 @@ use uv_git::ResolvedRepositoryReference;
use uv_normalize::PackageName;
use uv_python::{Interpreter, PythonFetch, PythonPreference, PythonRequest};
use uv_requirements::upgrade::{read_lock_requirements, LockedRequirements};
use uv_resolver::{FlatIndex, Lock, OptionsBuilder, PythonRequirement, RequiresPython};
use uv_resolver::{
FlatIndex, Lock, OptionsBuilder, PythonRequirement, RequiresPython, ResolverMarkers,
};
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
use uv_warnings::{warn_user, warn_user_once};
@ -88,8 +90,7 @@ pub(crate) async fn lock(
Err(ProjectError::Operation(pip::operations::Error::Resolve(
uv_resolver::ResolveError::NoSolution(err),
))) => {
let report = miette::Report::msg(format!("{err}"))
.context("No solution found when resolving dependencies:");
let report = miette::Report::msg(format!("{err}")).context(err.header());
eprint!("{report:?}");
Ok(ExitStatus::Failure)
}
@ -274,7 +275,7 @@ pub(super) async fn do_lock(
&Reinstall::default(),
upgrade,
None,
None,
ResolverMarkers::Universal,
python_requirement.clone(),
&client,
&flat_index,
@ -350,7 +351,7 @@ pub(super) async fn do_lock(
&Reinstall::default(),
upgrade,
None,
None,
ResolverMarkers::Universal,
python_requirement,
&client,
&flat_index,

View file

@ -21,7 +21,9 @@ use uv_python::{
PythonInstallation, PythonPreference, PythonRequest, VersionRequest,
};
use uv_requirements::{NamedRequirementsResolver, RequirementsSpecification};
use uv_resolver::{FlatIndex, OptionsBuilder, PythonRequirement, RequiresPython, ResolutionGraph};
use uv_resolver::{
FlatIndex, OptionsBuilder, PythonRequirement, RequiresPython, ResolutionGraph, ResolverMarkers,
};
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
use uv_warnings::warn_user;
@ -485,7 +487,7 @@ pub(crate) async fn resolve_environment<'a>(
&reinstall,
&upgrade,
Some(tags),
Some(markers),
ResolverMarkers::SpecificEnvironment(markers.clone()),
python_requirement,
&client,
&flat_index,
@ -737,7 +739,7 @@ pub(crate) async fn update_environment(
reinstall,
upgrade,
Some(tags),
Some(markers),
ResolverMarkers::SpecificEnvironment(markers.clone()),
python_requirement,
&client,
&flat_index,

View file

@ -92,8 +92,7 @@ pub(crate) async fn sync(
Err(ProjectError::Operation(pip::operations::Error::Resolve(
uv_resolver::ResolveError::NoSolution(err),
))) => {
let report = miette::Report::msg(format!("{err}"))
.context("No solution found when resolving dependencies:");
let report = miette::Report::msg(format!("{err}")).context(err.header());
anstream::eprint!("{report:?}");
return Ok(ExitStatus::Failure);
}
@ -133,8 +132,7 @@ pub(crate) async fn sync(
Err(ProjectError::Operation(pip::operations::Error::Resolve(
uv_resolver::ResolveError::NoSolution(err),
))) => {
let report = miette::Report::msg(format!("{err}"))
.context("No solution found when resolving dependencies:");
let report = miette::Report::msg(format!("{err}")).context(err.header());
anstream::eprint!("{report:?}");
return Ok(ExitStatus::Failure);
}

View file

@ -362,7 +362,7 @@ fn conflict_in_fork() -> Result<()> {
----- stderr -----
warning: `uv lock` is experimental and may change without warning.
× No solution found when resolving dependencies:
× No solution found when resolving dependencies for split (sys_platform == 'darwin'):
Because only package-b==1.0.0 is available and package-b==1.0.0 depends on package-d==1, we can conclude that all versions of package-b depend on package-d==1.
And because package-c==1.0.0 depends on package-d==2 and only package-c==1.0.0 is available, we can conclude that all versions of package-b and all versions of package-c are incompatible.
And because package-a{sys_platform == 'darwin'}==1.0.0 depends on package-b and package-c, we can conclude that package-a{sys_platform == 'darwin'}==1.0.0 cannot be used.

View file

@ -6872,7 +6872,7 @@ fn universal_requires_python() -> Result<()> {
----- stderr -----
warning: The requested Python version 3.8 is not available; 3.12.[X] will be used to build dependencies instead.
× No solution found when resolving dependencies:
× No solution found when resolving dependencies for split (python_version >= '3.9'):
Because only the following versions of numpy{python_version >= '3.9'} are available:
numpy{python_version >= '3.9'}<=1.26.0
numpy{python_version >= '3.9'}==1.26.1