use color_eyre::eyre::{self, WrapErr};
use std::collections::HashSet;
use std::path::PathBuf;
#[derive(Debug)]
pub enum Command {
FeatureMatrix {
pretty: bool,
},
Version,
Help,
}
#[derive(Debug, Default)]
#[allow(clippy::struct_excessive_bools)]
pub struct Options {
pub manifest_path: Option<PathBuf>,
pub packages: HashSet<String>,
pub exclude_packages: HashSet<String>,
pub command: Option<Command>,
pub only_packages_with_lib_target: bool,
pub silent: bool,
pub verbose: bool,
pub pedantic: bool,
pub errors_only: bool,
pub packages_only: bool,
pub fail_fast: bool,
}
pub trait ArgumentParser {
fn contains(&self, arg: &str) -> bool;
fn get_all(&self, arg: &str, has_value: bool)
-> Vec<(std::ops::RangeInclusive<usize>, String)>;
}
impl ArgumentParser for Vec<String> {
fn contains(&self, arg: &str) -> bool {
self.iter()
.any(|a| a == arg || a.starts_with(&format!("{arg}=")))
}
fn get_all(
&self,
arg: &str,
has_value: bool,
) -> Vec<(std::ops::RangeInclusive<usize>, String)> {
let mut matched = Vec::new();
for (idx, a) in self.iter().enumerate() {
match (a, self.get(idx + 1)) {
(key, Some(value)) if key == arg && has_value => {
matched.push((idx..=idx + 1, value.clone()));
}
(key, _) if key == arg && !has_value => {
matched.push((idx..=idx, key.clone()));
}
(key, _) if key.starts_with(&format!("{arg}=")) => {
let value = key.trim_start_matches(&format!("{arg}="));
matched.push((idx..=idx, value.to_string()));
}
_ => {}
}
}
matched.reverse();
matched
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) enum CargoSubcommand {
Build,
Check,
Test,
Doc,
Run,
Other,
}
pub(crate) fn cargo_subcommand(args: &[impl AsRef<str>]) -> CargoSubcommand {
let args: HashSet<&str> = args.iter().map(AsRef::as_ref).collect();
if args.contains("build") || args.contains("b") {
CargoSubcommand::Build
} else if args.contains("check") || args.contains("c") || args.contains("clippy") {
CargoSubcommand::Check
} else if args.contains("test") || args.contains("t") {
CargoSubcommand::Test
} else if args.contains("doc") || args.contains("d") {
CargoSubcommand::Doc
} else if args.contains("run") || args.contains("r") {
CargoSubcommand::Run
} else {
CargoSubcommand::Other
}
}
static VALID_BOOLS: [&str; 4] = ["yes", "true", "y", "t"];
const HELP_TEXT: &str = r#"Run cargo commands for all feature combinations
USAGE:
cargo fc [+toolchain] [SUBCOMMAND] [SUBCOMMAND_OPTIONS]
cargo fc [+toolchain] [OPTIONS] [CARGO_OPTIONS] [CARGO_SUBCOMMAND]
SUBCOMMAND:
matrix Print JSON feature combination matrix to stdout
--pretty Print pretty JSON
OPTIONS:
--help Print help information
--silent Hide cargo output and only show summary
--fail-fast Fail fast on the first bad feature combination
--errors-only Allow all warnings, show errors only (-Awarnings)
--exclude-package Exclude a package from feature combinations
--only-packages-with-lib-target
Only consider packages with a library target
--pedantic Treat warnings like errors in summary and
when using --fail-fast
Feature sets can be configured in your Cargo.toml configuration.
The following metadata key aliases are all supported:
[package.metadata.cargo-fc] (recommended)
[package.metadata.fc]
[package.metadata.cargo-feature-combinations]
[package.metadata.feature-combinations]
For example:
```toml
[package.metadata.cargo-fc]
# Exclude groupings of features that are incompatible or do not make sense
exclude_feature_sets = [ ["foo", "bar"], ] # formerly "skip_feature_sets"
# To exclude only the empty feature set from the matrix, you can either enable
# `no_empty_feature_set = true` or explicitly list an empty set here:
#
# exclude_feature_sets = [[]]
# Exclude features from the feature combination matrix
exclude_features = ["default", "full"] # formerly "denylist"
# Include features in the feature combination matrix
#
# These features will be added to every generated feature combination.
# This does not restrict which features are varied for the combinatorial
# matrix. To restrict the matrix to a specific allowlist of features, use
# `only_features`.
include_features = ["feature-that-must-always-be-set"]
# Only consider these features when generating the combinatorial matrix.
#
# When set, features not listed here are ignored for the combinatorial matrix.
# When empty, all package features are considered.
only_features = ["default", "full"]
# Skip implicit features that correspond to optional dependencies from the
# matrix.
#
# When enabled, the implicit features that Cargo generates for optional
# dependencies (of the form `foo = ["dep:foo"]` in the feature graph) are
# removed from the combinatorial matrix. This mirrors the behaviour of the
# `skip_optional_dependencies` flag in the `cargo-all-features` crate.
skip_optional_dependencies = true
# In the end, always add these exact combinations to the overall feature matrix,
# unless one is already present there.
#
# Non-existent features are ignored. Other configuration options are ignored.
include_feature_sets = [
["foo-a", "bar-a", "other-a"],
] # formerly "exact_combinations"
# Allow only the listed feature sets.
#
# When this list is non-empty, the feature matrix will consist exactly of the
# configured sets (after dropping non-existent features). No powerset is
# generated.
allow_feature_sets = [
["hydrate"],
["ssr"],
]
# When enabled, never include the empty feature set (no `--features`), even if
# it would otherwise be generated.
no_empty_feature_set = true
# When at least one isolated feature set is configured, stop taking all project
# features as a whole, and instead take them in these isolated sets. Build a
# sub-matrix for each isolated set, then merge sub-matrices into the overall
# feature matrix. If any two isolated sets produce an identical feature
# combination, such combination will be included in the overall matrix only once.
#
# This feature is intended for projects with large number of features, sub-sets
# of which are completely independent, and thus don't need cross-play.
#
# Other configuration options are still respected.
isolated_feature_sets = [
["foo-a", "foo-b", "foo-c"],
["bar-a", "bar-b"],
["other-a", "other-b", "other-c"],
]
```
Target-specific configuration can be expressed via Cargo-style `cfg(...)` selectors:
```toml
[package.metadata.cargo-fc]
exclude_features = ["default"]
[package.metadata.cargo-fc.target.'cfg(target_os = "linux")']
exclude_features = { add = ["metal"] }
```
Notes:
- Arrays in target overrides are always treated as overrides.
Use `{ add = [...] }` / `{ remove = [...] }` for additive changes.
- Patches are applied in order: override (or base), then remove, then add.
If a value appears in both `add` and `remove`, add wins.
- When multiple sections match, their `add`/`remove` sets are unioned.
Conflicting `override` values result in an error.
- `replace = true` starts from a fresh default config for that target.
When `replace = true` is set, patchable fields must not use `add`/`remove`.
- `cfg(feature = "...")` predicates are not supported in target override keys.
- If `--target <triple>` or `CARGO_BUILD_TARGET` is set, it is used to select
matching target overrides (this also applies to `cargo fc matrix`).
When using a cargo workspace, you can also exclude packages in your workspace `Cargo.toml`:
```toml
[workspace.metadata.cargo-fc]
# Exclude packages in the workspace metadata, or the metadata of the *root* package.
exclude_packages = ["package-a", "package-b"]
```
For more information, see 'https://github.com/romnn/cargo-feature-combinations'.
See 'cargo help <command>' for more information on a specific command.
"#;
pub(crate) fn print_help() {
println!("{HELP_TEXT}");
}
pub fn parse_arguments(bin_name: &str) -> eyre::Result<(Options, Vec<String>)> {
let mut args: Vec<String> = std::env::args_os()
.skip(1)
.skip_while(|arg| {
let arg = arg.as_os_str();
arg == bin_name || arg == "cargo"
})
.map(|s| s.to_string_lossy().to_string())
.collect();
let mut options = Options {
verbose: VALID_BOOLS.contains(
&std::env::var("VERBOSE")
.unwrap_or_default()
.to_lowercase()
.as_str(),
),
..Options::default()
};
for (span, manifest_path) in args.get_all("--manifest-path", true) {
let manifest_path = PathBuf::from(manifest_path);
let manifest_path = manifest_path
.canonicalize()
.wrap_err_with(|| format!("manifest {} does not exist", manifest_path.display()))?;
options.manifest_path = Some(manifest_path);
args.drain(span);
}
for flag in ["--package", "-p"] {
for (span, package) in args.get_all(flag, true) {
options.packages.insert(package);
args.drain(span);
}
}
for (span, package) in args.get_all("--exclude-package", true) {
options.exclude_packages.insert(package.trim().to_string());
args.drain(span);
}
for (span, _) in args.get_all("--only-packages-with-lib-target", false) {
options.only_packages_with_lib_target = true;
args.drain(span);
}
for (span, _) in args.get_all("matrix", false) {
options.command = Some(Command::FeatureMatrix { pretty: false });
args.drain(span);
}
for (span, _) in args.get_all("--pretty", false) {
if let Some(Command::FeatureMatrix { ref mut pretty }) = options.command {
*pretty = true;
}
args.drain(span);
}
for (span, _) in args.get_all("--help", false) {
options.command = Some(Command::Help);
args.drain(span);
}
for (span, _) in args.get_all("--version", false) {
options.command = Some(Command::Version);
args.drain(span);
}
for (span, _) in args.get_all("version", false) {
options.command = Some(Command::Version);
args.drain(span);
}
for (span, _) in args.get_all("--pedantic", false) {
options.pedantic = true;
args.drain(span);
}
for (span, _) in args.get_all("--errors-only", false) {
options.errors_only = true;
args.drain(span);
}
for (span, _) in args.get_all("--packages-only", false) {
options.packages_only = true;
args.drain(span);
}
for (span, _) in args.get_all("--silent", false) {
options.silent = true;
args.drain(span);
}
for (span, _) in args.get_all("--fail-fast", false) {
options.fail_fast = true;
args.drain(span);
}
for (span, _) in args.get_all("--workspace", false) {
args.drain(span);
}
Ok((options, args))
}