Skip to content

Commit 8e51eeb

Browse files
committed
Implement --at-least-one-of
Fixes #182
1 parent 611ca92 commit 8e51eeb

File tree

5 files changed

+101
-23
lines changed

5 files changed

+101
-23
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
131+
enable any 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

+41-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,25 @@ impl Args {
586587
}
587588
}
588589

590+
fn parse_grouped_features(group_features: Vec<String>, option_name: &str) -> Result<Vec<Feature>, anyhow::Error> {
591+
let group_features =
592+
group_features.iter().try_fold(Vec::with_capacity(group_features.len()), |mut v, g| {
593+
let g = if g.contains(',') {
594+
g.split(',')
595+
} else if g.contains(' ') {
596+
g.split(' ')
597+
} else {
598+
bail!(
599+
"--{option_name} requires a list of two or more features separated by space \
600+
or comma"
601+
);
602+
};
603+
v.push(Feature::group(g));
604+
Ok(v)
605+
})?;
606+
Ok(group_features)
607+
}
608+
589609
fn has_z_flag(args: &[String], name: &str) -> bool {
590610
let mut iter = args.iter().map(String::as_str);
591611
while let Some(mut arg) = iter.next() {
@@ -668,6 +688,12 @@ const HELP: &[HelpText<'_>] = &[
668688
--group-features c,d`",
669689
"This flag can only be used together with --feature-powerset flag.",
670690
]),
691+
("", "--at-least-one-of", "<FEATURES>...", "Space or comma separated list of features. Skips sets of features that don't
692+
enable any of the features listed.", &[
693+
"To specify multiple groups, use this option multiple times: `--at-least-one-of a,b \
694+
--at-least-one-of c,d`",
695+
"This flag can only be used together with --feature-powerset flag.",
696+
]),
671697
(
672698
"",
673699
"--include-features",

src/features.rs

+30-6
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,8 @@ impl AsRef<str> for Feature {
165165
pub(crate) fn feature_powerset<'a>(
166166
features: impl IntoIterator<Item = &'a Feature>,
167167
depth: Option<usize>,
168-
map: &BTreeMap<String, Vec<String>>,
168+
deps_map: &BTreeMap<&str, BTreeSet<&str>>,
169169
) -> Vec<Vec<&'a Feature>> {
170-
let deps_map = feature_deps(map);
171170
powerset(features, depth)
172171
.into_iter()
173172
.skip(1) // The first element of a powerset is `[]` so it should be skipped.
@@ -181,7 +180,7 @@ pub(crate) fn feature_powerset<'a>(
181180
.collect()
182181
}
183182

184-
fn feature_deps(map: &BTreeMap<String, Vec<String>>) -> BTreeMap<&str, BTreeSet<&str>> {
183+
pub(crate) fn flatten_features(map: &BTreeMap<String, Vec<String>>) -> BTreeMap<&str, BTreeSet<&str>> {
185184
fn f<'a>(
186185
map: &'a BTreeMap<String, Vec<String>>,
187186
set: &mut BTreeSet<&'a str>,
@@ -220,11 +219,36 @@ fn powerset<T: Copy>(iter: impl IntoIterator<Item = T>, depth: Option<usize>) ->
220219
})
221220
}
222221

222+
// Leave only features that are possible to enable in the package.
223+
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>> {
224+
if at_least_one_of.is_empty() {
225+
return vec![];
226+
}
227+
228+
let mut all_features_enabled_by = BTreeMap::new();
229+
for (&enabled_by, enables) in package_features_flattened {
230+
all_features_enabled_by.entry(enabled_by).or_insert_with(BTreeSet::new).insert(enabled_by);
231+
for &enabled_feature in enables {
232+
all_features_enabled_by.entry(enabled_feature).or_insert_with(BTreeSet::new).insert(enabled_by);
233+
}
234+
}
235+
236+
at_least_one_of.iter().map(|set| {
237+
set.as_group().iter().filter_map(|f| {
238+
all_features_enabled_by.get(f.as_str())
239+
})
240+
.flat_map(|f| f.iter().copied())
241+
.collect::<BTreeSet<_>>()
242+
})
243+
.filter(|set| !set.is_empty())
244+
.collect::<Vec<_>>()
245+
}
246+
223247
#[cfg(test)]
224248
mod tests {
225249
use std::collections::{BTreeMap, BTreeSet};
226250

227-
use super::{feature_deps, feature_powerset, powerset, Feature};
251+
use super::{flatten_features, feature_powerset, powerset, Feature};
228252

229253
macro_rules! v {
230254
($($expr:expr),* $(,)?) => {
@@ -247,7 +271,7 @@ mod tests {
247271
#[test]
248272
fn feature_deps1() {
249273
let map = map![("a", v![]), ("b", v!["a"]), ("c", v!["b"]), ("d", v!["a", "b"])];
250-
let fd = feature_deps(&map);
274+
let fd = flatten_features(&map);
251275
assert_eq!(fd, map![
252276
("a", set![]),
253277
("b", set!["a"]),
@@ -274,7 +298,7 @@ mod tests {
274298
vec!["b", "c", "d"],
275299
vec!["a", "b", "c", "d"],
276300
]);
277-
let filtered = feature_powerset(list.iter().collect::<Vec<_>>(), None, &map);
301+
let filtered = feature_powerset(list.iter().collect::<Vec<_>>(), None, &flatten_features(&map));
278302
assert_eq!(filtered, vec![vec!["a"], vec!["b"], vec!["c"], vec!["d"], vec!["c", "d"]]);
279303
}
280304

src/main.rs

+12-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ use std::{
3434
use anyhow::{bail, Result};
3535

3636
use crate::{
37-
context::Context, features::Feature, metadata::PackageId, process::ProcessBuilder,
37+
context::Context, features::{Feature, flatten_features}, metadata::PackageId, process::ProcessBuilder,
3838
rustup::Rustup,
3939
};
4040

@@ -237,7 +237,17 @@ 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 package_features_flattened = flatten_features(&package.features);
241+
let mut features = features::feature_powerset(features, cx.depth, &package_features_flattened);
242+
243+
let at_least_one_of = features::at_least_one_of_for_package(&cx.at_least_one_of, &package_features_flattened);
244+
if !at_least_one_of.is_empty() {
245+
features.retain(|feature_set| {
246+
at_least_one_of.iter().all(|required_set| {
247+
feature_set.iter().flat_map(|f| f.as_group()).any(|f| required_set.contains(f.as_str()))
248+
})
249+
});
250+
}
241251

242252
if (pkg_features.normal().is_empty() && pkg_features.optional_deps().is_empty()
243253
|| !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
102+
enable any 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.

0 commit comments

Comments
 (0)