Skip to main content

uv_resolver/
universal_marker.rs

1use std::borrow::Borrow;
2use std::collections::BTreeSet;
3use std::str::FromStr;
4
5use itertools::Itertools;
6use rustc_hash::FxHashMap;
7
8use uv_normalize::{ExtraName, GroupName, PackageName};
9use uv_pep508::{ExtraOperator, MarkerEnvironment, MarkerExpression, MarkerOperator, MarkerTree};
10use uv_pypi_types::{ConflictItem, ConflictKind, Conflicts, Inference};
11
12use crate::ResolveError;
13
14/// A representation of a marker for use in universal resolution.
15///
16/// (This degrades gracefully to a standard PEP 508 marker in the case of
17/// non-universal resolution.)
18///
19/// This universal marker is meant to combine both a PEP 508 marker and a
20/// marker for conflicting extras/groups. The latter specifically expresses
21/// whether a particular edge in a dependency graph should be followed
22/// depending on the activated extras and groups.
23///
24/// A universal marker evaluates to true only when *both* its PEP 508 marker
25/// and its conflict marker evaluate to true.
26#[derive(Default, Copy, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)]
27pub struct UniversalMarker {
28    /// The full combined PEP 508 and "conflict" marker.
29    ///
30    /// In the original design, the PEP 508 marker was kept separate
31    /// from the conflict marker, since the conflict marker is not really
32    /// specified by PEP 508. However, this approach turned out to be
33    /// bunk because the conflict marker vary depending on which part of
34    /// the PEP 508 marker is true. For example, you might have a different
35    /// conflict marker for one platform versus the other. The only way to
36    /// resolve this is to combine them both into one marker.
37    ///
38    /// The downside of this is that since conflict markers aren't part of
39    /// PEP 508, combining them is pretty weird. We could combine them into
40    /// a new type of marker that isn't PEP 508. But it's not clear what the
41    /// best design for that is, and at the time of writing, it would have
42    /// been a lot of additional work. (Our PEP 508 marker implementation is
43    /// rather sophisticated given its boolean simplification capabilities.
44    /// So leveraging all that work is a huge shortcut.) So to accomplish
45    /// this, we technically preserve PEP 508 compatibility but abuse the
46    /// `extra` attribute to encode conflicts.
47    ///
48    /// So for example, if a particular dependency should only be activated
49    /// on `Darwin` and when the extra `x1` for package `foo` is enabled,
50    /// then its "universal" marker looks like this:
51    ///
52    /// ```text
53    /// sys_platform == 'Darwin' and extra == 'extra-3-foo-x1'
54    /// ```
55    ///
56    /// Then, when `uv sync --extra x1` is called, we encode that was
57    /// `extra-3-foo-x1` and pass it as-needed when evaluating this marker.
58    ///
59    /// Why `extra-3-foo-x1`?
60    ///
61    /// * The `extra` prefix is there to distinguish it from `group`.
62    /// * The `3` is there to indicate the length of the package name,
63    ///   in bytes. This isn't strictly necessary for encoding, but
64    ///   is required if we were ever to need to decode a package and
65    ///   extra/group name from a conflict marker.
66    /// * The `foo` package name ensures we namespace the extra/group name,
67    ///   since multiple packages can have the same extra/group name.
68    ///
69    /// We only use alphanumeric characters and hyphens in order to limit
70    /// ourselves to valid extra names. (If we could use other characters then
71    /// that would avoid the need to encode the length of the package name.)
72    ///
73    /// So while the above marker is still technically valid from a PEP 508
74    /// stand-point, evaluating it requires uv's custom encoding of extras (and
75    /// groups).
76    marker: MarkerTree,
77    /// The strictly PEP 508 version of `marker`. Basically, `marker`, but
78    /// without any extras in it. This could be computed on demand (and
79    /// that's what we used to do), but we do it enough that it was causing a
80    /// regression in some cases.
81    pep508: MarkerTree,
82}
83
84impl UniversalMarker {
85    /// A constant universal marker that always evaluates to `true`.
86    pub(crate) const TRUE: Self = Self {
87        marker: MarkerTree::TRUE,
88        pep508: MarkerTree::TRUE,
89    };
90
91    /// A constant universal marker that always evaluates to `false`.
92    pub(crate) const FALSE: Self = Self {
93        marker: MarkerTree::FALSE,
94        pep508: MarkerTree::FALSE,
95    };
96
97    /// Creates a new universal marker from its constituent pieces.
98    pub(crate) fn new(mut pep508_marker: MarkerTree, conflict_marker: ConflictMarker) -> Self {
99        pep508_marker.and(conflict_marker.marker);
100        Self::from_combined(pep508_marker)
101    }
102
103    /// Creates a new universal marker from a marker that has already been
104    /// combined from a PEP 508 and conflict marker.
105    pub(crate) fn from_combined(marker: MarkerTree) -> Self {
106        Self {
107            marker,
108            pep508: marker.without_extras(),
109        }
110    }
111
112    /// Combine this universal marker with the one given in a way that unions
113    /// them. That is, the updated marker will evaluate to `true` if `self` or
114    /// `other` evaluate to `true`.
115    pub(crate) fn or(&mut self, other: Self) {
116        self.marker.or(other.marker);
117        self.pep508.or(other.pep508);
118    }
119
120    /// Combine this universal marker with the one given in a way that
121    /// intersects them. That is, the updated marker will evaluate to `true` if
122    /// `self` and `other` evaluate to `true`.
123    pub(crate) fn and(&mut self, other: Self) {
124        self.marker.and(other.marker);
125        self.pep508.and(other.pep508);
126    }
127
128    /// Imbibes the world knowledge expressed by `conflicts` into this marker.
129    ///
130    /// This will effectively simplify the conflict marker in this universal
131    /// marker. In particular, it enables simplifying based on the fact that no
132    /// two items from the same set in the given conflicts can be active at a
133    /// given time.
134    pub(crate) fn imbibe(&mut self, conflicts: ConflictMarker) {
135        let self_marker = self.marker;
136        self.marker = conflicts.marker;
137        self.marker.implies(self_marker);
138        self.pep508 = self.marker.without_extras();
139    }
140
141    /// If all inference sets reduce to the same marker, simplify the marker using that knowledge.
142    pub(crate) fn unify_inference_sets(&mut self, conflict_sets: &[BTreeSet<Inference>]) {
143        let mut previous_marker = None;
144
145        for conflict_set in conflict_sets {
146            let mut marker = self.marker;
147            for inference in conflict_set {
148                let extra = encode_conflict_item(&inference.item);
149
150                marker = if inference.included {
151                    marker.simplify_extras_with(|candidate| *candidate == extra)
152                } else {
153                    marker.simplify_not_extras_with(|candidate| *candidate == extra)
154                };
155            }
156            if let Some(previous_marker) = &previous_marker {
157                if previous_marker != &marker {
158                    return;
159                }
160            } else {
161                previous_marker = Some(marker);
162            }
163        }
164
165        if let Some(all_branches_marker) = previous_marker {
166            self.marker = all_branches_marker;
167            self.pep508 = self.marker.without_extras();
168        }
169    }
170
171    /// Assumes that a given extra/group for the given package is activated.
172    ///
173    /// This may simplify the conflicting marker component of this universal
174    /// marker.
175    pub(crate) fn assume_conflict_item(&mut self, item: &ConflictItem) {
176        match *item.kind() {
177            ConflictKind::Extra(ref extra) => self.assume_extra(item.package(), extra),
178            ConflictKind::Group(ref group) => self.assume_group(item.package(), group),
179            ConflictKind::Project => self.assume_project(item.package()),
180        }
181        self.pep508 = self.marker.without_extras();
182    }
183
184    /// Assumes that a given extra/group for the given package is not
185    /// activated.
186    ///
187    /// This may simplify the conflicting marker component of this universal
188    /// marker.
189    pub(crate) fn assume_not_conflict_item(&mut self, item: &ConflictItem) {
190        match *item.kind() {
191            ConflictKind::Extra(ref extra) => self.assume_not_extra(item.package(), extra),
192            ConflictKind::Group(ref group) => self.assume_not_group(item.package(), group),
193            ConflictKind::Project => self.assume_not_project(item.package()),
194        }
195        self.pep508 = self.marker.without_extras();
196    }
197
198    /// Assumes that the "production" dependencies for the given project are
199    /// activated.
200    ///
201    /// This may simplify the conflicting marker component of this universal
202    /// marker.
203    fn assume_project(&mut self, package: &PackageName) {
204        let extra = encode_project(package);
205        self.marker = self
206            .marker
207            .simplify_extras_with(|candidate| *candidate == extra);
208        self.pep508 = self.marker.without_extras();
209    }
210
211    /// Assumes that the "production" dependencies for the given project are
212    /// not activated.
213    ///
214    /// This may simplify the conflicting marker component of this universal
215    /// marker.
216    fn assume_not_project(&mut self, package: &PackageName) {
217        let extra = encode_project(package);
218        self.marker = self
219            .marker
220            .simplify_not_extras_with(|candidate| *candidate == extra);
221        self.pep508 = self.marker.without_extras();
222    }
223
224    /// Assumes that a given extra for the given package is activated.
225    ///
226    /// This may simplify the conflicting marker component of this universal
227    /// marker.
228    fn assume_extra(&mut self, package: &PackageName, extra: &ExtraName) {
229        let extra = encode_package_extra(package, extra);
230        self.marker = self
231            .marker
232            .simplify_extras_with(|candidate| *candidate == extra);
233        self.pep508 = self.marker.without_extras();
234    }
235
236    /// Assumes that a given extra for the given package is not activated.
237    ///
238    /// This may simplify the conflicting marker component of this universal
239    /// marker.
240    fn assume_not_extra(&mut self, package: &PackageName, extra: &ExtraName) {
241        let extra = encode_package_extra(package, extra);
242        self.marker = self
243            .marker
244            .simplify_not_extras_with(|candidate| *candidate == extra);
245        self.pep508 = self.marker.without_extras();
246    }
247
248    /// Assumes that a given group for the given package is activated.
249    ///
250    /// This may simplify the conflicting marker component of this universal
251    /// marker.
252    fn assume_group(&mut self, package: &PackageName, group: &GroupName) {
253        let extra = encode_package_group(package, group);
254        self.marker = self
255            .marker
256            .simplify_extras_with(|candidate| *candidate == extra);
257        self.pep508 = self.marker.without_extras();
258    }
259
260    /// Assumes that a given group for the given package is not activated.
261    ///
262    /// This may simplify the conflicting marker component of this universal
263    /// marker.
264    fn assume_not_group(&mut self, package: &PackageName, group: &GroupName) {
265        let extra = encode_package_group(package, group);
266        self.marker = self
267            .marker
268            .simplify_not_extras_with(|candidate| *candidate == extra);
269        self.pep508 = self.marker.without_extras();
270    }
271
272    /// Returns true if this universal marker will always evaluate to `true`.
273    pub(crate) fn is_true(self) -> bool {
274        self.marker.is_true()
275    }
276
277    /// Returns true if this universal marker will always evaluate to `false`.
278    pub(crate) fn is_false(self) -> bool {
279        self.marker.is_false()
280    }
281
282    /// Returns true if this universal marker contains a conflict marker.
283    ///
284    /// Conflict items are encoded as `extra` expressions in `marker`, while `pep508` is the same
285    /// canonical marker with all `extra` expressions removed. Since [`MarkerTree`] equality is
286    /// semantic, the trees differ exactly when the marker depends on a conflict item.
287    pub(crate) fn has_conflict_marker(self) -> bool {
288        self.marker != self.pep508
289    }
290
291    /// Returns true if this universal marker is disjoint with the one given.
292    ///
293    /// Two universal markers are disjoint when it is impossible for them both
294    /// to evaluate to `true` simultaneously.
295    pub(crate) fn is_disjoint(self, other: Self) -> bool {
296        self.marker.is_disjoint(other.marker)
297    }
298
299    /// Returns true if this universal marker is satisfied by the given marker
300    /// environment.
301    ///
302    /// This should only be used when evaluating a marker that is known not to
303    /// have any extras. For example, the PEP 508 markers on a fork.
304    pub(crate) fn evaluate_no_extras(self, env: &MarkerEnvironment) -> bool {
305        self.marker.evaluate(env, &[])
306    }
307
308    /// Returns true if this universal marker is satisfied by the given marker
309    /// environment and list of activated extras and groups.
310    ///
311    /// The activated extras and groups should be the complete set activated
312    /// for a particular context. And each extra and group must be scoped to
313    /// the particular package that it's enabled for.
314    pub(crate) fn evaluate<P, E, G>(
315        self,
316        env: &MarkerEnvironment,
317        projects: impl Iterator<Item = P>,
318        extras: impl Iterator<Item = (P, E)>,
319        groups: impl Iterator<Item = (P, G)>,
320    ) -> bool
321    where
322        P: Borrow<PackageName>,
323        E: Borrow<ExtraName>,
324        G: Borrow<GroupName>,
325    {
326        let projects = projects.map(|package| encode_project(package.borrow()));
327        let extras =
328            extras.map(|(package, extra)| encode_package_extra(package.borrow(), extra.borrow()));
329        let groups =
330            groups.map(|(package, group)| encode_package_group(package.borrow(), group.borrow()));
331        self.marker.evaluate(
332            env,
333            &projects
334                .chain(extras)
335                .chain(groups)
336                .collect::<Vec<ExtraName>>(),
337        )
338    }
339
340    /// Returns true if the marker always evaluates to true if the given set of extras is activated.
341    pub(crate) fn evaluate_only_extras<P, E, G>(self, extras: &[(P, E)], groups: &[(P, G)]) -> bool
342    where
343        P: Borrow<PackageName>,
344        E: Borrow<ExtraName>,
345        G: Borrow<GroupName>,
346    {
347        let extras = extras
348            .iter()
349            .map(|(package, extra)| encode_package_extra(package.borrow(), extra.borrow()));
350        let groups = groups
351            .iter()
352            .map(|(package, group)| encode_package_group(package.borrow(), group.borrow()));
353        self.marker
354            .evaluate_only_extras(&extras.chain(groups).collect::<Vec<ExtraName>>())
355    }
356
357    /// Returns the internal marker that combines both the PEP 508
358    /// and conflict marker.
359    pub fn combined(self) -> MarkerTree {
360        self.marker
361    }
362
363    /// Returns the PEP 508 marker for this universal marker.
364    ///
365    /// One should be cautious using this. Generally speaking, it should only
366    /// be used when one knows universal resolution isn't in effect. When
367    /// universal resolution is enabled (i.e., there may be multiple forks
368    /// producing different versions of the same package), then one should
369    /// always use a universal marker since it accounts for all possible ways
370    /// for a package to be installed.
371    pub(crate) fn pep508(self) -> MarkerTree {
372        self.pep508
373    }
374
375    /// Returns the non-PEP 508 marker expression that represents conflicting
376    /// extras/groups.
377    ///
378    /// Like with `UniversalMarker::pep508`, one should be cautious when using
379    /// this. It is generally always wrong to consider conflicts in isolation
380    /// from PEP 508 markers. But this can be useful for detecting failure
381    /// cases. For example, the code for emitting a `ResolverOutput` (even a
382    /// universal one) in a `requirements.txt` format checks for the existence
383    /// of non-trivial conflict markers and fails if any are found. (Because
384    /// conflict markers cannot be represented in the `requirements.txt`
385    /// format.)
386    pub(crate) fn conflict(self) -> ConflictMarker {
387        ConflictMarker {
388            marker: self.marker.only_extras(),
389        }
390    }
391
392    /// Returns the conflict marker that remains after evaluating all PEP 508 expressions in the
393    /// given environment.
394    ///
395    /// Unlike [`UniversalMarker::conflict`], this preserves the relationship between PEP 508 and
396    /// conflict expressions. For example, given `sys_platform == 'linux' or extra == 'foo'`, the
397    /// conflict marker is always true on Linux but still depends on `foo` elsewhere.
398    pub(crate) fn conflict_for_environment(self, env: &MarkerEnvironment) -> ConflictMarker {
399        let mut remaining = MarkerTree::FALSE;
400
401        'conjunctions: for conjunction in self.marker.to_dnf() {
402            let mut conflict = MarkerTree::TRUE;
403            for expression in conjunction {
404                match expression {
405                    expression @ MarkerExpression::Extra { .. } => {
406                        conflict.and(MarkerTree::expression(expression));
407                    }
408                    expression => {
409                        if !MarkerTree::expression(expression).evaluate(env, &[]) {
410                            continue 'conjunctions;
411                        }
412                    }
413                }
414            }
415            remaining.or(conflict);
416        }
417
418        ConflictMarker { marker: remaining }
419    }
420}
421
422impl std::fmt::Debug for UniversalMarker {
423    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
424        std::fmt::Debug::fmt(&self.marker, f)
425    }
426}
427
428/// A marker that is only for representing conflicting extras/groups.
429///
430/// This encapsulates the encoding of extras and groups into PEP 508
431/// markers.
432#[derive(Default, Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)]
433pub struct ConflictMarker {
434    marker: MarkerTree,
435}
436
437impl ConflictMarker {
438    /// A constant conflict marker that always evaluates to `true`.
439    pub const TRUE: Self = Self {
440        marker: MarkerTree::TRUE,
441    };
442
443    /// A constant conflict marker that always evaluates to `false`.
444    pub const FALSE: Self = Self {
445        marker: MarkerTree::FALSE,
446    };
447
448    /// Creates a new conflict marker from the declared conflicts provided.
449    pub(crate) fn from_conflicts(conflicts: &Conflicts) -> Self {
450        if conflicts.is_empty() {
451            return Self::TRUE;
452        }
453        let mut marker = Self::TRUE;
454        for set in conflicts.iter() {
455            for (item1, item2) in set.iter().tuple_combinations() {
456                let pair = Self::from_conflict_item(item1)
457                    .negate()
458                    .or(Self::from_conflict_item(item2).negate());
459                marker = marker.and(pair);
460            }
461        }
462        marker
463    }
464
465    /// Create a conflict marker that is true only when the given extra or
466    /// group (for a specific package) is activated.
467    pub(crate) fn from_conflict_item(item: &ConflictItem) -> Self {
468        match *item.kind() {
469            ConflictKind::Extra(ref extra) => Self::extra(item.package(), extra),
470            ConflictKind::Group(ref group) => Self::group(item.package(), group),
471            ConflictKind::Project => Self::project(item.package()),
472        }
473    }
474
475    /// Create a conflict marker that is true only when the production
476    /// dependencies for the given package are activated.
477    fn project(package: &PackageName) -> Self {
478        let operator = uv_pep508::ExtraOperator::Equal;
479        let name = uv_pep508::MarkerValueExtra::Extra(encode_project(package));
480        let expr = uv_pep508::MarkerExpression::Extra { operator, name };
481        let marker = MarkerTree::expression(expr);
482        Self { marker }
483    }
484
485    /// Create a conflict marker that is true only when the given extra for the
486    /// given package is activated.
487    fn extra(package: &PackageName, extra: &ExtraName) -> Self {
488        let operator = uv_pep508::ExtraOperator::Equal;
489        let name = uv_pep508::MarkerValueExtra::Extra(encode_package_extra(package, extra));
490        let expr = uv_pep508::MarkerExpression::Extra { operator, name };
491        let marker = MarkerTree::expression(expr);
492        Self { marker }
493    }
494
495    /// Create a conflict marker that is true only when the given group for the
496    /// given package is activated.
497    fn group(package: &PackageName, group: &GroupName) -> Self {
498        let operator = uv_pep508::ExtraOperator::Equal;
499        let name = uv_pep508::MarkerValueExtra::Extra(encode_package_group(package, group));
500        let expr = uv_pep508::MarkerExpression::Extra { operator, name };
501        let marker = MarkerTree::expression(expr);
502        Self { marker }
503    }
504
505    /// Returns a new conflict marker that is the negation of this one.
506    #[must_use]
507    pub(crate) fn negate(self) -> Self {
508        Self {
509            marker: self.marker.negate(),
510        }
511    }
512
513    /// Returns a new conflict marker corresponding to the union of `self` and
514    /// `other`.
515    #[must_use]
516    fn or(self, other: Self) -> Self {
517        let mut marker = self.marker;
518        marker.or(other.marker);
519        Self { marker }
520    }
521
522    /// Returns a new conflict marker corresponding to the intersection of
523    /// `self` and `other`.
524    #[must_use]
525    pub(crate) fn and(self, other: Self) -> Self {
526        let mut marker = self.marker;
527        marker.and(other.marker);
528        Self { marker }
529    }
530
531    /// Returns true if this conflict marker will always evaluate to `true`.
532    pub(crate) fn is_true(self) -> bool {
533        self.marker.is_true()
534    }
535
536    /// Returns true if this conflict marker always evaluates to the same value.
537    pub(crate) fn is_constant(self) -> bool {
538        self.marker.is_true() || self.marker.is_false()
539    }
540
541    /// Returns inclusion and exclusion (respectively) conflict items parsed
542    /// from this conflict marker.
543    ///
544    /// This returns an error if any `extra` could not be parsed as a valid
545    /// encoded conflict extra.
546    pub(crate) fn filter_rules(
547        self,
548    ) -> Result<(Vec<ConflictItem>, Vec<ConflictItem>), ResolveError> {
549        let (mut raw_include, mut raw_exclude) = (vec![], vec![]);
550        self.marker.visit_extras(|op, extra| {
551            match op {
552                MarkerOperator::Equal => raw_include.push(extra.to_owned()),
553                MarkerOperator::NotEqual => raw_exclude.push(extra.to_owned()),
554                // OK by the contract of `MarkerTree::visit_extras`.
555                _ => unreachable!(),
556            }
557        });
558        let include = raw_include
559            .into_iter()
560            .map(|extra| ParsedRawExtra::parse(&extra).and_then(|parsed| parsed.to_conflict_item()))
561            .collect::<Result<Vec<_>, _>>()?;
562        let exclude = raw_exclude
563            .into_iter()
564            .map(|extra| ParsedRawExtra::parse(&extra).and_then(|parsed| parsed.to_conflict_item()))
565            .collect::<Result<Vec<_>, _>>()?;
566        Ok((include, exclude))
567    }
568}
569
570impl std::fmt::Debug for ConflictMarker {
571    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
572        // This is a little more succinct than the default.
573        write!(f, "ConflictMarker({:?})", self.marker)
574    }
575}
576
577/// Encodes the given conflict into a valid `extra` value in a PEP 508 marker.
578fn encode_conflict_item(conflict: &ConflictItem) -> ExtraName {
579    match conflict.kind() {
580        ConflictKind::Extra(extra) => encode_package_extra(conflict.package(), extra),
581        ConflictKind::Group(group) => encode_package_group(conflict.package(), group),
582        ConflictKind::Project => encode_project(conflict.package()),
583    }
584}
585
586/// Encodes the given package name and its corresponding extra into a valid
587/// `extra` value in a PEP 508 marker.
588fn encode_package_extra(package: &PackageName, extra: &ExtraName) -> ExtraName {
589    // This is OK because `PackageName` and `ExtraName` have the same
590    // validation rules, and we combine them in a way that always results in a
591    // valid name.
592    //
593    // Note also that we encode the length of the package name (in bytes) into
594    // the encoded extra name as well. This ensures we can parse out both the
595    // package and extra name if necessary. If we didn't do this, then some
596    // cases could be ambiguous since our field delimiter (`-`) is also a valid
597    // character in `package` or `extra` values. But if we know the length of
598    // the package name, we can always parse each field unambiguously.
599    let package_len = package.as_str().len();
600    ExtraName::from_owned(format!("extra-{package_len}-{package}-{extra}")).unwrap()
601}
602
603/// Encodes the given package name and its corresponding group into a valid
604/// `extra` value in a PEP 508 marker.
605fn encode_package_group(package: &PackageName, group: &GroupName) -> ExtraName {
606    // See `encode_package_extra`, the same considerations apply here.
607    let package_len = package.as_str().len();
608    ExtraName::from_owned(format!("group-{package_len}-{package}-{group}")).unwrap()
609}
610
611/// Encodes the given project package name into a valid `extra` value in a PEP
612/// 508 marker.
613fn encode_project(package: &PackageName) -> ExtraName {
614    // See `encode_package_extra`, the same considerations apply here.
615    let package_len = package.as_str().len();
616    ExtraName::from_owned(format!("project-{package_len}-{package}")).unwrap()
617}
618
619#[derive(Debug)]
620enum ParsedRawExtra<'a> {
621    Project { package: &'a str },
622    Extra { package: &'a str, extra: &'a str },
623    Group { package: &'a str, group: &'a str },
624}
625
626impl<'a> ParsedRawExtra<'a> {
627    fn parse(raw_extra: &'a ExtraName) -> Result<Self, ResolveError> {
628        fn mkerr(raw_extra: &ExtraName, reason: impl Into<String>) -> ResolveError {
629            let raw_extra = raw_extra.to_owned();
630            let reason = reason.into();
631            ResolveError::InvalidExtraInConflictMarker { reason, raw_extra }
632        }
633
634        let raw = raw_extra.as_str();
635        let Some((kind, tail)) = raw.split_once('-') else {
636            return Err(mkerr(
637                raw_extra,
638                "expected to find leading `package`, `extra-` or `group-`",
639            ));
640        };
641        let Some((len, tail)) = tail.split_once('-') else {
642            return Err(mkerr(
643                raw_extra,
644                "expected to find `{number}-` after leading `package-`, `extra-` or `group-`",
645            ));
646        };
647        let len = len.parse::<usize>().map_err(|_| {
648            mkerr(
649                raw_extra,
650                format!("found package length number `{len}`, but could not parse into integer"),
651            )
652        })?;
653        let Some((package, tail)) = tail.split_at_checked(len) else {
654            return Err(mkerr(
655                raw_extra,
656                format!(
657                    "expected at least {len} bytes for package name, but found {found}",
658                    found = tail.len()
659                ),
660            ));
661        };
662        match kind {
663            "project" => Ok(ParsedRawExtra::Project { package }),
664            "extra" | "group" => {
665                if !tail.starts_with('-') {
666                    return Err(mkerr(
667                        raw_extra,
668                        format!("expected `-` after package name `{package}`"),
669                    ));
670                }
671                let tail = &tail[1..];
672                if kind == "extra" {
673                    Ok(ParsedRawExtra::Extra {
674                        package,
675                        extra: tail,
676                    })
677                } else {
678                    Ok(ParsedRawExtra::Group {
679                        package,
680                        group: tail,
681                    })
682                }
683            }
684            _ => Err(mkerr(
685                raw_extra,
686                format!("unrecognized kind `{kind}` (must be `extra` or `group`)"),
687            )),
688        }
689    }
690
691    fn to_conflict_item(&self) -> Result<ConflictItem, ResolveError> {
692        let package = PackageName::from_str(self.package()).map_err(|name_error| {
693            ResolveError::InvalidValueInConflictMarker {
694                kind: "package",
695                name_error,
696            }
697        })?;
698        match self {
699            Self::Project { .. } => Ok(ConflictItem::from(package)),
700            Self::Extra { extra, .. } => {
701                let extra = ExtraName::from_str(extra).map_err(|name_error| {
702                    ResolveError::InvalidValueInConflictMarker {
703                        kind: "extra",
704                        name_error,
705                    }
706                })?;
707                Ok(ConflictItem::from((package, extra)))
708            }
709            Self::Group { group, .. } => {
710                let group = GroupName::from_str(group).map_err(|name_error| {
711                    ResolveError::InvalidValueInConflictMarker {
712                        kind: "group",
713                        name_error,
714                    }
715                })?;
716                Ok(ConflictItem::from((package, group)))
717            }
718        }
719    }
720
721    fn package(&self) -> &'a str {
722        match self {
723            Self::Project { package, .. } => package,
724            Self::Extra { package, .. } => package,
725            Self::Group { package, .. } => package,
726        }
727    }
728}
729
730/// Resolve the conflict markers in a [`MarkerTree`] based on the conditions under which each
731/// conflict item is known to be true.
732///
733/// For example, if the `cpu` extra is known to be enabled when `sys_platform == 'darwin'`, then
734/// given the combined marker `python_version >= '3.8' and extra == 'extra-7-project-cpu'`, this
735/// method would return `python_version >= '3.8' and sys_platform == 'darwin'`.
736///
737/// If a conflict item isn't present in the map of known conflicts, it's assumed to be false in all
738/// environments.
739/// Resolve unencoded package extra markers and conflict-encoded extra markers in a
740/// [`MarkerTree`] based on the conditions under which each item is known to be true.
741///
742/// When `scope_package` is set, unencoded package extras like `extra == 'cpu'` are interpreted
743/// relative to that package. Conflict-encoded extras and groups are resolved independent of
744/// `scope_package`.
745pub(crate) fn resolve_activated_extras(
746    marker: MarkerTree,
747    scope_package: Option<&PackageName>,
748    known_conflicts: &FxHashMap<ConflictItem, MarkerTree>,
749) -> MarkerTree {
750    if marker.is_true() || marker.is_false() {
751        return marker;
752    }
753
754    let mut transformed = MarkerTree::FALSE;
755
756    // Convert the marker to DNF, then re-build it.
757    for dnf in marker.to_dnf() {
758        let mut or = MarkerTree::TRUE;
759
760        for marker in dnf {
761            let MarkerExpression::Extra {
762                ref operator,
763                ref name,
764            } = marker
765            else {
766                or.and(MarkerTree::expression(marker));
767                continue;
768            };
769
770            let Some(name) = name.as_extra() else {
771                or.and(MarkerTree::expression(marker));
772                continue;
773            };
774
775            // Given an extra marker (like `extra == 'extra-7-project-cpu'`), search for the
776            // corresponding conflict; once found, inline the marker of conditions under which the
777            // conflict is known to be true.
778            let mut found = false;
779            for (conflict_item, conflict_marker) in known_conflicts {
780                // Search for the conflict item as an extra.
781                if let Some(extra) = conflict_item.extra() {
782                    let package = conflict_item.package();
783                    let encoded = encode_package_extra(package, extra);
784                    if encoded == *name {
785                        match operator {
786                            ExtraOperator::Equal => {
787                                or.and(*conflict_marker);
788                                found = true;
789                                break;
790                            }
791                            ExtraOperator::NotEqual => {
792                                or.and(conflict_marker.negate());
793                                found = true;
794                                break;
795                            }
796                        }
797                    }
798                }
799
800                // Search for the conflict item as a group.
801                if let Some(group) = conflict_item.group() {
802                    let package = conflict_item.package();
803                    let encoded = encode_package_group(package, group);
804                    if encoded == *name {
805                        match operator {
806                            ExtraOperator::Equal => {
807                                or.and(*conflict_marker);
808                                found = true;
809                                break;
810                            }
811                            ExtraOperator::NotEqual => {
812                                or.and(conflict_marker.negate());
813                                found = true;
814                                break;
815                            }
816                        }
817                    }
818                }
819
820                // Search for the conflict item as a project.
821                if conflict_item.extra().is_none() && conflict_item.group().is_none() {
822                    let package = conflict_item.package();
823                    let encoded = encode_project(package);
824                    if encoded == *name {
825                        match operator {
826                            ExtraOperator::Equal => {
827                                or.and(*conflict_marker);
828                                found = true;
829                                break;
830                            }
831                            ExtraOperator::NotEqual => {
832                                or.and(conflict_marker.negate());
833                                found = true;
834                                break;
835                            }
836                        }
837                    }
838                }
839            }
840
841            // Search for an unencoded package extra in the current package scope.
842            if !found {
843                if let Some(package) = scope_package {
844                    let conflict_item = ConflictItem::from((package.clone(), name.clone()));
845                    if let Some(conflict_marker) = known_conflicts.get(&conflict_item) {
846                        match operator {
847                            ExtraOperator::Equal => {
848                                or.and(*conflict_marker);
849                                found = true;
850                            }
851                            ExtraOperator::NotEqual => {
852                                or.and(conflict_marker.negate());
853                                found = true;
854                            }
855                        }
856                    }
857                }
858            }
859
860            // If we didn't find the marker in the list of known conflicts, assume it's always
861            // false.
862            if !found {
863                match operator {
864                    ExtraOperator::Equal => {
865                        or.and(MarkerTree::FALSE);
866                    }
867                    ExtraOperator::NotEqual => {
868                        or.and(MarkerTree::TRUE);
869                    }
870                }
871            }
872        }
873
874        transformed.or(or);
875    }
876
877    transformed
878}
879
880#[cfg(test)]
881mod tests {
882    use super::*;
883    use std::str::FromStr;
884
885    use uv_pypi_types::ConflictSet;
886
887    /// Creates a collection of declared conflicts from the sets
888    /// provided.
889    fn create_conflicts(it: impl IntoIterator<Item = ConflictSet>) -> Conflicts {
890        let mut conflicts = Conflicts::empty();
891        for set in it {
892            conflicts.push(set);
893        }
894        conflicts
895    }
896
897    /// Creates a single set of conflicting items.
898    ///
899    /// For convenience, this always creates conflicting items with a package
900    /// name of `foo` and with the given string as the extra name.
901    fn create_set<'a>(it: impl IntoIterator<Item = &'a str>) -> ConflictSet {
902        let items = it
903            .into_iter()
904            .map(|extra| (create_package("pkg"), create_extra(extra)))
905            .map(ConflictItem::from)
906            .collect::<Vec<ConflictItem>>();
907        ConflictSet::try_from(items).unwrap()
908    }
909
910    /// Shortcut for creating a package name.
911    fn create_package(name: &str) -> PackageName {
912        PackageName::from_str(name).unwrap()
913    }
914
915    /// Shortcut for creating an extra name.
916    fn create_extra(name: &str) -> ExtraName {
917        ExtraName::from_str(name).unwrap()
918    }
919
920    /// Shortcut for creating a conflict marker from an extra name.
921    fn create_extra_marker(name: &str) -> ConflictMarker {
922        ConflictMarker::extra(&create_package("pkg"), &create_extra(name))
923    }
924
925    /// Shortcut for creating a conflict item from an extra name.
926    fn create_extra_item(name: &str) -> ConflictItem {
927        ConflictItem::from((create_package("pkg"), create_extra(name)))
928    }
929
930    /// Shortcut for creating a conflict map.
931    fn create_known_conflicts<'a>(
932        it: impl IntoIterator<Item = (&'a str, &'a str)>,
933    ) -> FxHashMap<ConflictItem, MarkerTree> {
934        it.into_iter()
935            .map(|(extra, marker)| {
936                (
937                    create_extra_item(extra),
938                    MarkerTree::from_str(marker).unwrap(),
939                )
940            })
941            .collect()
942    }
943
944    /// Returns a string representation of the given conflict marker.
945    ///
946    /// This is just the underlying marker. And if it's `true`, then a
947    /// non-conforming `true` string is returned. (Which is fine since
948    /// this is just for tests.)
949    fn to_str(cm: ConflictMarker) -> String {
950        cm.marker
951            .try_to_string()
952            .unwrap_or_else(|| "true".to_string())
953    }
954
955    /// This tests the conversion from declared conflicts into a conflict
956    /// marker. This is used to describe "world knowledge" about which
957    /// extras/groups are and aren't allowed to be activated together.
958    #[test]
959    fn conflicts_as_marker() {
960        let conflicts = create_conflicts([create_set(["foo", "bar"])]);
961        let cm = ConflictMarker::from_conflicts(&conflicts);
962        assert_eq!(
963            to_str(cm),
964            "extra != 'extra-3-pkg-foo' or extra != 'extra-3-pkg-bar'"
965        );
966
967        let conflicts = create_conflicts([create_set(["foo", "bar", "baz"])]);
968        let cm = ConflictMarker::from_conflicts(&conflicts);
969        assert_eq!(
970            to_str(cm),
971            "(extra != 'extra-3-pkg-baz' and extra != 'extra-3-pkg-foo') \
972             or (extra != 'extra-3-pkg-bar' and extra != 'extra-3-pkg-foo') \
973             or (extra != 'extra-3-pkg-bar' and extra != 'extra-3-pkg-baz')",
974        );
975
976        let conflicts = create_conflicts([create_set(["foo", "bar"]), create_set(["fox", "ant"])]);
977        let cm = ConflictMarker::from_conflicts(&conflicts);
978        assert_eq!(
979            to_str(cm),
980            "(extra != 'extra-3-pkg-bar' and extra != 'extra-3-pkg-fox') or \
981             (extra != 'extra-3-pkg-ant' and extra != 'extra-3-pkg-foo') or \
982             (extra != 'extra-3-pkg-ant' and extra != 'extra-3-pkg-bar') or \
983             (extra == 'extra-3-pkg-bar' and extra != 'extra-3-pkg-foo' and extra != 'extra-3-pkg-fox')",
984        );
985        // I believe because markers are put into DNF, the marker we get here
986        // is a lot bigger than what we might expect. Namely, this is how it's
987        // constructed:
988        //
989        //     (extra != 'extra-3-pkg-foo' or extra != 'extra-3-pkg-bar')
990        //     and (extra != 'extra-3-pkg-fox' or extra != 'extra-3-pkg-ant')
991        //
992        // In other words, you can't have both `foo` and `bar` active, and you
993        // can't have both `fox` and `ant` active. But any other combination
994        // is valid. So let's step through all of them to make sure the marker
995        // below gives the expected result. (I did this because it's not at all
996        // obvious to me that the above two markers are equivalent.)
997        let disallowed = [
998            vec!["foo", "bar"],
999            vec!["fox", "ant"],
1000            vec!["foo", "fox", "bar"],
1001            vec!["foo", "ant", "bar"],
1002            vec!["ant", "foo", "fox"],
1003            vec!["ant", "bar", "fox"],
1004            vec!["foo", "bar", "fox", "ant"],
1005        ];
1006        for extra_names in disallowed {
1007            let extras = extra_names
1008                .iter()
1009                .copied()
1010                .map(|name| (create_package("pkg"), create_extra(name)))
1011                .collect::<Vec<(PackageName, ExtraName)>>();
1012            let groups = Vec::<(PackageName, GroupName)>::new();
1013            assert!(
1014                !UniversalMarker::new(MarkerTree::TRUE, cm).evaluate_only_extras(&extras, &groups),
1015                "expected `{extra_names:?}` to evaluate to `false` in `{cm:?}`"
1016            );
1017        }
1018        let allowed = [
1019            vec![],
1020            vec!["foo"],
1021            vec!["bar"],
1022            vec!["fox"],
1023            vec!["ant"],
1024            vec!["foo", "fox"],
1025            vec!["foo", "ant"],
1026            vec!["bar", "fox"],
1027            vec!["bar", "ant"],
1028        ];
1029        for extra_names in allowed {
1030            let extras = extra_names
1031                .iter()
1032                .copied()
1033                .map(|name| (create_package("pkg"), create_extra(name)))
1034                .collect::<Vec<(PackageName, ExtraName)>>();
1035            let groups = Vec::<(PackageName, GroupName)>::new();
1036            assert!(
1037                UniversalMarker::new(MarkerTree::TRUE, cm).evaluate_only_extras(&extras, &groups),
1038                "expected `{extra_names:?}` to evaluate to `true` in `{cm:?}`"
1039            );
1040        }
1041    }
1042
1043    /// This tests conflict marker simplification after "imbibing" world
1044    /// knowledge about which extras/groups cannot be activated together.
1045    #[test]
1046    fn imbibe() {
1047        let conflicts = create_conflicts([create_set(["foo", "bar"])]);
1048        let conflicts_marker = ConflictMarker::from_conflicts(&conflicts);
1049        let foo = create_extra_marker("foo");
1050        let bar = create_extra_marker("bar");
1051
1052        // In this case, we simulate a dependency whose conflict marker
1053        // is just repeating the fact that conflicting extras cannot
1054        // both be activated. So this one simplifies to `true`.
1055        let mut dep_conflict_marker =
1056            UniversalMarker::new(MarkerTree::TRUE, foo.negate().or(bar.negate()));
1057        assert_eq!(
1058            format!("{dep_conflict_marker:?}"),
1059            "extra != 'extra-3-pkg-foo' or extra != 'extra-3-pkg-bar'"
1060        );
1061        dep_conflict_marker.imbibe(conflicts_marker);
1062        assert_eq!(format!("{dep_conflict_marker:?}"), "true");
1063    }
1064
1065    #[test]
1066    fn has_conflict_marker() {
1067        let pep508 =
1068            MarkerTree::from_str("sys_platform == 'darwin'").expect("valid marker expression");
1069        assert!(!UniversalMarker::from_combined(pep508).has_conflict_marker());
1070        assert!(UniversalMarker::new(pep508, create_extra_marker("foo")).has_conflict_marker());
1071    }
1072
1073    #[test]
1074    fn resolve() {
1075        let known_conflicts = create_known_conflicts([("foo", "sys_platform == 'darwin'")]);
1076        let cm = MarkerTree::from_str("(python_version >= '3.10' and extra == 'extra-3-pkg-foo') or (python_version < '3.10' and extra != 'extra-3-pkg-foo')").unwrap();
1077        let cm = resolve_activated_extras(cm, None, &known_conflicts);
1078        assert_eq!(
1079            cm.try_to_string().as_deref(),
1080            Some(
1081                "(python_full_version < '3.10' and sys_platform != 'darwin') or (python_full_version >= '3.10' and sys_platform == 'darwin')"
1082            )
1083        );
1084
1085        let cm = MarkerTree::from_str("python_version >= '3.10' and extra == 'extra-3-pkg-foo'")
1086            .unwrap();
1087        let cm = resolve_activated_extras(cm, None, &known_conflicts);
1088        assert_eq!(
1089            cm.try_to_string().as_deref(),
1090            Some("python_full_version >= '3.10' and sys_platform == 'darwin'")
1091        );
1092
1093        let cm = MarkerTree::from_str("python_version >= '3.10' and extra == 'extra-3-pkg-bar'")
1094            .unwrap();
1095        let cm = resolve_activated_extras(cm, None, &known_conflicts);
1096        assert!(cm.is_false());
1097    }
1098
1099    #[test]
1100    fn resolve_unencoded_package_extras() {
1101        let known_conflicts = create_known_conflicts([("foo", "sys_platform == 'darwin'")]);
1102        let package = create_package("pkg");
1103
1104        let cm = MarkerTree::from_str("python_version >= '3.10' and extra == 'foo'").unwrap();
1105        let cm = resolve_activated_extras(cm, Some(&package), &known_conflicts);
1106        assert_eq!(
1107            cm.try_to_string().as_deref(),
1108            Some("python_full_version >= '3.10' and sys_platform == 'darwin'")
1109        );
1110
1111        let cm = MarkerTree::from_str("python_version >= '3.10' and extra != 'foo'").unwrap();
1112        let cm = resolve_activated_extras(cm, Some(&package), &known_conflicts);
1113        assert_eq!(
1114            cm.try_to_string().as_deref(),
1115            Some("python_full_version >= '3.10' and sys_platform != 'darwin'")
1116        );
1117
1118        let cm = MarkerTree::from_str("python_version >= '3.10' and extra == 'bar'").unwrap();
1119        let cm = resolve_activated_extras(cm, Some(&package), &known_conflicts);
1120        assert!(cm.is_false());
1121    }
1122}