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}