1//! Cargo flags for selecting crates in a workspace.
2
3#[derive(Default, Clone, Debug, PartialEq, Eq)]
4#[cfg_attr(feature = "clap", derive(clap::Args))]
5#[non_exhaustive]
6pub struct Workspace {
7 #[cfg_attr(feature = "clap", arg(short, long, value_name = "SPEC"))]
8 /// Package to process (see `cargo help pkgid`)
9 pub package: Vec<String>,
10 #[cfg_attr(feature = "clap", arg(long))]
11 /// Process all packages in the workspace
12 pub workspace: bool,
13 #[cfg_attr(
14 feature = "clap",
15 arg(long, hide_short_help(true), hide_long_help(true))
16 )]
17 /// Process all packages in the workspace
18 pub all: bool,
19 #[cfg_attr(feature = "clap", arg(long, value_name = "SPEC"))]
20 /// Exclude packages from being processed
21 pub exclude: Vec<String>,
22}
23
24#[cfg(feature = "cargo_metadata")]
25impl Workspace {
26 /// Partition workspace members into those selected and those excluded.
27 ///
28 /// Notes:
29 /// - Requires the features `cargo_metadata`.
30 /// - Requires not calling `MetadataCommand::no_deps`
31 pub fn partition_packages<'m>(
32 &self,
33 meta: &'m cargo_metadata::Metadata,
34 ) -> (
35 Vec<&'m cargo_metadata::Package>,
36 Vec<&'m cargo_metadata::Package>,
37 ) {
38 let selection =
39 Packages::from_flags(self.workspace || self.all, &self.exclude, &self.package);
40 let workspace_members: std::collections::HashSet<_> =
41 meta.workspace_members.iter().collect();
42 let base_ids: std::collections::HashSet<_> = match selection {
43 Packages::Default => {
44 // Deviating from cargo because Metadata doesn't have default members
45 let resolve = meta.resolve.as_ref().expect("no-deps is unsupported");
46 match &resolve.root {
47 Some(root) => {
48 let mut base_ids = std::collections::HashSet::new();
49 base_ids.insert(root);
50 base_ids
51 }
52 None => workspace_members,
53 }
54 }
55 Packages::All => workspace_members,
56 Packages::OptOut(_) => workspace_members, // Deviating from cargo by only checking workspace members
57 Packages::Packages(patterns) => {
58 meta.packages
59 .iter()
60 // Deviating from cargo by not supporting patterns
61 // Deviating from cargo by only checking workspace members
62 .filter(|p| workspace_members.contains(&p.id) && patterns.contains(&p.name))
63 .map(|p| &p.id)
64 .collect()
65 }
66 };
67
68 meta.packages
69 .iter()
70 // Deviating from cargo by not supporting patterns
71 .partition(|p| base_ids.contains(&p.id) && !self.exclude.contains(&p.name))
72 }
73}
74
75// See cargo's src/cargo/ops/cargo_compile.rs
76#[derive(Clone, PartialEq, Eq, Debug)]
77#[cfg(feature = "cargo_metadata")]
78#[allow(clippy::enum_variant_names)]
79enum Packages<'p> {
80 Default,
81 All,
82 OptOut(&'p [String]),
83 Packages(&'p [String]),
84}
85
86#[cfg(feature = "cargo_metadata")]
87impl<'p> Packages<'p> {
88 pub fn from_flags(all: bool, exclude: &'p [String], package: &'p [String]) -> Self {
89 match (all, exclude.len(), package.len()) {
90 (false, 0, 0) => Packages::Default,
91 (false, 0, _) => Packages::Packages(package),
92 (false, _, 0) => Packages::OptOut(exclude), // Deviating from cargo because we don't do error handling
93 (false, _, _) => Packages::Packages(package), // Deviating from cargo because we don't do error handling
94 (true, 0, _) => Packages::All,
95 (true, _, _) => Packages::OptOut(exclude),
96 }
97 }
98}
99
100#[cfg(test)]
101mod test {
102 use super::*;
103
104 #[test]
105 #[cfg(feature = "clap")]
106 fn verify_app() {
107 #[derive(Debug, clap::Parser)]
108 struct Cli {
109 #[command(flatten)]
110 workspace: Workspace,
111 }
112
113 use clap::CommandFactory;
114 Cli::command().debug_assert()
115 }
116
117 #[test]
118 #[cfg(feature = "clap")]
119 fn parse_multiple_occurrences() {
120 use clap::Parser;
121
122 #[derive(PartialEq, Eq, Debug, Parser)]
123 struct Args {
124 positional: Option<String>,
125 #[command(flatten)]
126 workspace: Workspace,
127 }
128
129 assert_eq!(
130 Args {
131 positional: None,
132 workspace: Workspace {
133 package: vec![],
134 workspace: false,
135 all: false,
136 exclude: vec![],
137 }
138 },
139 Args::parse_from(["test"])
140 );
141 assert_eq!(
142 Args {
143 positional: Some("baz".to_owned()),
144 workspace: Workspace {
145 package: vec!["foo".to_owned(), "bar".to_owned()],
146 workspace: false,
147 all: false,
148 exclude: vec![],
149 }
150 },
151 Args::parse_from(["test", "--package", "foo", "--package", "bar", "baz"])
152 );
153 assert_eq!(
154 Args {
155 positional: Some("baz".to_owned()),
156 workspace: Workspace {
157 package: vec![],
158 workspace: false,
159 all: false,
160 exclude: vec!["foo".to_owned(), "bar".to_owned()],
161 }
162 },
163 Args::parse_from(["test", "--exclude", "foo", "--exclude", "bar", "baz"])
164 );
165 }
166
167 #[cfg(feature = "cargo_metadata")]
168 #[cfg(test)]
169 mod partition_default {
170 use super::*;
171
172 #[test]
173 fn single_crate() {
174 let mut metadata = cargo_metadata::MetadataCommand::new();
175 metadata.manifest_path("tests/fixtures/simple/Cargo.toml");
176 let metadata = metadata.exec().unwrap();
177
178 let workspace = Workspace {
179 ..Default::default()
180 };
181 let (included, excluded) = workspace.partition_packages(&metadata);
182 assert_eq!(included.len(), 1);
183 assert_eq!(excluded.len(), 0);
184 }
185
186 #[test]
187 fn mixed_ws_root() {
188 let mut metadata = cargo_metadata::MetadataCommand::new();
189 metadata.manifest_path("tests/fixtures/mixed_ws/Cargo.toml");
190 let metadata = metadata.exec().unwrap();
191
192 let workspace = Workspace {
193 ..Default::default()
194 };
195 let (included, excluded) = workspace.partition_packages(&metadata);
196 assert_eq!(included.len(), 1);
197 assert_eq!(excluded.len(), 2);
198 }
199
200 #[test]
201 fn mixed_ws_leaf() {
202 let mut metadata = cargo_metadata::MetadataCommand::new();
203 metadata.manifest_path("tests/fixtures/mixed_ws/c/Cargo.toml");
204 let metadata = metadata.exec().unwrap();
205
206 let workspace = Workspace {
207 ..Default::default()
208 };
209 let (included, excluded) = workspace.partition_packages(&metadata);
210 assert_eq!(included.len(), 1);
211 assert_eq!(excluded.len(), 2);
212 }
213
214 #[test]
215 fn pure_ws_root() {
216 let mut metadata = cargo_metadata::MetadataCommand::new();
217 metadata.manifest_path("tests/fixtures/pure_ws/Cargo.toml");
218 let metadata = metadata.exec().unwrap();
219
220 let workspace = Workspace {
221 ..Default::default()
222 };
223 let (included, excluded) = workspace.partition_packages(&metadata);
224 assert_eq!(included.len(), 3);
225 assert_eq!(excluded.len(), 0);
226 }
227
228 #[test]
229 fn pure_ws_leaf() {
230 let mut metadata = cargo_metadata::MetadataCommand::new();
231 metadata.manifest_path("tests/fixtures/pure_ws/c/Cargo.toml");
232 let metadata = metadata.exec().unwrap();
233
234 let workspace = Workspace {
235 ..Default::default()
236 };
237 let (included, excluded) = workspace.partition_packages(&metadata);
238 assert_eq!(included.len(), 1);
239 assert_eq!(excluded.len(), 2);
240 }
241 }
242
243 #[cfg(feature = "cargo_metadata")]
244 #[cfg(test)]
245 mod partition_all {
246 use super::*;
247
248 #[test]
249 fn single_crate() {
250 let mut metadata = cargo_metadata::MetadataCommand::new();
251 metadata.manifest_path("tests/fixtures/simple/Cargo.toml");
252 let metadata = metadata.exec().unwrap();
253
254 let workspace = Workspace {
255 all: true,
256 ..Default::default()
257 };
258 let (included, excluded) = workspace.partition_packages(&metadata);
259 assert_eq!(included.len(), 1);
260 assert_eq!(excluded.len(), 0);
261 }
262
263 #[test]
264 fn mixed_ws_root() {
265 let mut metadata = cargo_metadata::MetadataCommand::new();
266 metadata.manifest_path("tests/fixtures/mixed_ws/Cargo.toml");
267 let metadata = metadata.exec().unwrap();
268
269 let workspace = Workspace {
270 all: true,
271 ..Default::default()
272 };
273 let (included, excluded) = workspace.partition_packages(&metadata);
274 assert_eq!(included.len(), 3);
275 assert_eq!(excluded.len(), 0);
276 }
277
278 #[test]
279 fn mixed_ws_leaf() {
280 let mut metadata = cargo_metadata::MetadataCommand::new();
281 metadata.manifest_path("tests/fixtures/mixed_ws/c/Cargo.toml");
282 let metadata = metadata.exec().unwrap();
283
284 let workspace = Workspace {
285 all: true,
286 ..Default::default()
287 };
288 let (included, excluded) = workspace.partition_packages(&metadata);
289 assert_eq!(included.len(), 3);
290 assert_eq!(excluded.len(), 0);
291 }
292
293 #[test]
294 fn pure_ws_root() {
295 let mut metadata = cargo_metadata::MetadataCommand::new();
296 metadata.manifest_path("tests/fixtures/pure_ws/Cargo.toml");
297 let metadata = metadata.exec().unwrap();
298
299 let workspace = Workspace {
300 all: true,
301 ..Default::default()
302 };
303 let (included, excluded) = workspace.partition_packages(&metadata);
304 assert_eq!(included.len(), 3);
305 assert_eq!(excluded.len(), 0);
306 }
307
308 #[test]
309 fn pure_ws_leaf() {
310 let mut metadata = cargo_metadata::MetadataCommand::new();
311 metadata.manifest_path("tests/fixtures/pure_ws/c/Cargo.toml");
312 let metadata = metadata.exec().unwrap();
313
314 let workspace = Workspace {
315 all: true,
316 ..Default::default()
317 };
318 let (included, excluded) = workspace.partition_packages(&metadata);
319 assert_eq!(included.len(), 3);
320 assert_eq!(excluded.len(), 0);
321 }
322 }
323
324 #[cfg(feature = "cargo_metadata")]
325 #[cfg(test)]
326 mod partition_package {
327 use super::*;
328
329 #[test]
330 fn single_crate() {
331 let mut metadata = cargo_metadata::MetadataCommand::new();
332 metadata.manifest_path("tests/fixtures/simple/Cargo.toml");
333 let metadata = metadata.exec().unwrap();
334
335 let workspace = Workspace {
336 package: vec!["simple".to_owned()],
337 ..Default::default()
338 };
339 let (included, excluded) = workspace.partition_packages(&metadata);
340 assert_eq!(included.len(), 1);
341 assert_eq!(excluded.len(), 0);
342 }
343
344 #[test]
345 fn mixed_ws_root() {
346 let mut metadata = cargo_metadata::MetadataCommand::new();
347 metadata.manifest_path("tests/fixtures/mixed_ws/Cargo.toml");
348 let metadata = metadata.exec().unwrap();
349
350 let workspace = Workspace {
351 package: vec!["a".to_owned()],
352 ..Default::default()
353 };
354 let (included, excluded) = workspace.partition_packages(&metadata);
355 assert_eq!(included.len(), 1);
356 assert_eq!(excluded.len(), 2);
357 }
358
359 #[test]
360 fn mixed_ws_leaf() {
361 let mut metadata = cargo_metadata::MetadataCommand::new();
362 metadata.manifest_path("tests/fixtures/mixed_ws/c/Cargo.toml");
363 let metadata = metadata.exec().unwrap();
364
365 let workspace = Workspace {
366 package: vec!["a".to_owned()],
367 ..Default::default()
368 };
369 let (included, excluded) = workspace.partition_packages(&metadata);
370 assert_eq!(included.len(), 1);
371 assert_eq!(excluded.len(), 2);
372 }
373
374 #[test]
375 fn pure_ws_root() {
376 let mut metadata = cargo_metadata::MetadataCommand::new();
377 metadata.manifest_path("tests/fixtures/pure_ws/Cargo.toml");
378 let metadata = metadata.exec().unwrap();
379
380 let workspace = Workspace {
381 package: vec!["a".to_owned()],
382 ..Default::default()
383 };
384 let (included, excluded) = workspace.partition_packages(&metadata);
385 assert_eq!(included.len(), 1);
386 assert_eq!(excluded.len(), 2);
387 }
388
389 #[test]
390 fn pure_ws_leaf() {
391 let mut metadata = cargo_metadata::MetadataCommand::new();
392 metadata.manifest_path("tests/fixtures/pure_ws/c/Cargo.toml");
393 let metadata = metadata.exec().unwrap();
394
395 let workspace = Workspace {
396 package: vec!["a".to_owned()],
397 ..Default::default()
398 };
399 let (included, excluded) = workspace.partition_packages(&metadata);
400 assert_eq!(included.len(), 1);
401 assert_eq!(excluded.len(), 2);
402 }
403 }
404}
405