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}