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 1, 2023
1 parent 611ca92 commit a1eae6d
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 23 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: Vec<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
47 changes: 41 additions & 6 deletions src/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,8 @@ 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>>,
deps_map: &BTreeMap<&str, BTreeSet<&str>>,
) -> Vec<Vec<&'a Feature>> {
let deps_map = feature_deps(map);
powerset(features, depth)
.into_iter()
.skip(1) // The first element of a powerset is `[]` so it should be skipped.
Expand All @@ -181,7 +180,9 @@ pub(crate) fn feature_powerset<'a>(
.collect()
}

fn feature_deps(map: &BTreeMap<String, Vec<String>>) -> BTreeMap<&str, BTreeSet<&str>> {
pub(crate) fn flatten_features(
map: &BTreeMap<String, Vec<String>>,
) -> BTreeMap<&str, BTreeSet<&str>> {
fn f<'a>(
map: &'a BTreeMap<String, Vec<String>>,
set: &mut BTreeSet<&'a str>,
Expand Down Expand Up @@ -220,11 +221,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::{feature_powerset, flatten_features, powerset, Feature};

macro_rules! v {
($($expr:expr),* $(,)?) => {
Expand All @@ -247,7 +281,7 @@ mod tests {
#[test]
fn feature_deps1() {
let map = map![("a", v![]), ("b", v!["a"]), ("c", v!["b"]), ("d", v!["a", "b"])];
let fd = feature_deps(&map);
let fd = flatten_features(&map);
assert_eq!(fd, map![
("a", set![]),
("b", set!["a"]),
Expand All @@ -274,7 +308,8 @@ 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, &flatten_features(&map));
assert_eq!(filtered, vec![vec!["a"], vec!["b"], vec!["c"], vec!["d"], vec!["c", "d"]]);
}

Expand Down
22 changes: 20 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ use std::{
use anyhow::{bail, Result};

use crate::{
context::Context, features::Feature, metadata::PackageId, process::ProcessBuilder,
context::Context,
features::{flatten_features, Feature},
metadata::PackageId,
process::ProcessBuilder,
rustup::Rustup,
};

Expand Down Expand Up @@ -237,7 +240,22 @@ fn determine_kind<'a>(
Kind::Each { features }
}
} else if cx.feature_powerset {
let features = features::feature_powerset(features, cx.depth, &package.features);
let package_features_flattened = flatten_features(&package.features);
let mut features =
features::feature_powerset(features, cx.depth, &package_features_flattened);

let at_least_one_of =
features::at_least_one_of_for_package(&cx.at_least_one_of, &package_features_flattened);
if !at_least_one_of.is_empty() {
features.retain(|feature_set| {
at_least_one_of.iter().all(|required_set| {
feature_set
.iter()
.flat_map(|f| f.as_group())
.any(|f| required_set.contains(f.as_str()))
})
});
}

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 a1eae6d

Please sign in to comment.