Skip to content

Commit

Permalink
Implement --at-least-one-of
Browse files Browse the repository at this point in the history
  • Loading branch information
kornelski committed Sep 4, 2023
1 parent ab4c446 commit 94e6832
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 20 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,15 @@ OPTIONS:

This flag can only be used together with --feature-powerset flag.

--at-least-one-of <FEATURES>...
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 <FEATURES>...
Include only the specified features in the feature combinations instead of package
features.
Expand Down
58 changes: 43 additions & 15 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ pub(crate) struct Args {
pub(crate) depth: Option<usize>,
/// --group-features <FEATURES>...
pub(crate) group_features: Vec<Feature>,
/// --at-least-one-of <FEATURES>...
/// Implies --exclude-no-default-features. Can be specified multiple times.
pub(crate) at_least_one_of: Vec<Feature>,

// options that will be propagated to cargo
/// --features <FEATURES>...
Expand Down Expand Up @@ -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![];
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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"])?;
Expand All @@ -410,21 +423,8 @@ impl Args {
}

let depth = depth.as_deref().map(str::parse::<usize>).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 {
Expand Down Expand Up @@ -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,
Expand All @@ -586,6 +587,28 @@ impl Args {
}
}

fn parse_grouped_features(
group_features: &[String],
option_name: &str,
) -> Result<Vec<Feature>, 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() {
Expand Down Expand Up @@ -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", "<FEATURES>...", "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",
Expand Down
80 changes: 76 additions & 4 deletions src/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,12 @@ impl AsRef<str> for Feature {
pub(crate) fn feature_powerset<'a>(
features: impl IntoIterator<Item = &'a Feature>,
depth: Option<usize>,
map: &BTreeMap<String, Vec<String>>,
at_least_one_of: &[Feature],
package_features: &BTreeMap<String, Vec<String>>,
) -> Vec<Vec<&'a Feature>> {
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.
Expand All @@ -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()
}

Expand Down Expand Up @@ -237,11 +249,44 @@ fn powerset<T: Copy>(iter: impl IntoIterator<Item = T>, depth: Option<usize>) ->
})
}

// 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<BTreeSet<&'a str>> {
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::<BTreeSet<_>>()
})
.filter(|set| !set.is_empty())
.collect::<Vec<_>>()
}

#[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),* $(,)?) => {
Expand All @@ -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<Feature> = 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, &v!["a"], &map);
assert_eq!(filtered, vec![vec!["a"], vec!["b"], vec!["c"], vec!["d"], vec!["c", "d"]]);

let filtered = feature_powerset(&list, None, &v!["c"], &map);
assert_eq!(filtered, vec![vec!["c"], vec!["c", "d"]]);

let filtered = feature_powerset(&list, None, &v!["a", "c"], &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"])];
Expand Down Expand Up @@ -291,7 +363,7 @@ mod tests {
vec!["b", "c", "d"],
vec!["a", "b", "c", "d"],
]);
let filtered = feature_powerset(list.iter().collect::<Vec<_>>(), None, &map);
let filtered = feature_powerset(list.iter().collect::<Vec<_>>(), None, &[], &map);
assert_eq!(filtered, vec![vec!["a"], vec!["b"], vec!["c"], vec!["d"], vec!["c", "d"]]);
}

Expand Down
3 changes: 2 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
9 changes: 9 additions & 0 deletions tests/long-help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,15 @@ OPTIONS:

This flag can only be used together with --feature-powerset flag.

--at-least-one-of <FEATURES>...
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 <FEATURES>...
Include only the specified features in the feature combinations instead of package
features.
Expand Down
2 changes: 2 additions & 0 deletions tests/short-help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ OPTIONS:
--depth <NUM> Specify a max number of simultaneous feature flags of
--feature-powerset
--group-features <FEATURES>... Space or comma separated list of features to group
--at-least-one-of <FEATURES>... Space or comma separated list of features. Skips sets of
features that don't enable any of the features listed
--include-features <FEATURES>... Include only the specified features in the feature
combinations instead of package features
--no-dev-deps Perform without dev-dependencies
Expand Down

0 comments on commit 94e6832

Please sign in to comment.