uv_configuration/
dependency_groups.rs

1use std::{borrow::Cow, sync::Arc};
2
3use uv_normalize::{DEV_DEPENDENCIES, DefaultGroups, GroupName};
4
5/// Manager of all dependency-group decisions and settings history.
6///
7/// This is an Arc mostly just to avoid size bloat on things that contain these.
8#[derive(Debug, Default, Clone)]
9pub struct DependencyGroups(Arc<DependencyGroupsInner>);
10
11/// Manager of all dependency-group decisions and settings history.
12#[derive(Debug, Default, Clone)]
13pub struct DependencyGroupsInner {
14    /// Groups to include.
15    include: IncludeGroups,
16    /// Groups to exclude (always wins over include).
17    exclude: Vec<GroupName>,
18    /// Whether an `--only` flag was passed.
19    ///
20    /// If true, users of this API should refrain from looking at packages
21    /// that *aren't* specified by the dependency-groups. This is exposed
22    /// via [`DependencyGroupsInner::prod`][].
23    only_groups: bool,
24    /// The "raw" flags/settings we were passed for diagnostics.
25    history: DependencyGroupsHistory,
26}
27
28impl DependencyGroups {
29    /// Create from history.
30    ///
31    /// This is the "real" constructor, it's basically taking raw CLI flags but in
32    /// a way that's a bit nicer for other constructors to use.
33    fn from_history(history: DependencyGroupsHistory) -> Self {
34        let DependencyGroupsHistory {
35            dev_mode,
36            mut group,
37            mut only_group,
38            mut no_group,
39            all_groups,
40            no_default_groups,
41            mut defaults,
42        } = history.clone();
43
44        // First desugar --dev flags
45        match dev_mode {
46            Some(DevMode::Include) => group.push(DEV_DEPENDENCIES.clone()),
47            Some(DevMode::Only) => only_group.push(DEV_DEPENDENCIES.clone()),
48            Some(DevMode::Exclude) => no_group.push(DEV_DEPENDENCIES.clone()),
49            None => {}
50        }
51
52        // `group` and `only_group` actually have the same meanings: packages to include.
53        // But if `only_group` is non-empty then *other* packages should be excluded.
54        // So we just record whether it was and then treat the two lists as equivalent.
55        let only_groups = !only_group.is_empty();
56        // --only flags imply --no-default-groups
57        let default_groups = !no_default_groups && !only_groups;
58
59        let include = if all_groups {
60            // If this is set we can ignore group/only_group/defaults as irrelevant
61            // (`--all-groups --only-*` is rejected at the CLI level, don't worry about it).
62            IncludeGroups::All
63        } else {
64            // Merge all these lists, they're equivalent now
65            group.append(&mut only_group);
66            // Resolve default groups potentially also setting All
67            if default_groups {
68                match &mut defaults {
69                    DefaultGroups::All => IncludeGroups::All,
70                    DefaultGroups::List(defaults) => {
71                        group.append(defaults);
72                        IncludeGroups::Some(group)
73                    }
74                }
75            } else {
76                IncludeGroups::Some(group)
77            }
78        };
79
80        Self(Arc::new(DependencyGroupsInner {
81            include,
82            exclude: no_group,
83            only_groups,
84            history,
85        }))
86    }
87
88    /// Create from raw CLI args
89    #[allow(clippy::fn_params_excessive_bools)]
90    pub fn from_args(
91        dev: bool,
92        no_dev: bool,
93        only_dev: bool,
94        group: Vec<GroupName>,
95        no_group: Vec<GroupName>,
96        no_default_groups: bool,
97        only_group: Vec<GroupName>,
98        all_groups: bool,
99    ) -> Self {
100        // Lower the --dev flags into a single dev mode.
101        //
102        // In theory only one of these 3 flags should be set (enforced by CLI),
103        // but we explicitly allow `--dev` and `--only-dev` to both be set,
104        // and "saturate" that to `--only-dev`.
105        let dev_mode = if only_dev {
106            Some(DevMode::Only)
107        } else if no_dev {
108            Some(DevMode::Exclude)
109        } else if dev {
110            Some(DevMode::Include)
111        } else {
112            None
113        };
114
115        Self::from_history(DependencyGroupsHistory {
116            dev_mode,
117            group,
118            only_group,
119            no_group,
120            all_groups,
121            no_default_groups,
122            // This is unknown at CLI-time, use `.with_defaults(...)` to apply this later!
123            defaults: DefaultGroups::default(),
124        })
125    }
126
127    /// Helper to make a spec from just a --dev flag
128    pub fn from_dev_mode(dev_mode: DevMode) -> Self {
129        Self::from_history(DependencyGroupsHistory {
130            dev_mode: Some(dev_mode),
131            ..Default::default()
132        })
133    }
134
135    /// Helper to make a spec from just a --group
136    pub fn from_group(group: GroupName) -> Self {
137        Self::from_history(DependencyGroupsHistory {
138            group: vec![group],
139            ..Default::default()
140        })
141    }
142
143    /// Apply defaults to a base [`DependencyGroups`].
144    ///
145    /// This is appropriate in projects, where the `dev` group is synced by default.
146    pub fn with_defaults(&self, defaults: DefaultGroups) -> DependencyGroupsWithDefaults {
147        // Explicitly clone the inner history and set the defaults, then remake the result.
148        let mut history = self.0.history.clone();
149        history.defaults = defaults;
150
151        DependencyGroupsWithDefaults {
152            cur: Self::from_history(history),
153            prev: self.clone(),
154        }
155    }
156}
157
158impl std::ops::Deref for DependencyGroups {
159    type Target = DependencyGroupsInner;
160    fn deref(&self) -> &Self::Target {
161        &self.0
162    }
163}
164
165impl DependencyGroupsInner {
166    /// Returns `true` if packages other than the ones referenced by these
167    /// dependency-groups should be considered.
168    ///
169    /// That is, if I tell you to install a project and this is false,
170    /// you should ignore the project itself and all its dependencies,
171    /// and instead just install the dependency-groups.
172    ///
173    /// (This is really just asking if an --only flag was passed.)
174    pub fn prod(&self) -> bool {
175        !self.only_groups
176    }
177
178    /// Returns `true` if the specification includes the given group.
179    pub fn contains(&self, group: &GroupName) -> bool {
180        // exclude always trumps include
181        !self.exclude.contains(group) && self.include.contains(group)
182    }
183
184    /// Iterate over all groups that we think should exist.
185    pub fn desugarred_names(&self) -> impl Iterator<Item = &GroupName> {
186        self.include.names().chain(&self.exclude)
187    }
188
189    /// Returns an iterator over all groups that are included in the specification,
190    /// assuming `all_names` is an iterator over all groups.
191    pub fn group_names<'a, Names>(
192        &'a self,
193        all_names: Names,
194    ) -> impl Iterator<Item = &'a GroupName> + 'a
195    where
196        Names: Iterator<Item = &'a GroupName> + 'a,
197    {
198        all_names.filter(move |name| self.contains(name))
199    }
200
201    /// Iterate over all groups the user explicitly asked for on the CLI
202    pub fn explicit_names(&self) -> impl Iterator<Item = &GroupName> {
203        let DependencyGroupsHistory {
204            // Strictly speaking this is an explicit reference to "dev"
205            // but we're currently tolerant of dev not existing when referenced with
206            // these flags, since it kinda implicitly always exists even if
207            // it's not properly defined in a config file.
208            dev_mode: _,
209            group,
210            only_group,
211            no_group,
212            // These reference no groups explicitly
213            all_groups: _,
214            no_default_groups: _,
215            // This doesn't include defaults because the `dev` group may not be defined
216            // but gets implicitly added as a default sometimes!
217            defaults: _,
218        } = self.history();
219
220        group.iter().chain(no_group).chain(only_group)
221    }
222
223    /// Returns `true` if the specification will have no effect.
224    pub fn is_empty(&self) -> bool {
225        self.prod() && self.exclude.is_empty() && self.include.is_empty()
226    }
227
228    /// Get the raw history for diagnostics
229    pub fn history(&self) -> &DependencyGroupsHistory {
230        &self.history
231    }
232}
233
234/// Context about a [`DependencyGroups`][] that we've preserved for diagnostics
235#[derive(Debug, Default, Clone)]
236pub struct DependencyGroupsHistory {
237    pub dev_mode: Option<DevMode>,
238    pub group: Vec<GroupName>,
239    pub only_group: Vec<GroupName>,
240    pub no_group: Vec<GroupName>,
241    pub all_groups: bool,
242    pub no_default_groups: bool,
243    pub defaults: DefaultGroups,
244}
245
246impl DependencyGroupsHistory {
247    /// Returns all the CLI flags that this represents.
248    ///
249    /// If a flag was provided multiple times (e.g. `--group A --group B`) this will
250    /// elide the arguments and just show the flag once (e.g. just yield "--group").
251    ///
252    /// Conceptually this being an empty list should be equivalent to
253    /// [`DependencyGroups::is_empty`][] when there aren't any defaults set.
254    /// When there are defaults the two will disagree, and rightfully so!
255    pub fn as_flags_pretty(&self) -> Vec<Cow<'_, str>> {
256        let Self {
257            dev_mode,
258            group,
259            only_group,
260            no_group,
261            all_groups,
262            no_default_groups,
263            // defaults aren't CLI flags!
264            defaults: _,
265        } = self;
266
267        let mut flags = vec![];
268        if *all_groups {
269            flags.push(Cow::Borrowed("--all-groups"));
270        }
271        if *no_default_groups {
272            flags.push(Cow::Borrowed("--no-default-groups"));
273        }
274        if let Some(dev_mode) = dev_mode {
275            flags.push(Cow::Borrowed(dev_mode.as_flag()));
276        }
277        match &**group {
278            [] => {}
279            [group] => flags.push(Cow::Owned(format!("--group {group}"))),
280            [..] => flags.push(Cow::Borrowed("--group")),
281        }
282        match &**only_group {
283            [] => {}
284            [group] => flags.push(Cow::Owned(format!("--only-group {group}"))),
285            [..] => flags.push(Cow::Borrowed("--only-group")),
286        }
287        match &**no_group {
288            [] => {}
289            [group] => flags.push(Cow::Owned(format!("--no-group {group}"))),
290            [..] => flags.push(Cow::Borrowed("--no-group")),
291        }
292        flags
293    }
294}
295
296/// A trivial newtype wrapped around [`DependencyGroups`][] that signifies "defaults applied"
297///
298/// It includes a copy of the previous semantics to provide info on if
299/// the group being a default actually affected it being enabled, because it's obviously "correct".
300/// (These are Arcs so it's ~free to hold onto the previous semantics)
301#[derive(Debug, Clone)]
302pub struct DependencyGroupsWithDefaults {
303    /// The active semantics
304    cur: DependencyGroups,
305    /// The semantics before defaults were applied
306    prev: DependencyGroups,
307}
308
309impl DependencyGroupsWithDefaults {
310    /// Do not enable any groups
311    ///
312    /// Many places in the code need to know what dependency-groups are active,
313    /// but various commands or subsystems never enable any dependency-groups,
314    /// in which case they want this.
315    pub fn none() -> Self {
316        DependencyGroups::default().with_defaults(DefaultGroups::default())
317    }
318
319    /// Returns `true` if the specification was enabled, and *only* because it was a default
320    pub fn contains_because_default(&self, group: &GroupName) -> bool {
321        self.cur.contains(group) && !self.prev.contains(group)
322    }
323}
324impl std::ops::Deref for DependencyGroupsWithDefaults {
325    type Target = DependencyGroups;
326    fn deref(&self) -> &Self::Target {
327        &self.cur
328    }
329}
330
331#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
332pub enum DevMode {
333    /// Include development dependencies.
334    #[default]
335    Include,
336    /// Exclude development dependencies.
337    Exclude,
338    /// Only include development dependencies, excluding all other dependencies.
339    Only,
340}
341
342impl DevMode {
343    /// Returns the flag that was used to request development dependencies.
344    pub fn as_flag(&self) -> &'static str {
345        match self {
346            Self::Exclude => "--no-dev",
347            Self::Include => "--dev",
348            Self::Only => "--only-dev",
349        }
350    }
351}
352
353#[derive(Debug, Clone)]
354pub enum IncludeGroups {
355    /// Include dependencies from the specified groups.
356    Some(Vec<GroupName>),
357    /// A marker indicates including dependencies from all groups.
358    All,
359}
360
361impl IncludeGroups {
362    /// Returns `true` if the specification includes the given group.
363    pub fn contains(&self, group: &GroupName) -> bool {
364        match self {
365            Self::Some(groups) => groups.contains(group),
366            Self::All => true,
367        }
368    }
369
370    /// Returns `true` if the specification will have no effect.
371    pub fn is_empty(&self) -> bool {
372        match self {
373            Self::Some(groups) => groups.is_empty(),
374            // Although technically this is a noop if they have no groups,
375            // conceptually they're *trying* to have an effect, so treat it as one.
376            Self::All => false,
377        }
378    }
379
380    /// Iterate over all groups referenced in the [`IncludeGroups`].
381    pub fn names(&self) -> std::slice::Iter<'_, GroupName> {
382        match self {
383            Self::Some(groups) => groups.iter(),
384            Self::All => [].iter(),
385        }
386    }
387}
388
389impl Default for IncludeGroups {
390    fn default() -> Self {
391        Self::Some(Vec::new())
392    }
393}