Skip to content

Commit 544a5ce

Browse files
committed
Add --must-have-and-exclude-feature option.
1 parent a91e788 commit 544a5ce

16 files changed

Lines changed: 274 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ Note: In this file, do not use the hard wrap in the middle of a sentence for com
1212

1313
## [Unreleased]
1414

15+
- Add `--must-have-and-exclude-feature` option. ([#262](https://github.com/taiki-e/cargo-hack/pull/262), thanks @xStrom)
16+
1517
## [0.6.35] - 2025-02-11
1618

1719
- Performance improvements.

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,15 @@ OPTIONS:
162162
This flag can only be used together with either --each-feature flag or
163163
--feature-powerset flag.
164164

165+
--must-have-and-exclude-feature <FEATURE>
166+
Require the specified feature to be present but excluded.
167+
168+
Exclude the specified feature and all other features which depend on it.
169+
170+
Exclude packages which don't have the specified feature.
171+
172+
This is useful for doing no_std testing with --must-have-and-exclude-feature std.
173+
165174
--no-dev-deps
166175
Perform without dev-dependencies.
167176

src/cli.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ pub(crate) struct Args {
3737
pub(crate) each_feature: bool,
3838
/// --feature-powerset
3939
pub(crate) feature_powerset: bool,
40+
/// --must-have-and-exclude-feature <FEATURE>
41+
pub(crate) must_have_and_exclude_feature: Option<String>,
4042
/// --no-dev-deps
4143
pub(crate) no_dev_deps: bool,
4244
/// --remove-dev-deps
@@ -152,6 +154,7 @@ impl Args {
152154
let mut remove_dev_deps = false;
153155
let mut each_feature = false;
154156
let mut feature_powerset = false;
157+
let mut must_have_and_exclude_feature = None;
155158
let mut no_private = false;
156159
let mut ignore_private = false;
157160
let mut ignore_unknown_features = false;
@@ -303,6 +306,9 @@ impl Args {
303306
Long("remove-dev-deps") => parse_flag!(remove_dev_deps),
304307
Long("each-feature") => parse_flag!(each_feature),
305308
Long("feature-powerset") => parse_flag!(feature_powerset),
309+
Long("must-have-and-exclude-feature") => {
310+
parse_opt!(must_have_and_exclude_feature, false);
311+
}
306312
Long("at-least-one-of") => at_least_one_of.push(parser.value()?.parse()?),
307313
Long("no-private") => parse_flag!(no_private),
308314
Long("ignore-private") => parse_flag!(ignore_private),
@@ -492,6 +498,8 @@ impl Args {
492498
conflicts("--all-features", "--each-feature")?;
493499
} else if feature_powerset {
494500
conflicts("--all-features", "--feature-powerset")?;
501+
} else if must_have_and_exclude_feature.is_some() {
502+
conflicts("--all-features", "--must-have-and-exclude-feature")?;
495503
}
496504
}
497505
if no_default_features {
@@ -520,6 +528,21 @@ impl Args {
520528
}
521529
}
522530

531+
if let Some(f) = must_have_and_exclude_feature.as_ref() {
532+
if features.contains(f) {
533+
bail!("feature `{f}` specified by both --must-have-and-exclude-feature and --features");
534+
}
535+
if optional_deps.as_ref().is_some_and(|d| d.contains(f)) {
536+
bail!("feature `{f}` specified by both --must-have-and-exclude-feature and --optional-deps");
537+
}
538+
if group_features.iter().any(|v| v.matches(f)) {
539+
bail!("feature `{f}` specified by both --must-have-and-exclude-feature and --group-features");
540+
}
541+
if include_features.contains(f) {
542+
bail!("feature `{f}` specified by both --must-have-and-exclude-feature and --include-features");
543+
}
544+
}
545+
523546
if subcommand.is_none() {
524547
if cargo_args.iter().any(|a| a == "--list") {
525548
cmd!(cargo, "--list").run()?;
@@ -591,7 +614,8 @@ impl Args {
591614
exclude_no_default_features |= !include_features.is_empty();
592615
exclude_all_features |= !include_features.is_empty()
593616
|| !exclude_features.is_empty()
594-
|| !mutually_exclusive_features.is_empty();
617+
|| !mutually_exclusive_features.is_empty()
618+
|| must_have_and_exclude_feature.is_some();
595619
exclude_features.extend_from_slice(&features);
596620

597621
term::verbose::set(verbose != 0);
@@ -613,6 +637,7 @@ impl Args {
613637
workspace,
614638
each_feature,
615639
feature_powerset,
640+
must_have_and_exclude_feature,
616641
no_dev_deps,
617642
remove_dev_deps,
618643
no_private,
@@ -758,6 +783,11 @@ const HELP: &[HelpText<'_>] = &[
758783
--feature-powerset flag.",
759784
],
760785
),
786+
("", "--must-have-and-exclude-feature", "<FEATURE>", "Require the specified feature to be present but excluded", &[
787+
"Exclude the specified feature and all other features which depend on it.",
788+
"Exclude packages which don't have the specified feature.",
789+
"This is useful for doing no_std testing with --must-have-and-exclude-feature std.",
790+
]),
761791
("", "--no-dev-deps", "", "Perform without dev-dependencies", &[
762792
"Note that this flag removes dev-dependencies from real `Cargo.toml` while cargo-hack is \
763793
running and restores it when finished.",

src/features.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ impl Features {
8484
pub(crate) fn contains(&self, name: &str) -> bool {
8585
self.features.iter().any(|f| f == name)
8686
}
87+
88+
pub(crate) fn get(&self, name: &str) -> Option<&Feature> {
89+
self.features.iter().find(|f| *f == name)
90+
}
8791
}
8892

8993
/// The representation of Cargo feature.

src/main.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,9 +200,13 @@ fn determine_kind<'a>(
200200

201201
let package = cx.packages(id);
202202
let pkg_features = cx.pkg_features(id);
203+
let recursively_exclude_feature =
204+
cx.must_have_and_exclude_feature.as_ref().and_then(|s| pkg_features.get(s));
203205
let filter = |&f: &&Feature| {
204206
!cx.exclude_features.iter().any(|s| f == s)
205207
&& !cx.group_features.iter().any(|g| g.matches(f.name()))
208+
&& !recursively_exclude_feature
209+
.is_some_and(|rf| rf.matches_recursive(f.name(), &package.features))
206210
};
207211
let features = if cx.include_features.is_empty() {
208212
// TODO
@@ -340,10 +344,14 @@ fn determine_package_list(cx: &Context) -> Result<Vec<PackageRuns<'_>>> {
340344
);
341345
}
342346
}
347+
let has_required_features = |id: &&PackageId| {
348+
cx.must_have_and_exclude_feature.as_ref().is_none_or(|s| cx.pkg_features(id).contains(s))
349+
};
343350
Ok(if cx.workspace {
344351
let ids: Vec<_> = cx
345352
.workspace_members()
346353
.filter(|id| !cx.exclude.contains(&cx.packages(id).name))
354+
.filter(has_required_features)
347355
.collect();
348356
let multiple_packages = ids.len() > 1;
349357
ids.iter().filter_map(|id| determine_kind(cx, id, multiple_packages)).collect()
@@ -360,13 +368,15 @@ fn determine_package_list(cx: &Context) -> Result<Vec<PackageRuns<'_>>> {
360368
.workspace_members()
361369
.filter(|id| cx.package.contains(&cx.packages(id).name))
362370
.filter(|id| !cx.exclude.contains(&cx.packages(id).name))
371+
.filter(has_required_features)
363372
.collect();
364373
let multiple_packages = ids.len() > 1;
365374
ids.iter().filter_map(|id| determine_kind(cx, id, multiple_packages)).collect()
366375
} else if cx.current_package().is_none() {
367376
let ids: Vec<_> = cx
368377
.workspace_members()
369378
.filter(|id| !cx.exclude.contains(&cx.packages(id).name))
379+
.filter(has_required_features)
370380
.collect();
371381
let multiple_packages = ids.len() > 1;
372382
ids.iter().filter_map(|id| determine_kind(cx, id, multiple_packages)).collect()
@@ -376,6 +386,7 @@ fn determine_package_list(cx: &Context) -> Result<Vec<PackageRuns<'_>>> {
376386
cx.workspace_members()
377387
.find(|id| cx.packages(id).name == *current_package)
378388
.filter(|id| !cx.exclude.contains(&cx.packages(id).name))
389+
.filter(has_required_features)
379390
.and_then(|id| determine_kind(cx, id, multiple_packages).map(|p| vec![p]))
380391
.unwrap_or_default()
381392
})

src/process.rs

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -98,19 +98,28 @@ impl<'a> ProcessBuilder<'a> {
9898
}
9999

100100
pub(crate) fn append_features_from_args(&mut self, cx: &Context, id: &PackageId) {
101-
if cx.ignore_unknown_features {
102-
self.append_features(cx.features.iter().filter(|&f| {
103-
if cx.pkg_features(id).contains(f) {
104-
true
105-
} else {
106-
// ignored
107-
info!("skipped applying unknown `{f}` feature to {}", cx.packages(id).name);
108-
false
109-
}
110-
}));
111-
} else if !cx.features.is_empty() {
112-
self.append_features(&cx.features);
113-
}
101+
let package = cx.packages(id);
102+
let pkg_features = cx.pkg_features(id);
103+
let recursively_exclude_feature =
104+
cx.must_have_and_exclude_feature.as_ref().and_then(|s| pkg_features.get(s));
105+
106+
self.append_features(cx.features.iter().filter(|&f| {
107+
if recursively_exclude_feature
108+
.is_some_and(|rf| rf.matches_recursive(f, &package.features))
109+
{
110+
info!(
111+
"skipped applying `{f}` feature to {} because it would enable excluded feature `{}`",
112+
package.name,
113+
recursively_exclude_feature.unwrap().name()
114+
);
115+
false
116+
} else if cx.ignore_unknown_features && !pkg_features.contains(f) {
117+
info!("skipped applying unknown `{f}` feature to {}", package.name);
118+
false
119+
} else {
120+
true
121+
}
122+
}));
114123
}
115124

116125
/// Gets the comma-separated features list
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[workspace]
2+
resolver = "2"
3+
members = [
4+
"member1",
5+
"member2",
6+
"member3",
7+
]
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "member1"
3+
version = "0.0.0"
4+
5+
[features]
6+
default = ["c"]
7+
a = []
8+
b = []
9+
c = ["b"]
10+
11+
[dependencies]
12+
13+
[dev-dependencies]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
fn main() {}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "member2"
3+
version = "0.0.0"
4+
5+
[features]
6+
default = ["a"]
7+
a = []
8+
b = []
9+
c = ["b"]
10+
11+
[dependencies]
12+
13+
[dev-dependencies]

0 commit comments

Comments
 (0)