use std::collections::BTreeSet;
use std::sync::Arc;
use itertools::Itertools;
use tracing::trace;
use uv_distribution_types::{RequiresPython, RequiresPythonRange};
use uv_pep440::VersionSpecifiers;
use uv_pep508::{MarkerEnvironment, MarkerTree};
use uv_pypi_types::{
ConflictItem, ConflictItemRef, ConflictKind, ConflictKindRef, ResolverMarkerEnvironment,
};
use crate::pubgrub::{PubGrubDependency, PubGrubPackage};
use crate::resolver::ForkState;
use crate::universal_marker::{ConflictMarker, UniversalMarker};
use crate::{PythonRequirement, ResolveError};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ResolverEnvironment {
kind: Kind,
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum Kind {
Specific {
marker_env: ResolverMarkerEnvironment,
},
Universal {
initial_forks: Arc<[MarkerTree]>,
markers: MarkerTree,
include: Arc<crate::FxHashbrownSet<ConflictItem>>,
exclude: Arc<crate::FxHashbrownSet<ConflictItem>>,
},
}
impl ResolverEnvironment {
pub fn specific(marker_env: ResolverMarkerEnvironment) -> Self {
let kind = Kind::Specific { marker_env };
Self { kind }
}
pub fn universal(initial_forks: Vec<MarkerTree>) -> Self {
let kind = Kind::Universal {
initial_forks: initial_forks.into(),
markers: MarkerTree::TRUE,
include: Arc::new(crate::FxHashbrownSet::default()),
exclude: Arc::new(crate::FxHashbrownSet::default()),
};
Self { kind }
}
pub fn marker_environment(&self) -> Option<&MarkerEnvironment> {
match self.kind {
Kind::Specific { ref marker_env } => Some(marker_env),
Kind::Universal { .. } => None,
}
}
pub(crate) fn included_by_marker(&self, marker: MarkerTree) -> bool {
match self.kind {
Kind::Specific { .. } => true,
Kind::Universal { ref markers, .. } => !markers.is_disjoint(marker),
}
}
pub(crate) fn included_by_group(&self, group: ConflictItemRef<'_>) -> bool {
match self.kind {
Kind::Specific { .. } => true,
Kind::Universal {
ref include,
ref exclude,
..
} => {
if exclude.contains(&group) {
return false;
}
if matches!(group.kind(), ConflictKindRef::Extra(_)) {
if exclude.contains(&ConflictItemRef::from(group.package())) {
return include.contains(&group);
}
}
true
}
}
}
pub(crate) fn requires_python(&self) -> Option<RequiresPythonRange> {
let Kind::Universal {
markers: pep508_marker,
..
} = self.kind
else {
return None;
};
crate::marker::requires_python(pep508_marker)
}
pub(crate) fn fork_markers(&self) -> Option<MarkerTree> {
match self.kind {
Kind::Specific { .. } => None,
Kind::Universal { markers, .. } => Some(markers),
}
}
fn narrow_environment(&self, rhs: MarkerTree) -> Self {
match self.kind {
Kind::Specific { .. } => {
unreachable!("environment narrowing only happens in universal resolution")
}
Kind::Universal {
ref initial_forks,
markers: ref lhs,
ref include,
ref exclude,
} => {
let mut markers = *lhs;
markers.and(rhs);
let kind = Kind::Universal {
initial_forks: Arc::clone(initial_forks),
markers,
include: Arc::clone(include),
exclude: Arc::clone(exclude),
};
Self { kind }
}
}
}
pub(crate) fn filter_by_group(
&self,
rules: impl IntoIterator<Item = Result<ConflictItem, ConflictItem>>,
) -> Option<Self> {
match self.kind {
Kind::Specific { .. } => {
unreachable!("environment narrowing only happens in universal resolution")
}
Kind::Universal {
ref initial_forks,
ref markers,
ref include,
ref exclude,
} => {
let mut include: crate::FxHashbrownSet<_> = (**include).clone();
let mut exclude: crate::FxHashbrownSet<_> = (**exclude).clone();
for rule in rules {
match rule {
Ok(item) => {
if exclude.contains(&item) {
return None;
}
include.insert(item);
}
Err(item) => {
if include.contains(&item) {
return None;
}
exclude.insert(item);
}
}
}
let kind = Kind::Universal {
initial_forks: Arc::clone(initial_forks),
markers: *markers,
include: Arc::new(include),
exclude: Arc::new(exclude),
};
Some(Self { kind })
}
}
}
pub(crate) fn initial_forked_states(
&self,
init: ForkState,
) -> Result<Vec<ForkState>, ResolveError> {
let Kind::Universal {
ref initial_forks,
markers: ref _markers,
include: ref _include,
exclude: ref _exclude,
} = self.kind
else {
return Ok(vec![init]);
};
if initial_forks.is_empty() {
return Ok(vec![init]);
}
initial_forks
.iter()
.rev()
.filter_map(|&initial_fork| {
let combined = UniversalMarker::from_combined(initial_fork);
let (include, exclude) = match combined.conflict().filter_rules() {
Ok(rules) => rules,
Err(err) => return Some(Err(err)),
};
let mut env = self.filter_by_group(
include
.into_iter()
.map(Ok)
.chain(exclude.into_iter().map(Err)),
)?;
env = env.narrow_environment(combined.pep508());
Some(Ok(init.clone().with_env(env)))
})
.collect()
}
pub(crate) fn narrow_python_requirement(
&self,
python_requirement: &PythonRequirement,
) -> Option<PythonRequirement> {
python_requirement.narrow(&self.requires_python()?)
}
pub(crate) fn end_user_fork_display(&self) -> Option<String> {
match &self.kind {
Kind::Specific { .. } => None,
Kind::Universal {
initial_forks: _,
markers,
include,
exclude,
} => {
let format_conflict_item = |conflict_item: &ConflictItem| {
format!(
"{}{}",
conflict_item.package(),
match conflict_item.kind() {
ConflictKind::Extra(extra) => format!("[{extra}]"),
ConflictKind::Group(group) => {
format!("[group:{group}]")
}
ConflictKind::Project => String::new(),
}
)
};
if markers.is_true() && include.is_empty() && exclude.is_empty() {
return None;
}
let mut descriptors = Vec::new();
if !markers.is_true() {
descriptors.push(format!("markers: {markers:?}"));
}
if !include.is_empty() {
descriptors.push(format!(
"included: {}",
include
.iter()
.map(format_conflict_item)
.collect::<BTreeSet<_>>()
.into_iter()
.join(", "),
));
}
if !exclude.is_empty() {
descriptors.push(format!(
"excluded: {}",
exclude
.iter()
.map(format_conflict_item)
.collect::<BTreeSet<_>>()
.into_iter()
.join(", "),
));
}
Some(format!("split ({})", descriptors.join("; ")))
}
}
}
pub(crate) fn try_universal_markers(&self) -> Option<UniversalMarker> {
match self.kind {
Kind::Specific { .. } => None,
Kind::Universal {
ref markers,
ref include,
ref exclude,
..
} => {
let mut conflict_marker = ConflictMarker::TRUE;
for item in exclude.iter() {
conflict_marker =
conflict_marker.and(ConflictMarker::from_conflict_item(item).negate());
}
for item in include.iter() {
conflict_marker = conflict_marker.and(ConflictMarker::from_conflict_item(item));
}
Some(UniversalMarker::new(*markers, conflict_marker))
}
}
}
}
impl std::fmt::Display for ResolverEnvironment {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self.kind {
Kind::Specific { .. } => write!(f, "marker environment"),
Kind::Universal { ref markers, .. } => {
if markers.is_true() {
write!(f, "all marker environments")
} else {
write!(f, "split `{markers:?}`")
}
}
}
}
}
#[derive(Debug)]
pub(crate) enum ForkingPossibility<'d> {
Possible(Forker<'d>),
DependencyAlwaysExcluded,
NoForkingPossible,
}
impl<'d> ForkingPossibility<'d> {
pub(crate) fn new(env: &ResolverEnvironment, dep: &'d PubGrubDependency) -> Self {
let marker = dep.package.marker();
if !env.included_by_marker(marker) {
ForkingPossibility::DependencyAlwaysExcluded
} else if marker.is_true() {
ForkingPossibility::NoForkingPossible
} else {
let forker = Forker {
package: &dep.package,
marker,
};
ForkingPossibility::Possible(forker)
}
}
}
#[derive(Debug)]
pub(crate) struct Forker<'d> {
package: &'d PubGrubPackage,
marker: MarkerTree,
}
impl Forker<'_> {
pub(crate) fn fork(
&self,
env: &ResolverEnvironment,
) -> Option<(Self, Vec<ResolverEnvironment>)> {
if !env.included_by_marker(self.marker) {
return None;
}
let Kind::Universal {
markers: ref env_marker,
..
} = env.kind
else {
panic!("resolver must be in universal mode for forking")
};
let mut envs = vec![];
{
let not_marker = self.marker.negate();
if !env_marker.is_disjoint(not_marker) {
envs.push(env.narrow_environment(not_marker));
}
}
envs.push(env.narrow_environment(self.marker));
let mut remaining_marker = self.marker;
remaining_marker.and(env_marker.negate());
let remaining_forker = Forker {
package: self.package,
marker: remaining_marker,
};
Some((remaining_forker, envs))
}
pub(crate) fn included(&self, env: &ResolverEnvironment) -> bool {
let marker = self.package.marker();
env.included_by_marker(marker)
}
}
pub(crate) fn fork_version_by_python_requirement(
requires_python: &VersionSpecifiers,
python_requirement: &PythonRequirement,
env: &ResolverEnvironment,
) -> Vec<ResolverEnvironment> {
let requires_python = RequiresPython::from_specifiers(requires_python);
let lower = requires_python.range().lower().clone();
let Some((lower, upper)) = python_requirement.split(lower.into()) else {
trace!(
"Unable to split Python requirement `{}` via `Requires-Python` specifier `{}`",
python_requirement.target(),
requires_python,
);
return vec![];
};
let Kind::Universal {
markers: ref env_marker,
..
} = env.kind
else {
panic!("resolver must be in universal mode for forking")
};
let mut envs = vec![];
if !env_marker.is_disjoint(lower.to_marker_tree()) {
envs.push(env.narrow_environment(lower.to_marker_tree()));
}
if !env_marker.is_disjoint(upper.to_marker_tree()) {
envs.push(env.narrow_environment(upper.to_marker_tree()));
}
debug_assert!(!envs.is_empty(), "at least one fork should be produced");
envs
}
pub(crate) fn fork_version_by_marker(
env: &ResolverEnvironment,
marker: MarkerTree,
) -> Option<(ResolverEnvironment, ResolverEnvironment)> {
let Kind::Universal {
markers: ref env_marker,
..
} = env.kind
else {
panic!("resolver must be in universal mode for forking")
};
if env_marker.is_disjoint(marker) {
return None;
}
let with_marker = env.narrow_environment(marker);
let complement = marker.negate();
if env_marker.is_disjoint(complement) {
return None;
}
let without_marker = env.narrow_environment(complement);
Some((with_marker, without_marker))
}
#[cfg(test)]
mod tests {
use std::ops::Bound;
use std::sync::LazyLock;
use uv_pep440::{LowerBound, UpperBound, Version};
use uv_pep508::{MarkerEnvironment, MarkerEnvironmentBuilder};
use uv_distribution_types::{RequiresPython, RequiresPythonRange};
use super::*;
static MARKER_ENV: LazyLock<MarkerEnvironment> = LazyLock::new(|| {
MarkerEnvironment::try_from(MarkerEnvironmentBuilder {
implementation_name: "cpython",
implementation_version: "3.11.5",
os_name: "posix",
platform_machine: "arm64",
platform_python_implementation: "CPython",
platform_release: "21.6.0",
platform_system: "Darwin",
platform_version: "Darwin Kernel Version 21.6.0: Mon Aug 22 20:19:52 PDT 2022; root:xnu-8020.140.49~2/RELEASE_ARM64_T6000",
python_full_version: "3.11.5",
python_version: "3.11",
sys_platform: "darwin",
}).unwrap()
});
fn requires_python_lower(lower_version_bound: &str) -> RequiresPython {
RequiresPython::greater_than_equal_version(&version(lower_version_bound))
}
fn requires_python_range_lower(lower_version_bound: &str) -> RequiresPythonRange {
let lower = LowerBound::new(Bound::Included(version(lower_version_bound)));
RequiresPythonRange::new(lower, UpperBound::default())
}
fn marker(marker: &str) -> MarkerTree {
marker
.parse::<MarkerTree>()
.expect("valid pep508 marker expression")
}
fn version(v: &str) -> Version {
v.parse().expect("valid pep440 version string")
}
fn python_requirement(python_version_greater_than_equal: &str) -> PythonRequirement {
let requires_python = requires_python_lower(python_version_greater_than_equal);
PythonRequirement::from_marker_environment(&MARKER_ENV, requires_python)
}
#[test]
fn narrow_python_requirement_specific() {
let resolver_marker_env = ResolverMarkerEnvironment::from(MARKER_ENV.clone());
let resolver_env = ResolverEnvironment::specific(resolver_marker_env);
let pyreq = python_requirement("3.10");
assert_eq!(resolver_env.narrow_python_requirement(&pyreq), None);
let pyreq = python_requirement("3.11");
assert_eq!(resolver_env.narrow_python_requirement(&pyreq), None);
let pyreq = python_requirement("3.12");
assert_eq!(resolver_env.narrow_python_requirement(&pyreq), None);
}
#[test]
fn narrow_python_requirement_universal() {
let resolver_env = ResolverEnvironment::universal(vec![]);
let pyreq = python_requirement("3.10");
assert_eq!(resolver_env.narrow_python_requirement(&pyreq), None);
let pyreq = python_requirement("3.11");
assert_eq!(resolver_env.narrow_python_requirement(&pyreq), None);
let pyreq = python_requirement("3.12");
assert_eq!(resolver_env.narrow_python_requirement(&pyreq), None);
}
#[test]
fn narrow_python_requirement_forking_no_op() {
let pyreq = python_requirement("3.10");
let resolver_env = ResolverEnvironment::universal(vec![])
.narrow_environment(marker("python_version >= '3.10'"));
assert_eq!(resolver_env.narrow_python_requirement(&pyreq), None);
}
#[test]
fn narrow_python_requirement_forking_stricter() {
let pyreq = python_requirement("3.10");
let resolver_env = ResolverEnvironment::universal(vec![])
.narrow_environment(marker("python_version >= '3.11'"));
let expected = {
let range = requires_python_range_lower("3.11");
let requires_python = requires_python_lower("3.10").narrow(&range).unwrap();
PythonRequirement::from_marker_environment(&MARKER_ENV, requires_python)
};
assert_eq!(
resolver_env.narrow_python_requirement(&pyreq),
Some(expected)
);
}
#[test]
fn narrow_python_requirement_forking_relaxed() {
let pyreq = python_requirement("3.11");
let resolver_env = ResolverEnvironment::universal(vec![])
.narrow_environment(marker("python_version >= '3.10'"));
assert_eq!(
resolver_env.narrow_python_requirement(&pyreq),
Some(python_requirement("3.11")),
);
}
}