From adce4dab8a6bf2256e6ddeb25d2806f076eb0605 Mon Sep 17 00:00:00 2001 From: Kornel Date: Fri, 1 Sep 2023 16:22:02 +0100 Subject: [PATCH] Implement --at-least-one-of Fixes #182 --- README.md | 9 +++++ src/cli.rs | 58 +++++++++++++++++++++++--------- src/features.rs | 80 +++++++++++++++++++++++++++++++++++++++++--- src/main.rs | 3 +- tests/long-help.txt | 9 +++++ tests/short-help.txt | 2 ++ 6 files changed, 141 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 2b9f7333..4bccb550 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,15 @@ OPTIONS: This flag can only be used together with --feature-powerset flag. + --at-least-one-of ... + Space or comma separated list of features. Skips sets of features that don't enable any + of the features listed. + + To specify multiple groups, use this option multiple times: `--at-least-one-of a,b + --at-least-one-of c,d` + + This flag can only be used together with --feature-powerset flag. + --include-features ... Include only the specified features in the feature combinations instead of package features. diff --git a/src/cli.rs b/src/cli.rs index 6838e1ed..df8c8126 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -78,6 +78,9 @@ pub(crate) struct Args { pub(crate) depth: Option, /// --group-features ... pub(crate) group_features: Vec, + /// --at-least-one-of ... + /// Implies --exclude-no-default-features. Can be specified multiple times. + pub(crate) at_least_one_of: Vec, // options that will be propagated to cargo /// --features ... @@ -151,6 +154,7 @@ impl Args { let mut optional_deps = None; let mut include_features = vec![]; + let mut at_least_one_of = vec![]; let mut include_deps_features = false; let mut exclude_features = vec![]; @@ -277,6 +281,7 @@ impl Args { Long("remove-dev-deps") => parse_flag!(remove_dev_deps), Long("each-feature") => parse_flag!(each_feature), Long("feature-powerset") => parse_flag!(feature_powerset), + Long("at-least-one-of") => at_least_one_of.push(parser.value()?.parse()?), Long("no-private") => parse_flag!(no_private), Long("ignore-private") => parse_flag!(ignore_private), Long("exclude-no-default-features") => parse_flag!(exclude_no_default_features), @@ -391,8 +396,16 @@ impl Args { requires("--include-features", &["--each-feature", "--feature-powerset"])?; } else if include_deps_features { requires("--include-deps-features", &["--each-feature", "--feature-powerset"])?; + } else if !at_least_one_of.is_empty() { + requires("--at-least-one-of", &["--feature-powerset"])?; } } + + if !at_least_one_of.is_empty() { + // there will always be a feature set + exclude_no_default_features = true; + } + if !feature_powerset { if depth.is_some() { requires("--depth", &["--feature-powerset"])?; @@ -410,21 +423,8 @@ impl Args { } let depth = depth.as_deref().map(str::parse::).transpose()?; - let group_features = - group_features.iter().try_fold(Vec::with_capacity(group_features.len()), |mut v, g| { - let g = if g.contains(',') { - g.split(',') - } else if g.contains(' ') { - g.split(' ') - } else { - bail!( - "--group-features requires a list of two or more features separated by space \ - or comma" - ); - }; - v.push(Feature::group(g)); - Ok(v) - })?; + let group_features = parse_grouped_features(&group_features, "group-features")?; + let at_least_one_of = parse_grouped_features(&at_least_one_of, "at-least-one-of")?; if let Some(subcommand) = subcommand.as_deref() { match subcommand { @@ -567,6 +567,7 @@ impl Args { print_command_list, no_manifest_path, include_features: include_features.into_iter().map(Into::into).collect(), + at_least_one_of, include_deps_features, version_range, version_step, @@ -586,6 +587,28 @@ impl Args { } } +fn parse_grouped_features( + group_features: &[String], + option_name: &str, +) -> Result, anyhow::Error> { + let group_features = + group_features.iter().try_fold(Vec::with_capacity(group_features.len()), |mut v, g| { + let g = if g.contains(',') { + g.split(',') + } else if g.contains(' ') { + g.split(' ') + } else { + bail!( + "--{option_name} requires a list of two or more features separated by space \ + or comma" + ); + }; + v.push(Feature::group(g)); + Ok(v) + })?; + Ok(group_features) +} + fn has_z_flag(args: &[String], name: &str) -> bool { let mut iter = args.iter().map(String::as_str); while let Some(mut arg) = iter.next() { @@ -668,6 +691,11 @@ const HELP: &[HelpText<'_>] = &[ --group-features c,d`", "This flag can only be used together with --feature-powerset flag.", ]), + ("", "--at-least-one-of", "...", "Space or comma separated list of features. Skips sets of features that don't enable any of the features listed", &[ + "To specify multiple groups, use this option multiple times: `--at-least-one-of a,b \ + --at-least-one-of c,d`", + "This flag can only be used together with --feature-powerset flag.", + ]), ( "", "--include-features", diff --git a/src/features.rs b/src/features.rs index 935f7251..640b2649 100644 --- a/src/features.rs +++ b/src/features.rs @@ -178,9 +178,12 @@ impl AsRef for Feature { pub(crate) fn feature_powerset<'a>( features: impl IntoIterator, depth: Option, - map: &BTreeMap>, + at_least_one_of: &[Feature], + package_features: &BTreeMap>, ) -> Vec> { - let deps_map = feature_deps(map); + let deps_map = feature_deps(package_features); + let at_least_one_of = at_least_one_of_for_package(at_least_one_of, &deps_map); + powerset(features, depth) .into_iter() .skip(1) // The first element of a powerset is `[]` so it should be skipped. @@ -191,6 +194,15 @@ pub(crate) fn feature_powerset<'a>( }) }) }) + .filter(move |fs| { + // all() returns true if at_least_one_of is empty + at_least_one_of.iter().all(|required_set| { + fs + .iter() + .flat_map(|f| f.as_group()) + .any(|f| required_set.contains(f.as_str())) + }) + }) .collect() } @@ -237,11 +249,44 @@ fn powerset(iter: impl IntoIterator, depth: Option) -> }) } +// Leave only features that are possible to enable in the package. +pub(crate) fn at_least_one_of_for_package<'a>( + at_least_one_of: &[Feature], + package_features_flattened: &BTreeMap<&'a str, BTreeSet<&'a str>>, +) -> Vec> { + if at_least_one_of.is_empty() { + return vec![]; + } + + let mut all_features_enabled_by = BTreeMap::new(); + for (&enabled_by, enables) in package_features_flattened { + all_features_enabled_by.entry(enabled_by).or_insert_with(BTreeSet::new).insert(enabled_by); + for &enabled_feature in enables { + all_features_enabled_by + .entry(enabled_feature) + .or_insert_with(BTreeSet::new) + .insert(enabled_by); + } + } + + at_least_one_of + .iter() + .map(|set| { + set.as_group() + .iter() + .filter_map(|f| all_features_enabled_by.get(f.as_str())) + .flat_map(|f| f.iter().copied()) + .collect::>() + }) + .filter(|set| !set.is_empty()) + .collect::>() +} + #[cfg(test)] mod tests { use std::collections::{BTreeMap, BTreeSet}; - use super::{feature_deps, feature_powerset, powerset, Feature}; + use super::{at_least_one_of_for_package, feature_deps, feature_powerset, powerset, Feature}; macro_rules! v { ($($expr:expr),* $(,)?) => { @@ -261,6 +306,33 @@ mod tests { }; } + #[test] + fn at_least_one_of_for_package_filter() { + let map = map![("a", v![]), ("b", v!["a"]), ("c", v!["b"]), ("d", v!["a", "b"])]; + let fd = feature_deps(&map); + let list: Vec = v!["b", "x", "y", "z"]; + let filtered = at_least_one_of_for_package(&list, &fd); + assert_eq!(filtered, vec![set!("b", "c", "d")]); + } + + #[test] + fn powerset_with_filter() { + let map = map![("a", v![]), ("b", v!["a"]), ("c", v!["b"]), ("d", v!["a", "b"])]; + + let list = v!["a", "b", "c", "d"]; + let filtered = feature_powerset(&list, None, &[], &map); + assert_eq!(filtered, vec![vec!["a"], vec!["b"], vec!["c"], vec!["d"], vec!["c", "d"]]); + + let filtered = feature_powerset(&list, None, &["a".into()], &map); + assert_eq!(filtered, vec![vec!["a"], vec!["b"], vec!["c"], vec!["d"], vec!["c", "d"]]); + + let filtered = feature_powerset(&list, None, &["c".into()], &map); + assert_eq!(filtered, vec![vec!["c"], vec!["c", "d"]]); + + let filtered = feature_powerset(&list, None, &["a".into(), "c".into()], &map); + assert_eq!(filtered, vec![vec!["c"], vec!["c", "d"]]); + } + #[test] fn feature_deps1() { let map = map![("a", v![]), ("b", v!["a"]), ("c", v!["b"]), ("d", v!["a", "b"])]; @@ -291,7 +363,7 @@ mod tests { vec!["b", "c", "d"], vec!["a", "b", "c", "d"], ]); - let filtered = feature_powerset(list.iter().collect::>(), None, &map); + let filtered = feature_powerset(list.iter().collect::>(), None, &[], &map); assert_eq!(filtered, vec![vec!["a"], vec!["b"], vec!["c"], vec!["d"], vec!["c", "d"]]); } diff --git a/src/main.rs b/src/main.rs index f9475845..448fa76a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -237,7 +237,8 @@ fn determine_kind<'a>( Kind::Each { features } } } else if cx.feature_powerset { - let features = features::feature_powerset(features, cx.depth, &package.features); + let features = + features::feature_powerset(features, cx.depth, &cx.at_least_one_of, &package.features); if (pkg_features.normal().is_empty() && pkg_features.optional_deps().is_empty() || !cx.include_features.is_empty()) diff --git a/tests/long-help.txt b/tests/long-help.txt index dbf9542f..a1a0015f 100644 --- a/tests/long-help.txt +++ b/tests/long-help.txt @@ -97,6 +97,15 @@ OPTIONS: This flag can only be used together with --feature-powerset flag. + --at-least-one-of ... + Space or comma separated list of features. Skips sets of features that don't enable any + of the features listed. + + To specify multiple groups, use this option multiple times: `--at-least-one-of a,b + --at-least-one-of c,d` + + This flag can only be used together with --feature-powerset flag. + --include-features ... Include only the specified features in the feature combinations instead of package features. diff --git a/tests/short-help.txt b/tests/short-help.txt index b364fe61..2c5ba961 100644 --- a/tests/short-help.txt +++ b/tests/short-help.txt @@ -23,6 +23,8 @@ OPTIONS: --depth Specify a max number of simultaneous feature flags of --feature-powerset --group-features ... Space or comma separated list of features to group + --at-least-one-of ... Space or comma separated list of features. Skips sets of + features that don't enable any of the features listed --include-features ... Include only the specified features in the feature combinations instead of package features --no-dev-deps Perform without dev-dependencies