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