-
Notifications
You must be signed in to change notification settings - Fork 2.6k
implement package feature unification #15684
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
Conversation
r? @weihanglo rustbot has assigned @weihanglo. Use |
ad48f18
to
e94564d
Compare
e94564d
to
c6090d5
Compare
Thanks for moving this forward! |
c6090d5
to
e5a3134
Compare
Feel free to edit the commits as needed |
b10f5d9
to
2457810
Compare
I've also just added a test with weak dependencies, which works. I tried to also add namespaced |
2457810
to
d28a30f
Compare
d28a30f
to
a8b46d7
Compare
a8b46d7
to
52725c0
Compare
52725c0
to
6c7ca34
Compare
I had to also narrow down However, simply narrowing it down does not work for resolver v1 because there are tests like I've added a test for this but added it just to the commit that's supposed to fix the tests. I think I've done three different things regarding splitting tests into commits by now since this is adding a feature so behavior of tests before and after the addition may be either behavior with |
Per usual, would prefer tests and test cases to be added in that initial commit. Whatever it does (nothing, change behavior, start passing, start erroring), seeing that change of state through the diffs is very helpful for reviewing and demonstrating what the behavior is. |
6c7ca34
to
06c0d91
Compare
Ok, I've added all the tests with |
src/cargo/ops/resolve.rs
Outdated
// We want to narrow the features to the current specs so that stuff like `cargo check -p a | ||
// -p b -F a/a,b/b` works and the resolver does not contain that `a` does not have feature | ||
// `b` and vice-versa. However, resolver v1 needs to see even features of unselected | ||
// packages turned on if it was because of working directory being inside the unselected | ||
// package, because they might turn on a feature of a selected package. | ||
let narrowed_features = match feature_unification { | ||
FeatureUnification::Package => { | ||
let mut narrowed_features = cli_features.clone(); | ||
let enabled_features = members_with_features | ||
.iter() | ||
.filter_map(|(package, cli_features)| { | ||
specs | ||
.iter() | ||
.any(|spec| spec.matches(package.package_id())) | ||
.then_some(cli_features.features.iter()) | ||
}) | ||
.flatten() | ||
.cloned() | ||
.collect(); | ||
narrowed_features.features = Rc::new(enabled_features); | ||
Cow::Owned(narrowed_features) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From the --features
docs
Space or comma separated list of features to activate. Features of workspace members may be enabled with package-name/feature-name syntax. This flag may be specified multiple times, which enables all specified features.
The way this is worded, it makes it sound like you can specify a feature of a dependency so long as its a workspace member. Whether its intended or not, you can even specify features for transitive dependencies so long as all of the activated deps are already in your Cargo.lock
@ehuss wanted to double check my reading of those docs and if you had any additional ideas about the problem of CARGO_RESOLVER_FEATURE_UNIFICATION=package cargo check -p a -F a/a -p b -F b/b
trying to activate the b/b
feature when b
isn't in the resolve.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does work that way, I've added a test case just now to the cli features tests having cargo check -p a -F a/a,common/b
because members_with_features
already contains the a
feature and also a DepFeature
for common/b
, both for package a
. If that's what you mean.
Arguably that reading could even allow cargo check -p a -F b/b
even if b
is not a dependency of a
(the quoted docs don't mention dependencies at all) but that's rejected regardless of feature unification settings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So if I'm understanding correctly, the narrowed features are preserving that existing behavior
Didn't help that I read through this multiple times before it clicked what you are doing with this code block. You had already divided up features by workspace member, so now you are getting the ones only relevant for the selected workspace members to apply to resolution.
What would this then do with CARGO_RESOLVER_FEATURE_UNIFICATION=package cargo check -p a -F transitive/feature
? I'm assuming it silently ignores transitive/feature
as it wasn't in members_with_features
and so doesn't get forwarded on to the feature resolver.
That case isn't officially supported but it would at least be good to call out if the behavior deviates from regular cargo behavior in the tracking issue. We can then decide how we want to handle that case more generally.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean something like this? It fails for both package
and selected
so I assume not, but then I don't understand what case do you mean.
Test case
#[cargo_test]
fn transitive_feature() {
Package::new("child", "1.0.0")
.add_dep(&Dependency::new("grand_child", "1.0"))
.publish();
Package::new("grand_child", "1.0.0")
.feature("a", &[])
.file(
"lib.rs",
r#"
#[cfg(not(feature = "a"))]
compile_error!("missing feature");
"#,
)
.publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
edition = "2024"
[dependencies]
chile = "1.0"
"#,
)
.file("src/lib.rs", "")
.build();
p.cargo("check -F grand_child/a")
.arg("-Zfeature-unification")
.masquerade_as_nightly_cargo(&["feature-unification"])
.env("CARGO_RESOLVER_FEATURE_UNIFICATION", "selected")
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] the package 'foo' does not contain this feature: grand_child/a
"#]])
.run();
p.cargo("check -F grand_child/a")
.arg("-Zfeature-unification")
.masquerade_as_nightly_cargo(&["feature-unification"])
.env("CARGO_RESOLVER_FEATURE_UNIFICATION", "package")
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] the package 'foo' does not contain this feature: grand_child/a
"#]])
.run();
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like I over-estimated what is supported. The case I'm talking about is limited to direct dependencies:
#[cargo_test]
fn non_member_features() {
Package::new("child", "1.0.0")
.add_dep(&Dependency::new("grand_child", "1.0"))
.feature("a", &[])
.file(
"src/lib.rs",
r#"
#[cfg(feature = "a")]
compile_error!("`a` is active");
"#,
)
.publish();
Package::new("grand_child", "1.0.0")
.feature("a", &[])
.file(
"src/lib.rs",
r#"
#[cfg(feature = "a")]
compile_error!("`a` is active");
"#,
)
.publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
edition = "2024"
[dependencies]
child = "1.0"
"#,
)
.file("src/lib.rs", "")
.build();
p.cargo("check -F child/a")
.with_status(101)
.with_stderr_data(str![[r#"
[UPDATING] `dummy-registry` index
[LOCKING] 2 packages to latest Rust 1.87.0 compatible versions
[DOWNLOADING] crates ...
[DOWNLOADED] grand_child v1.0.0 (registry `dummy-registry`)
[DOWNLOADED] child v1.0.0 (registry `dummy-registry`)
[CHECKING] grand_child v1.0.0
[CHECKING] child v1.0.0
[ERROR] `a` is active
--> [ROOT]/home/.cargo/registry/src/-[HASH]/child-1.0.0/src/lib.rs:3:13
|
3 | compile_error!("`a` is active");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[ERROR] could not compile `child` (lib) due to 1 previous error
"#]])
.run();
p.cargo("check -F grand_child/a")
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] the package 'foo' does not contain this feature: grand_child/a
"#]])
.run();
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added a test at the end trying to cover as much of this as I could with all three cases of feature unification. It seems to work the same way regardless of the option, i.e. you can set features on direct dependencies but not on grand-dependencies.
This has a consequence that Cargo.toml
s are more expressive than command line, e.g. you can set default features so that a
builds with common/a
and b
builds with common/b
. If there are no default features (or features on a
and b
turning on the respective features), one cannot just write something like cargo check -p a -p b -F a/common/a -F b/common/b
to turn the featur on only for the given root package. I would say this is correct behavior, just something to point out, because until now I believe one could theoretically just not define any features and then work around that on the command line, which is no longer the case with feature-unification = package
. I'm not sure if what I wrote is comprehensible, if not, I'll try to crate full examples for illustration.
Just need to double check on one question (#15684 (comment)) but otherwise, this looks good. Thanks for your work on this and patience through the review feedback! |
cd1cff4
to
20021a9
Compare
Honestly, this has been a blast. I wouldn't say I needed pretty much any patience with how fast you were reacting to each iteration. Thanks for the time and reviews! |
#[cargo_test] | ||
fn package_feature_unification_grandchild_cli_features() { | ||
let p = project() | ||
.file( | ||
"Cargo.toml", | ||
r#" | ||
[workspace] | ||
resolver = "2" | ||
members = ["parent", "child", "grandchild"] | ||
"#, | ||
) | ||
.file( | ||
"grandchild/Cargo.toml", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whats the intention of this test?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to cover what you were talking about regarding turning on features of non-selected dependency crates inside a workspace. I thought it would be different in some cases based on feature-unification
but it's consistent.
Do you want me to remove this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are three cases for foo
in foo/bar
- A workspace member
- A direct dep of a workspace member
- An indirect dep of a workspace member
In #15684 (comment), I was talking about (2) while this test covers (1).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, I tried to cover the first two cases in the original package_feature_unification_cli_features
.
Here I tried specifically to get close to 3 but since that does not work in the general case, I tried to get closer to it by the indirect dependency being part of the workspace, but either not being selected or being excluded.
I was unsure about how some of the scenarios would (or wouldn't) work but arguably since all of the cases work the same way regardless of feature-unification
, it might not belong here as it's more of a general features test. Or it might as it should be the same for all of them.
So I can either leave this as is, move it somewhere or just remove it completely.
And if you want the other cases covered more than they are in the other test, feel free to tell me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, I tried to cover the first two cases in the original package_feature_unification_cli_features.
Ok, that looks to be the case "check -p a -F a/a,outside/b"
.
So I can either leave this as is, move it somewhere or just remove it completely.
Unsure but we should at least make the name more clear that this is focused on activating features for specific packages only within a workspace, contracted with package_feature_unification_cli_features
which focused on a combination of workspace members and registry dependencies while having a similar name of package_feature_unification_grandchild_cli_features
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've renamed the second one to feature_unification_of_cli_features_within_workspace
. I didn't come up with anything more descriptive yet reasonable than package_feature_unification_cli_features
for the first one so I've left that as is.
20021a9
to
f7b58c2
Compare
@mladedav looks like there is a conflict? |
f7b58c2
to
e4a616b
Compare
Right, I've rebased this on top of master now |
Thanks! |
Update cargo 14 commits in 930b4f62cfcd1f0eabdb30a56d91bf6844b739bf..eabb4cd923deb73e714f7ad3f5234d68ca284dbe 2025-06-28 14:58:43 +0000 to 2025-07-09 22:07:55 +0000 - feat: Implementation and tests for `multiple-build-scripts` (rust-lang/cargo#15704) - perf: Speed up TOML parsing by upgrading toml (rust-lang/cargo#15736) - Mark cachelock tests that rely on interprocess blocking behaviour as unsupported on AIX. (rust-lang/cargo#15734) - feat(publish): Stabilize multi-package publishing (rust-lang/cargo#15636) - Update to Rust 2024 (rust-lang/cargo#15732) - Clarify package ID specifications in SBOMs are fully qualified (rust-lang/cargo#15731) - chore(deps): update cargo-semver-checks to v0.42.0 (rust-lang/cargo#15730) - test: Switch config tests to use snapshots (rust-lang/cargo#15729) - implement package feature unification (rust-lang/cargo#15684) - chore: Upgrade dependencies (rust-lang/cargo#15722) - Report valid file name when we can't find a build target for `name = "foo.rs"` (rust-lang/cargo#15707) - chore(release): Publish build-rs on release (rust-lang/cargo#15708) - Override `Cargo.lock` checksums when doing a dry-run `publish` (rust-lang/cargo#15711) - test(rustfix): Update for nightly (rust-lang/cargo#15717) r? ghost
What does this PR try to resolve?
Implements another part of feature unification (#14774, rfc). The
workspace
option was implemented in #15157, this adds thepackage
option.How to test and review this PR?
The important change is changing
WorkspaceResolve
so it can contain multipleResolvedFeature
s. Along with that, it also needs to know which specs those features are resolved for. This was used in several other places:cargo fix --edition
(from 2018 to 2021) - I think it should be ok to disallow usingcargo fix --edition
when someone already uses this feature.cargo tree
- I just use the first feature set. This is definitely not ideal, but I'm not entirely sure what's the correct solution here. Printing multiple trees? Disallowing this, forcing users to select only one package?Based on comments in #15157 I've added tests first with
selected
feature unification and then changed that after implementation. I'm not sure if that's how you expect the tests to be added first, if not, I can change the history.I've expanded the test checking that this is ignored for
cargo install
although it should work the same way even if it is not ignored (selected
andpackage
are the same thing when just one package is selected).