cargo_feature_combinations/
cli.rs1use color_eyre::eyre::{self, WrapErr};
4use std::collections::HashSet;
5use std::path::PathBuf;
6
7#[derive(Debug)]
9pub enum Command {
10 FeatureMatrix {
15 pretty: bool,
17 },
18 Version,
20 Help,
22}
23
24#[derive(Debug, Default)]
29#[allow(clippy::struct_excessive_bools)]
30pub struct Options {
31 pub manifest_path: Option<PathBuf>,
33 pub packages: HashSet<String>,
35 pub exclude_packages: HashSet<String>,
37 pub command: Option<Command>,
39 pub only_packages_with_lib_target: bool,
41 pub summary_only: bool,
46 pub diagnostics_only: bool,
51 pub dedupe: bool,
56 pub verbose: bool,
58 pub pedantic: bool,
60 pub errors_only: bool,
62 pub packages_only: bool,
64 pub fail_fast: bool,
66 pub no_prune_implied: bool,
70 pub show_pruned: bool,
74}
75
76pub trait ArgumentParser {
78 fn contains(&self, arg: &str) -> bool;
81 fn get_all(&self, arg: &str, has_value: bool)
87 -> Vec<(std::ops::RangeInclusive<usize>, String)>;
88}
89
90impl ArgumentParser for Vec<String> {
91 fn contains(&self, arg: &str) -> bool {
92 self.iter()
93 .any(|a| a == arg || a.starts_with(&format!("{arg}=")))
94 }
95
96 fn get_all(
97 &self,
98 arg: &str,
99 has_value: bool,
100 ) -> Vec<(std::ops::RangeInclusive<usize>, String)> {
101 let mut matched = Vec::new();
102 for (idx, a) in self.iter().enumerate() {
103 match (a, self.get(idx + 1)) {
104 (key, Some(value)) if key == arg && has_value => {
105 matched.push((idx..=idx + 1, value.clone()));
106 }
107 (key, _) if key == arg && !has_value => {
108 matched.push((idx..=idx, key.clone()));
109 }
110 (key, _) if key.starts_with(&format!("{arg}=")) => {
111 let value = key.trim_start_matches(&format!("{arg}="));
112 matched.push((idx..=idx, value.to_string()));
113 }
114 _ => {}
115 }
116 }
117 matched.reverse();
118 matched
119 }
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
123pub(crate) enum CargoSubcommand {
124 Build,
125 Check,
126 Test,
127 Doc,
128 Run,
129 Other,
130}
131
132pub(crate) fn cargo_subcommand(args: &[impl AsRef<str>]) -> CargoSubcommand {
134 let args: HashSet<&str> = args.iter().map(AsRef::as_ref).collect();
135 if args.contains("build") || args.contains("b") {
136 CargoSubcommand::Build
137 } else if args.contains("check") || args.contains("c") || args.contains("clippy") {
138 CargoSubcommand::Check
139 } else if args.contains("test") || args.contains("t") {
140 CargoSubcommand::Test
141 } else if args.contains("doc") || args.contains("d") {
142 CargoSubcommand::Doc
143 } else if args.contains("run") || args.contains("r") {
144 CargoSubcommand::Run
145 } else {
146 CargoSubcommand::Other
147 }
148}
149
150static VALID_BOOLS: [&str; 4] = ["yes", "true", "y", "t"];
151
152const HELP_TEXT: &str = r#"Run cargo commands for all feature combinations
153
154USAGE:
155 cargo fc [+toolchain] [SUBCOMMAND] [SUBCOMMAND_OPTIONS]
156 cargo fc [+toolchain] [OPTIONS] [CARGO_OPTIONS] [CARGO_SUBCOMMAND]
157
158SUBCOMMAND:
159 matrix Print JSON feature combination matrix to stdout
160 --pretty Print pretty JSON
161
162OPTIONS:
163 --help Print help information
164 --diagnostics-only Show only diagnostics (warnings/errors) per
165 feature combination, suppressing build noise
166 --dedupe Like --diagnostics-only, but also deduplicate
167 identical diagnostics across feature combinations
168 --summary-only Hide cargo output and only show the final summary
169 --fail-fast Fail fast on the first bad feature combination
170 --errors-only Allow all warnings, show errors only (-Awarnings)
171 --exclude-package Exclude a package from feature combinations
172 --only-packages-with-lib-target
173 Only consider packages with a library target
174 --pedantic Treat warnings like errors in summary and
175 when using --fail-fast
176 --no-prune-implied Disable automatic pruning of redundant feature
177 combinations implied by other features
178 --show-pruned Show pruned feature combinations in the summary
179
180Feature sets can be configured in your Cargo.toml configuration.
181The following metadata key aliases are all supported:
182
183 [package.metadata.cargo-fc] (recommended)
184 [package.metadata.fc]
185 [package.metadata.cargo-feature-combinations]
186 [package.metadata.feature-combinations]
187
188For example:
189
190```toml
191[package.metadata.cargo-fc]
192
193# Exclude groupings of features that are incompatible or do not make sense
194exclude_feature_sets = [ ["foo", "bar"], ] # formerly "skip_feature_sets"
195
196# To exclude only the empty feature set from the matrix, you can either enable
197# `no_empty_feature_set = true` or explicitly list an empty set here:
198#
199# exclude_feature_sets = [[]]
200
201# Exclude features from the feature combination matrix
202exclude_features = ["default", "full"] # formerly "denylist"
203
204# Include features in the feature combination matrix
205#
206# These features will be added to every generated feature combination.
207# This does not restrict which features are varied for the combinatorial
208# matrix. To restrict the matrix to a specific allowlist of features, use
209# `only_features`.
210include_features = ["feature-that-must-always-be-set"]
211
212# Only consider these features when generating the combinatorial matrix.
213#
214# When set, features not listed here are ignored for the combinatorial matrix.
215# When empty, all package features are considered.
216only_features = ["default", "full"]
217
218# Skip implicit features that correspond to optional dependencies from the
219# matrix.
220#
221# When enabled, the implicit features that Cargo generates for optional
222# dependencies (of the form `foo = ["dep:foo"]` in the feature graph) are
223# removed from the combinatorial matrix. This mirrors the behaviour of the
224# `skip_optional_dependencies` flag in the `cargo-all-features` crate.
225skip_optional_dependencies = true
226
227# In the end, always add these exact combinations to the overall feature matrix,
228# unless one is already present there.
229#
230# Non-existent features are ignored. Other configuration options are ignored.
231include_feature_sets = [
232 ["foo-a", "bar-a", "other-a"],
233] # formerly "exact_combinations"
234
235# Allow only the listed feature sets.
236#
237# When this list is non-empty, the feature matrix will consist exactly of the
238# configured sets (after dropping non-existent features). No powerset is
239# generated.
240allow_feature_sets = [
241 ["hydrate"],
242 ["ssr"],
243]
244
245# When enabled, never include the empty feature set (no `--features`), even if
246# it would otherwise be generated.
247no_empty_feature_set = true
248
249# Automatically prune redundant feature combinations whose resolved feature
250# set (after Cargo's feature unification) matches a smaller combination.
251# Enabled by default. Disable with `prune_implied = false`.
252# prune_implied = true
253
254# When at least one isolated feature set is configured, stop taking all project
255# features as a whole, and instead take them in these isolated sets. Build a
256# sub-matrix for each isolated set, then merge sub-matrices into the overall
257# feature matrix. If any two isolated sets produce an identical feature
258# combination, such combination will be included in the overall matrix only once.
259#
260# This feature is intended for projects with large number of features, sub-sets
261# of which are completely independent, and thus don't need cross-play.
262#
263# Other configuration options are still respected.
264isolated_feature_sets = [
265 ["foo-a", "foo-b", "foo-c"],
266 ["bar-a", "bar-b"],
267 ["other-a", "other-b", "other-c"],
268]
269```
270
271Target-specific configuration can be expressed via Cargo-style `cfg(...)` selectors:
272
273```toml
274[package.metadata.cargo-fc]
275exclude_features = ["default"]
276
277[package.metadata.cargo-fc.target.'cfg(target_os = "linux")']
278exclude_features = { add = ["metal"] }
279```
280
281Notes:
282
283- Arrays in target overrides are always treated as overrides.
284 Use `{ add = [...] }` / `{ remove = [...] }` for additive changes.
285- Patches are applied in order: override (or base), then remove, then add.
286 If a value appears in both `add` and `remove`, add wins.
287- When multiple sections match, their `add`/`remove` sets are unioned.
288 Conflicting `override` values result in an error.
289- `replace = true` starts from a fresh default config for that target.
290 When `replace = true` is set, patchable fields must not use `add`/`remove`.
291- `cfg(feature = "...")` predicates are not supported in target override keys.
292- If `--target <triple>` or `CARGO_BUILD_TARGET` is set, it is used to select
293 matching target overrides (this also applies to `cargo fc matrix`).
294
295When using a cargo workspace, you can also exclude packages in your workspace `Cargo.toml`:
296
297```toml
298[workspace.metadata.cargo-fc]
299# Exclude packages in the workspace metadata, or the metadata of the *root* package.
300exclude_packages = ["package-a", "package-b"]
301```
302
303For more information, see 'https://github.com/romnn/cargo-feature-combinations'.
304
305See 'cargo help <command>' for more information on a specific command.
306"#;
307
308pub(crate) fn print_help() {
310 println!("{HELP_TEXT}");
311}
312
313pub fn parse_arguments(bin_name: &str) -> eyre::Result<(Options, Vec<String>)> {
323 let mut args: Vec<String> = std::env::args_os()
324 .skip(1)
326 .skip_while(|arg| {
328 let arg = arg.as_os_str();
329 arg == bin_name || arg == "cargo"
330 })
331 .map(|s| s.to_string_lossy().to_string())
332 .collect();
333
334 let mut options = Options {
335 verbose: VALID_BOOLS.contains(
336 &std::env::var("VERBOSE")
337 .unwrap_or_default()
338 .to_lowercase()
339 .as_str(),
340 ),
341 ..Options::default()
342 };
343
344 for (span, manifest_path) in args.get_all("--manifest-path", true) {
346 let manifest_path = PathBuf::from(manifest_path);
347 let manifest_path = manifest_path
348 .canonicalize()
349 .wrap_err_with(|| format!("manifest {} does not exist", manifest_path.display()))?;
350 options.manifest_path = Some(manifest_path);
351 args.drain(span);
352 }
353
354 for flag in ["--package", "-p"] {
356 for (span, package) in args.get_all(flag, true) {
357 options.packages.insert(package);
358 args.drain(span);
359 }
360 }
361
362 for (span, package) in args.get_all("--exclude-package", true) {
363 options.exclude_packages.insert(package.trim().to_string());
364 args.drain(span);
365 }
366
367 for (span, _) in args.get_all("--only-packages-with-lib-target", false) {
368 options.only_packages_with_lib_target = true;
369 args.drain(span);
370 }
371
372 for (span, _) in args.get_all("matrix", false) {
374 options.command = Some(Command::FeatureMatrix { pretty: false });
375 args.drain(span);
376 }
377 for (span, _) in args.get_all("--pretty", false) {
379 if let Some(Command::FeatureMatrix { ref mut pretty }) = options.command {
380 *pretty = true;
381 }
382 args.drain(span);
383 }
384
385 for (span, _) in args.get_all("--help", false) {
387 options.command = Some(Command::Help);
388 args.drain(span);
389 }
390
391 for (span, _) in args.get_all("--version", false) {
393 options.command = Some(Command::Version);
394 args.drain(span);
395 }
396
397 for (span, _) in args.get_all("version", false) {
399 options.command = Some(Command::Version);
400 args.drain(span);
401 }
402
403 let mut drain_flag = |flag: &str, field: &mut bool| {
404 for (span, _) in args.get_all(flag, false) {
405 *field = true;
406 args.drain(span);
407 }
408 };
409 drain_flag("--pedantic", &mut options.pedantic);
410 drain_flag("--errors-only", &mut options.errors_only);
411 drain_flag("--packages-only", &mut options.packages_only);
412 drain_flag("--diagnostics-only", &mut options.diagnostics_only);
413 drain_flag("--fail-fast", &mut options.fail_fast);
414 drain_flag("--no-prune-implied", &mut options.no_prune_implied);
415 drain_flag("--show-pruned", &mut options.show_pruned);
416
417 for flag in ["--dedupe", "--dedup"] {
419 for (span, _) in args.get_all(flag, false) {
420 options.dedupe = true;
421 options.diagnostics_only = true;
422 args.drain(span);
423 }
424 }
425
426 for flag in ["--summary-only", "--summary", "--silent"] {
428 for (span, _) in args.get_all(flag, false) {
429 options.summary_only = true;
430 args.drain(span);
431 }
432 }
433
434 for (span, _) in args.get_all("--workspace", false) {
441 args.drain(span);
442 }
443
444 Ok((options, args))
445}