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