Skip to content

Commit b71af30

Browse files
committed
Implement --at-least-one-of
Fixes #182
1 parent ab4c446 commit b71af30

File tree

6 files changed

+141
-20
lines changed

6 files changed

+141
-20
lines changed

README.md

+9
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,15 @@ OPTIONS:
126126

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

129+
--at-least-one-of <FEATURES>...
130+
Space or comma separated list of features. Skips sets of features that don't enable any
131+
of the features listed.
132+
133+
To specify multiple groups, use this option multiple times: `--at-least-one-of a,b
134+
--at-least-one-of c,d`
135+
136+
This flag can only be used together with --feature-powerset flag.
137+
129138
--include-features <FEATURES>...
130139
Include only the specified features in the feature combinations instead of package
131140
features.

src/cli.rs

+43-15
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ pub(crate) struct Args {
7878
pub(crate) depth: Option<usize>,
7979
/// --group-features <FEATURES>...
8080
pub(crate) group_features: Vec<Feature>,
81+
/// --at-least-one-of <FEATURES>...
82+
/// Implies --exclude-no-default-features. Can be specified multiple times.
83+
pub(crate) at_least_one_of: Vec<Feature>,
8184

8285
// options that will be propagated to cargo
8386
/// --features <FEATURES>...
@@ -151,6 +154,7 @@ impl Args {
151154

152155
let mut optional_deps = None;
153156
let mut include_features = vec![];
157+
let mut at_least_one_of = vec![];
154158
let mut include_deps_features = false;
155159

156160
let mut exclude_features = vec![];
@@ -277,6 +281,7 @@ impl Args {
277281
Long("remove-dev-deps") => parse_flag!(remove_dev_deps),
278282
Long("each-feature") => parse_flag!(each_feature),
279283
Long("feature-powerset") => parse_flag!(feature_powerset),
284+
Long("at-least-one-of") => at_least_one_of.push(parser.value()?.parse()?),
280285
Long("no-private") => parse_flag!(no_private),
281286
Long("ignore-private") => parse_flag!(ignore_private),
282287
Long("exclude-no-default-features") => parse_flag!(exclude_no_default_features),
@@ -391,8 +396,16 @@ impl Args {
391396
requires("--include-features", &["--each-feature", "--feature-powerset"])?;
392397
} else if include_deps_features {
393398
requires("--include-deps-features", &["--each-feature", "--feature-powerset"])?;
399+
} else if !at_least_one_of.is_empty() {
400+
requires("--at-least-one-of", &["--feature-powerset"])?;
394401
}
395402
}
403+
404+
if !at_least_one_of.is_empty() {
405+
// there will always be a feature set
406+
exclude_no_default_features = true;
407+
}
408+
396409
if !feature_powerset {
397410
if depth.is_some() {
398411
requires("--depth", &["--feature-powerset"])?;
@@ -410,21 +423,8 @@ impl Args {
410423
}
411424

412425
let depth = depth.as_deref().map(str::parse::<usize>).transpose()?;
413-
let group_features =
414-
group_features.iter().try_fold(Vec::with_capacity(group_features.len()), |mut v, g| {
415-
let g = if g.contains(',') {
416-
g.split(',')
417-
} else if g.contains(' ') {
418-
g.split(' ')
419-
} else {
420-
bail!(
421-
"--group-features requires a list of two or more features separated by space \
422-
or comma"
423-
);
424-
};
425-
v.push(Feature::group(g));
426-
Ok(v)
427-
})?;
426+
let group_features = parse_grouped_features(&group_features, "group-features")?;
427+
let at_least_one_of = parse_grouped_features(&at_least_one_of, "at-least-one-of")?;
428428

429429
if let Some(subcommand) = subcommand.as_deref() {
430430
match subcommand {
@@ -567,6 +567,7 @@ impl Args {
567567
print_command_list,
568568
no_manifest_path,
569569
include_features: include_features.into_iter().map(Into::into).collect(),
570+
at_least_one_of,
570571
include_deps_features,
571572
version_range,
572573
version_step,
@@ -586,6 +587,28 @@ impl Args {
586587
}
587588
}
588589

590+
fn parse_grouped_features(
591+
group_features: &[String],
592+
option_name: &str,
593+
) -> Result<Vec<Feature>, anyhow::Error> {
594+
let group_features =
595+
group_features.iter().try_fold(Vec::with_capacity(group_features.len()), |mut v, g| {
596+
let g = if g.contains(',') {
597+
g.split(',')
598+
} else if g.contains(' ') {
599+
g.split(' ')
600+
} else {
601+
bail!(
602+
"--{option_name} requires a list of two or more features separated by space \
603+
or comma"
604+
);
605+
};
606+
v.push(Feature::group(g));
607+
Ok(v)
608+
})?;
609+
Ok(group_features)
610+
}
611+
589612
fn has_z_flag(args: &[String], name: &str) -> bool {
590613
let mut iter = args.iter().map(String::as_str);
591614
while let Some(mut arg) = iter.next() {
@@ -668,6 +691,11 @@ const HELP: &[HelpText<'_>] = &[
668691
--group-features c,d`",
669692
"This flag can only be used together with --feature-powerset flag.",
670693
]),
694+
("", "--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", &[
695+
"To specify multiple groups, use this option multiple times: `--at-least-one-of a,b \
696+
--at-least-one-of c,d`",
697+
"This flag can only be used together with --feature-powerset flag.",
698+
]),
671699
(
672700
"",
673701
"--include-features",

src/features.rs

+76-4
Original file line numberDiff line numberDiff line change
@@ -178,9 +178,12 @@ impl AsRef<str> for Feature {
178178
pub(crate) fn feature_powerset<'a>(
179179
features: impl IntoIterator<Item = &'a Feature>,
180180
depth: Option<usize>,
181-
map: &BTreeMap<String, Vec<String>>,
181+
at_least_one_of: &[Feature],
182+
package_features: &BTreeMap<String, Vec<String>>,
182183
) -> Vec<Vec<&'a Feature>> {
183-
let deps_map = feature_deps(map);
184+
let deps_map = feature_deps(package_features);
185+
let at_least_one_of = at_least_one_of_for_package(at_least_one_of, &deps_map);
186+
184187
powerset(features, depth)
185188
.into_iter()
186189
.skip(1) // The first element of a powerset is `[]` so it should be skipped.
@@ -191,6 +194,15 @@ pub(crate) fn feature_powerset<'a>(
191194
})
192195
})
193196
})
197+
.filter(move |fs| {
198+
// all() returns true if at_least_one_of is empty
199+
at_least_one_of.iter().all(|required_set| {
200+
fs
201+
.iter()
202+
.flat_map(|f| f.as_group())
203+
.any(|f| required_set.contains(f.as_str()))
204+
})
205+
})
194206
.collect()
195207
}
196208

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

252+
// Leave only features that are possible to enable in the package.
253+
pub(crate) fn at_least_one_of_for_package<'a>(
254+
at_least_one_of: &[Feature],
255+
package_features_flattened: &BTreeMap<&'a str, BTreeSet<&'a str>>,
256+
) -> Vec<BTreeSet<&'a str>> {
257+
if at_least_one_of.is_empty() {
258+
return vec![];
259+
}
260+
261+
let mut all_features_enabled_by = BTreeMap::new();
262+
for (&enabled_by, enables) in package_features_flattened {
263+
all_features_enabled_by.entry(enabled_by).or_insert_with(BTreeSet::new).insert(enabled_by);
264+
for &enabled_feature in enables {
265+
all_features_enabled_by
266+
.entry(enabled_feature)
267+
.or_insert_with(BTreeSet::new)
268+
.insert(enabled_by);
269+
}
270+
}
271+
272+
at_least_one_of
273+
.iter()
274+
.map(|set| {
275+
set.as_group()
276+
.iter()
277+
.filter_map(|f| all_features_enabled_by.get(f.as_str()))
278+
.flat_map(|f| f.iter().copied())
279+
.collect::<BTreeSet<_>>()
280+
})
281+
.filter(|set| !set.is_empty())
282+
.collect::<Vec<_>>()
283+
}
284+
240285
#[cfg(test)]
241286
mod tests {
242287
use std::collections::{BTreeMap, BTreeSet};
243288

244-
use super::{feature_deps, feature_powerset, powerset, Feature};
289+
use super::{at_least_one_of_for_package, feature_deps, feature_powerset, powerset, Feature};
245290

246291
macro_rules! v {
247292
($($expr:expr),* $(,)?) => {
@@ -261,6 +306,33 @@ mod tests {
261306
};
262307
}
263308

309+
#[test]
310+
fn at_least_one_of_for_package_filter() {
311+
let map = map![("a", v![]), ("b", v!["a"]), ("c", v!["b"]), ("d", v!["a", "b"])];
312+
let fd = feature_deps(&map);
313+
let list: Vec<Feature> = v!["b", "x", "y", "z"];
314+
let filtered = at_least_one_of_for_package(&list, &fd);
315+
assert_eq!(filtered, vec![set!("b", "c", "d")]);
316+
}
317+
318+
#[test]
319+
fn powerset_with_filter() {
320+
let map = map![("a", v![]), ("b", v!["a"]), ("c", v!["b"]), ("d", v!["a", "b"])];
321+
322+
let list = v!["a", "b", "c", "d"];
323+
let filtered = feature_powerset(&list, None, &[], &map);
324+
assert_eq!(filtered, vec![vec!["a"], vec!["b"], vec!["c"], vec!["d"], vec!["c", "d"]]);
325+
326+
let filtered = feature_powerset(&list, None, &["a".into()], &map);
327+
assert_eq!(filtered, vec![vec!["a"], vec!["b"], vec!["c"], vec!["d"], vec!["c", "d"]]);
328+
329+
let filtered = feature_powerset(&list, None, &["c".into()], &map);
330+
assert_eq!(filtered, vec![vec!["c"], vec!["c", "d"]]);
331+
332+
let filtered = feature_powerset(&list, None, &["a".into(), "c".into()], &map);
333+
assert_eq!(filtered, vec![vec!["c"], vec!["c", "d"]]);
334+
}
335+
264336
#[test]
265337
fn feature_deps1() {
266338
let map = map![("a", v![]), ("b", v!["a"]), ("c", v!["b"]), ("d", v!["a", "b"])];
@@ -291,7 +363,7 @@ mod tests {
291363
vec!["b", "c", "d"],
292364
vec!["a", "b", "c", "d"],
293365
]);
294-
let filtered = feature_powerset(list.iter().collect::<Vec<_>>(), None, &map);
366+
let filtered = feature_powerset(list.iter().collect::<Vec<_>>(), None, &[], &map);
295367
assert_eq!(filtered, vec![vec!["a"], vec!["b"], vec!["c"], vec!["d"], vec!["c", "d"]]);
296368
}
297369

src/main.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,8 @@ fn determine_kind<'a>(
237237
Kind::Each { features }
238238
}
239239
} else if cx.feature_powerset {
240-
let features = features::feature_powerset(features, cx.depth, &package.features);
240+
let features =
241+
features::feature_powerset(features, cx.depth, &cx.at_least_one_of, &package.features);
241242

242243
if (pkg_features.normal().is_empty() && pkg_features.optional_deps().is_empty()
243244
|| !cx.include_features.is_empty())

tests/long-help.txt

+9
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,15 @@ OPTIONS:
9797

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

100+
--at-least-one-of <FEATURES>...
101+
Space or comma separated list of features. Skips sets of features that don't enable any
102+
of the features listed.
103+
104+
To specify multiple groups, use this option multiple times: `--at-least-one-of a,b
105+
--at-least-one-of c,d`
106+
107+
This flag can only be used together with --feature-powerset flag.
108+
100109
--include-features <FEATURES>...
101110
Include only the specified features in the feature combinations instead of package
102111
features.

tests/short-help.txt

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ OPTIONS:
2323
--depth <NUM> Specify a max number of simultaneous feature flags of
2424
--feature-powerset
2525
--group-features <FEATURES>... Space or comma separated list of features to group
26+
--at-least-one-of <FEATURES>... Space or comma separated list of features. Skips sets of
27+
features that don't enable any of the features listed
2628
--include-features <FEATURES>... Include only the specified features in the feature
2729
combinations instead of package features
2830
--no-dev-deps Perform without dev-dependencies

0 commit comments

Comments
 (0)