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

Add --must-have-and-exclude-feature option. #262

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Note: In this file, do not use the hard wrap in the middle of a sentence for com

## [Unreleased]

- Add `--must-have-and-exclude-feature` option. ([#262](https://github.com/taiki-e/cargo-hack/pull/262), thanks @xStrom)

## [0.6.35] - 2025-02-11

- Performance improvements.
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,15 @@ OPTIONS:
This flag can only be used together with either --each-feature flag or
--feature-powerset flag.

--must-have-and-exclude-feature <FEATURE>
Require the specified feature to be present but excluded.

Exclude the specified feature and all other features which depend on it.

Exclude packages which don't have the specified feature.

This is useful for doing no_std testing with --must-have-and-exclude-feature std.

--no-dev-deps
Perform without dev-dependencies.

Expand Down
32 changes: 31 additions & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ pub(crate) struct Args {
pub(crate) each_feature: bool,
/// --feature-powerset
pub(crate) feature_powerset: bool,
/// --must-have-and-exclude-feature <FEATURE>
pub(crate) must_have_and_exclude_feature: Option<String>,
/// --no-dev-deps
pub(crate) no_dev_deps: bool,
/// --remove-dev-deps
Expand Down Expand Up @@ -152,6 +154,7 @@ impl Args {
let mut remove_dev_deps = false;
let mut each_feature = false;
let mut feature_powerset = false;
let mut must_have_and_exclude_feature = None;
let mut no_private = false;
let mut ignore_private = false;
let mut ignore_unknown_features = false;
Expand Down Expand Up @@ -303,6 +306,9 @@ 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("must-have-and-exclude-feature") => {
parse_opt!(must_have_and_exclude_feature, false);
}
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),
Expand Down Expand Up @@ -492,6 +498,8 @@ impl Args {
conflicts("--all-features", "--each-feature")?;
} else if feature_powerset {
conflicts("--all-features", "--feature-powerset")?;
} else if must_have_and_exclude_feature.is_some() {
conflicts("--all-features", "--must-have-and-exclude-feature")?;
}
}
if no_default_features {
Expand Down Expand Up @@ -520,6 +528,21 @@ impl Args {
}
}

if let Some(f) = must_have_and_exclude_feature.as_ref() {
if features.contains(f) {
bail!("feature `{f}` specified by both --must-have-and-exclude-feature and --features");
}
if optional_deps.as_ref().is_some_and(|d| d.contains(f)) {
bail!("feature `{f}` specified by both --must-have-and-exclude-feature and --optional-deps");
}
if group_features.iter().any(|v| v.matches(f)) {
bail!("feature `{f}` specified by both --must-have-and-exclude-feature and --group-features");
}
if include_features.contains(f) {
bail!("feature `{f}` specified by both --must-have-and-exclude-feature and --include-features");
}
}

if subcommand.is_none() {
if cargo_args.iter().any(|a| a == "--list") {
cmd!(cargo, "--list").run()?;
Expand Down Expand Up @@ -591,7 +614,8 @@ impl Args {
exclude_no_default_features |= !include_features.is_empty();
exclude_all_features |= !include_features.is_empty()
|| !exclude_features.is_empty()
|| !mutually_exclusive_features.is_empty();
|| !mutually_exclusive_features.is_empty()
|| must_have_and_exclude_feature.is_some();
exclude_features.extend_from_slice(&features);

term::verbose::set(verbose != 0);
Expand All @@ -613,6 +637,7 @@ impl Args {
workspace,
each_feature,
feature_powerset,
must_have_and_exclude_feature,
no_dev_deps,
remove_dev_deps,
no_private,
Expand Down Expand Up @@ -758,6 +783,11 @@ const HELP: &[HelpText<'_>] = &[
--feature-powerset flag.",
],
),
("", "--must-have-and-exclude-feature", "<FEATURE>", "Require the specified feature to be present but excluded", &[
"Exclude the specified feature and all other features which depend on it.",
"Exclude packages which don't have the specified feature.",
"This is useful for doing no_std testing with --must-have-and-exclude-feature std.",
]),
("", "--no-dev-deps", "", "Perform without dev-dependencies", &[
"Note that this flag removes dev-dependencies from real `Cargo.toml` while cargo-hack is \
running and restores it when finished.",
Expand Down
4 changes: 4 additions & 0 deletions src/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ impl Features {
pub(crate) fn contains(&self, name: &str) -> bool {
self.features.iter().any(|f| f == name)
}

pub(crate) fn get(&self, name: &str) -> Option<&Feature> {
self.features.iter().find(|f| *f == name)
}
}

/// The representation of Cargo feature.
Expand Down
11 changes: 11 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,9 +200,13 @@ fn determine_kind<'a>(

let package = cx.packages(id);
let pkg_features = cx.pkg_features(id);
let recursively_exclude_feature =
cx.must_have_and_exclude_feature.as_ref().and_then(|s| pkg_features.get(s));
let filter = |&f: &&Feature| {
!cx.exclude_features.iter().any(|s| f == s)
&& !cx.group_features.iter().any(|g| g.matches(f.name()))
&& !recursively_exclude_feature
.is_some_and(|rf| rf.matches_recursive(f.name(), &package.features))
};
let features = if cx.include_features.is_empty() {
// TODO
Expand Down Expand Up @@ -340,10 +344,14 @@ fn determine_package_list(cx: &Context) -> Result<Vec<PackageRuns<'_>>> {
);
}
}
let has_required_features = |id: &&PackageId| {
!cx.must_have_and_exclude_feature.as_ref().is_some_and(|s| !cx.pkg_features(id).contains(s))
};
Ok(if cx.workspace {
let ids: Vec<_> = cx
.workspace_members()
.filter(|id| !cx.exclude.contains(&cx.packages(id).name))
.filter(has_required_features)
.collect();
let multiple_packages = ids.len() > 1;
ids.iter().filter_map(|id| determine_kind(cx, id, multiple_packages)).collect()
Expand All @@ -360,13 +368,15 @@ fn determine_package_list(cx: &Context) -> Result<Vec<PackageRuns<'_>>> {
.workspace_members()
.filter(|id| cx.package.contains(&cx.packages(id).name))
.filter(|id| !cx.exclude.contains(&cx.packages(id).name))
.filter(has_required_features)
.collect();
let multiple_packages = ids.len() > 1;
ids.iter().filter_map(|id| determine_kind(cx, id, multiple_packages)).collect()
} else if cx.current_package().is_none() {
let ids: Vec<_> = cx
.workspace_members()
.filter(|id| !cx.exclude.contains(&cx.packages(id).name))
.filter(has_required_features)
.collect();
let multiple_packages = ids.len() > 1;
ids.iter().filter_map(|id| determine_kind(cx, id, multiple_packages)).collect()
Expand All @@ -376,6 +386,7 @@ fn determine_package_list(cx: &Context) -> Result<Vec<PackageRuns<'_>>> {
cx.workspace_members()
.find(|id| cx.packages(id).name == *current_package)
.filter(|id| !cx.exclude.contains(&cx.packages(id).name))
.filter(has_required_features)
.and_then(|id| determine_kind(cx, id, multiple_packages).map(|p| vec![p]))
.unwrap_or_default()
})
Expand Down
35 changes: 22 additions & 13 deletions src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,19 +98,28 @@ impl<'a> ProcessBuilder<'a> {
}

pub(crate) fn append_features_from_args(&mut self, cx: &Context, id: &PackageId) {
if cx.ignore_unknown_features {
self.append_features(cx.features.iter().filter(|&f| {
if cx.pkg_features(id).contains(f) {
true
} else {
// ignored
info!("skipped applying unknown `{f}` feature to {}", cx.packages(id).name);
false
}
}));
} else if !cx.features.is_empty() {
self.append_features(&cx.features);
}
let package = cx.packages(id);
let pkg_features = cx.pkg_features(id);
let recursively_exclude_feature =
cx.must_have_and_exclude_feature.as_ref().and_then(|s| pkg_features.get(s));

self.append_features(cx.features.iter().filter(|&f| {
if recursively_exclude_feature
.is_some_and(|rf| rf.matches_recursive(f, &package.features))
{
info!(
"skipped applying `{f}` feature to {} because it would enable excluded feature `{}`",
package.name,
recursively_exclude_feature.unwrap().name()
);
false
} else if cx.ignore_unknown_features && !pkg_features.contains(f) {
info!("skipped applying unknown `{f}` feature to {}", package.name);
false
} else {
true
}
}));
}

/// Gets the comma-separated features list
Expand Down
7 changes: 7 additions & 0 deletions tests/fixtures/must_have_and_exclude_feature/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[workspace]
resolver = "2"
members = [
"member1",
"member2",
"member3",
]
13 changes: 13 additions & 0 deletions tests/fixtures/must_have_and_exclude_feature/member1/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "member1"
version = "0.0.0"

[features]
default = ["c"]
a = []
b = []
c = ["b"]

[dependencies]

[dev-dependencies]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fn main() {}
13 changes: 13 additions & 0 deletions tests/fixtures/must_have_and_exclude_feature/member2/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "member2"
version = "0.0.0"

[features]
default = ["a"]
a = []
b = []
c = ["b"]

[dependencies]

[dev-dependencies]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fn main() {}
12 changes: 12 additions & 0 deletions tests/fixtures/must_have_and_exclude_feature/member3/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "member3"
version = "0.0.0"

[features]
default = ["c"]
a = []
c = ["a"]

[dependencies]

[dev-dependencies]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fn main() {}
9 changes: 9 additions & 0 deletions tests/long-help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,15 @@ OPTIONS:
This flag can only be used together with either --each-feature flag or
--feature-powerset flag.

--must-have-and-exclude-feature <FEATURE>
Require the specified feature to be present but excluded.

Exclude the specified feature and all other features which depend on it.

Exclude packages which don't have the specified feature.

This is useful for doing no_std testing with --must-have-and-exclude-feature std.

--no-dev-deps
Perform without dev-dependencies.

Expand Down
1 change: 1 addition & 0 deletions tests/short-help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ OPTIONS:
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
--must-have-and-exclude-feature <FEATURE> Require the specified feature to be present but excluded
--no-dev-deps Perform without dev-dependencies
--remove-dev-deps Equivalent to --no-dev-deps flag except for does not
restore the original `Cargo.toml` after performed
Expand Down
Loading
Loading