Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement --at-least-one-of #193

Merged
merged 2 commits into from
Sep 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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, &["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"])];
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