Skip to main content

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    pub fn from_args(
90        dev_mode: Option<DevMode>,
91        group: Vec<GroupName>,
92        no_group: Vec<GroupName>,
93        no_default_groups: bool,
94        only_group: Vec<GroupName>,
95        all_groups: bool,
96    ) -> Self {
97        Self::from_history(DependencyGroupsHistory {
98            dev_mode,
99            group,
100            only_group,
101            no_group,
102            all_groups,
103            no_default_groups,
104            // This is unknown at CLI-time, use `.with_defaults(...)` to apply this later!
105            defaults: DefaultGroups::default(),
106        })
107    }
108
109    /// Helper to make a spec from just a --dev flag
110    pub fn from_dev_mode(dev_mode: DevMode) -> Self {
111        Self::from_history(DependencyGroupsHistory {
112            dev_mode: Some(dev_mode),
113            ..Default::default()
114        })
115    }
116
117    /// Helper to make a spec from just a --group
118    pub fn from_group(group: GroupName) -> Self {
119        Self::from_history(DependencyGroupsHistory {
120            group: vec![group],
121            ..Default::default()
122        })
123    }
124
125    /// Helper to make a spec from just --all-groups.
126    pub fn from_all_groups() -> Self {
127        Self::from_history(DependencyGroupsHistory {
128            all_groups: true,
129            ..Default::default()
130        })
131    }
132
133    /// Apply defaults to a base [`DependencyGroups`].
134    ///
135    /// This is appropriate in projects, where the `dev` group is synced by default.
136    pub fn with_defaults(&self, defaults: DefaultGroups) -> DependencyGroupsWithDefaults {
137        // Explicitly clone the inner history and set the defaults, then remake the result.
138        let mut history = self.0.history.clone();
139        history.defaults = defaults;
140
141        DependencyGroupsWithDefaults {
142            cur: Self::from_history(history),
143            prev: self.clone(),
144        }
145    }
146}
147
148impl std::ops::Deref for DependencyGroups {
149    type Target = DependencyGroupsInner;
150    fn deref(&self) -> &Self::Target {
151        &self.0
152    }
153}
154
155impl DependencyGroupsInner {
156    /// Returns `true` if packages other than the ones referenced by these
157    /// dependency-groups should be considered.
158    ///
159    /// That is, if I tell you to install a project and this is false,
160    /// you should ignore the project itself and all its dependencies,
161    /// and instead just install the dependency-groups.
162    ///
163    /// (This is really just asking if an --only flag was passed.)
164    pub fn prod(&self) -> bool {
165        !self.only_groups
166    }
167
168    /// Returns `true` if the specification includes the given group.
169    pub fn contains(&self, group: &GroupName) -> bool {
170        // exclude always trumps include
171        !self.exclude.contains(group) && self.include.contains(group)
172    }
173
174    /// Returns an iterator over all groups that are included in the specification,
175    /// assuming `all_names` is an iterator over all groups.
176    pub fn group_names<'a, Names>(
177        &'a self,
178        all_names: Names,
179    ) -> impl Iterator<Item = &'a GroupName> + 'a
180    where
181        Names: Iterator<Item = &'a GroupName> + 'a,
182    {
183        all_names.filter(move |name| self.contains(name))
184    }
185
186    /// Iterate over all groups the user explicitly asked for on the CLI
187    pub fn explicit_names(&self) -> impl Iterator<Item = &GroupName> {
188        let DependencyGroupsHistory {
189            // Strictly speaking this is an explicit reference to "dev"
190            // but we're currently tolerant of dev not existing when referenced with
191            // these flags, since it kinda implicitly always exists even if
192            // it's not properly defined in a config file.
193            dev_mode: _,
194            group,
195            only_group,
196            no_group,
197            // These reference no groups explicitly
198            all_groups: _,
199            no_default_groups: _,
200            // This doesn't include defaults because the `dev` group may not be defined
201            // but gets implicitly added as a default sometimes!
202            defaults: _,
203        } = self.history();
204
205        group.iter().chain(no_group).chain(only_group)
206    }
207
208    /// Get the raw history for diagnostics
209    pub fn history(&self) -> &DependencyGroupsHistory {
210        &self.history
211    }
212}
213
214/// Context about a [`DependencyGroups`][] that we've preserved for diagnostics
215#[derive(Debug, Default, Clone)]
216pub struct DependencyGroupsHistory {
217    dev_mode: Option<DevMode>,
218    group: Vec<GroupName>,
219    only_group: Vec<GroupName>,
220    no_group: Vec<GroupName>,
221    all_groups: bool,
222    no_default_groups: bool,
223    defaults: DefaultGroups,
224}
225
226impl DependencyGroupsHistory {
227    /// Returns all the CLI flags that this represents.
228    ///
229    /// If a flag was provided multiple times (e.g. `--group A --group B`) this will
230    /// elide the arguments and just show the flag once (e.g. just yield "--group").
231    ///
232    /// Conceptually this being an empty list should be equivalent to
233    /// [`DependencyGroups::is_empty`][] when there aren't any defaults set.
234    /// When there are defaults the two will disagree, and rightfully so!
235    pub fn as_flags_pretty(&self) -> Vec<Cow<'_, str>> {
236        let Self {
237            dev_mode,
238            group,
239            only_group,
240            no_group,
241            all_groups,
242            no_default_groups,
243            // defaults aren't CLI flags!
244            defaults: _,
245        } = self;
246
247        let mut flags = vec![];
248        if *all_groups {
249            flags.push(Cow::Borrowed("--all-groups"));
250        }
251        if *no_default_groups {
252            flags.push(Cow::Borrowed("--no-default-groups"));
253        }
254        if let Some(dev_mode) = dev_mode {
255            flags.push(Cow::Borrowed(dev_mode.as_flag()));
256        }
257        match &**group {
258            [] => {}
259            [group] => flags.push(Cow::Owned(format!("--group {group}"))),
260            [..] => flags.push(Cow::Borrowed("--group")),
261        }
262        match &**only_group {
263            [] => {}
264            [group] => flags.push(Cow::Owned(format!("--only-group {group}"))),
265            [..] => flags.push(Cow::Borrowed("--only-group")),
266        }
267        match &**no_group {
268            [] => {}
269            [group] => flags.push(Cow::Owned(format!("--no-group {group}"))),
270            [..] => flags.push(Cow::Borrowed("--no-group")),
271        }
272        flags
273    }
274}
275
276/// A trivial newtype wrapped around [`DependencyGroups`][] that signifies "defaults applied"
277///
278/// It includes a copy of the previous semantics to provide info on if
279/// the group being a default actually affected it being enabled, because it's obviously "correct".
280/// (These are Arcs so it's ~free to hold onto the previous semantics)
281#[derive(Debug, Clone)]
282pub struct DependencyGroupsWithDefaults {
283    /// The active semantics
284    cur: DependencyGroups,
285    /// The semantics before defaults were applied
286    prev: DependencyGroups,
287}
288
289impl DependencyGroupsWithDefaults {
290    /// Do not enable any groups
291    ///
292    /// Many places in the code need to know what dependency-groups are active,
293    /// but various commands or subsystems never enable any dependency-groups,
294    /// in which case they want this.
295    pub fn none() -> Self {
296        DependencyGroups::default().with_defaults(DefaultGroups::default())
297    }
298
299    /// Returns `true` if the specification was enabled, and *only* because it was a default
300    pub fn contains_because_default(&self, group: &GroupName) -> bool {
301        self.cur.contains(group) && !self.prev.contains(group)
302    }
303}
304impl std::ops::Deref for DependencyGroupsWithDefaults {
305    type Target = DependencyGroups;
306    fn deref(&self) -> &Self::Target {
307        &self.cur
308    }
309}
310
311#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
312pub enum DevMode {
313    /// Include development dependencies.
314    #[default]
315    Include,
316    /// Exclude development dependencies.
317    Exclude,
318    /// Only include development dependencies, excluding all other dependencies.
319    Only,
320}
321
322impl DevMode {
323    /// Determine the development dependency mode from the command-line arguments.
324    pub fn from_args(dev: bool, no_dev: bool, only_dev: bool) -> Option<Self> {
325        // In theory only one of these 3 flags should be set (enforced by CLI),
326        // but we explicitly allow `--dev` and `--only-dev` to both be set,
327        // and "saturate" that to `--only-dev`.
328        if only_dev {
329            Some(Self::Only)
330        } else if no_dev {
331            Some(Self::Exclude)
332        } else if dev {
333            Some(Self::Include)
334        } else {
335            None
336        }
337    }
338
339    /// Returns the flag that was used to request development dependencies.
340    fn as_flag(self) -> &'static str {
341        match self {
342            Self::Exclude => "--no-dev",
343            Self::Include => "--dev",
344            Self::Only => "--only-dev",
345        }
346    }
347}
348
349#[derive(Debug, Clone)]
350pub enum IncludeGroups {
351    /// Include dependencies from the specified groups.
352    Some(Vec<GroupName>),
353    /// A marker indicates including dependencies from all groups.
354    All,
355}
356
357impl IncludeGroups {
358    /// Returns `true` if the specification includes the given group.
359    fn contains(&self, group: &GroupName) -> bool {
360        match self {
361            Self::Some(groups) => groups.contains(group),
362            Self::All => true,
363        }
364    }
365}
366
367impl Default for IncludeGroups {
368    fn default() -> Self {
369        Self::Some(Vec::new())
370    }
371}