Skip to content

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

Open
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

mladedav
Copy link

@mladedav mladedav commented Jun 19, 2025

What does this PR try to resolve?

Implements another part of feature unification (#14774, rfc). The workspace option was implemented in #15157, this adds the package option.

How to test and review this PR?

The important change is changing WorkspaceResolve so it can contain multiple ResolvedFeatures. 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 using cargo fix --edition when someone already uses this feature.
  • building std - it should be safe to assume std is not using this feature so I just unwrap there. I'm not sure if some attempt to later feature unification would be better.
  • 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 and package are the same thing when just one package is selected).

@rustbot
Copy link
Collaborator

rustbot commented Jun 19, 2025

r? @weihanglo

rustbot has assigned @weihanglo.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@rustbot rustbot added A-configuration Area: cargo config files and env vars A-documenting-cargo-itself Area: Cargo's documentation Command-fix Command-tree S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jun 19, 2025
@mladedav mladedav force-pushed the dm/unification-package branch from ad48f18 to e94564d Compare June 19, 2025 21:41
@mladedav mladedav changed the title implement projects feature unification implement package feature unification Jun 19, 2025
@mladedav mladedav force-pushed the dm/unification-package branch from e94564d to c6090d5 Compare June 19, 2025 21:53
@epage
Copy link
Contributor

epage commented Jun 19, 2025

Thanks for moving this forward!

@mladedav mladedav force-pushed the dm/unification-package branch from c6090d5 to e5a3134 Compare June 20, 2025 20:13
@epage
Copy link
Contributor

epage commented Jun 20, 2025

Feel free to edit the commits as needed

@mladedav mladedav force-pushed the dm/unification-package branch 3 times, most recently from b10f5d9 to 2457810 Compare June 23, 2025 06:59
@mladedav
Copy link
Author

I've also just added a test with weak dependencies, which works. I tried to also add namespaced dep: features, but those seem to not be unified (#15694) so I'm ignoring that, but I guess that might also go towards the feature unification's unresolved questions.

@mladedav mladedav force-pushed the dm/unification-package branch from 52725c0 to 6c7ca34 Compare June 24, 2025 08:08
@mladedav
Copy link
Author

mladedav commented Jun 24, 2025

I had to also narrow down CliFeatures passed to the feature resolver because otherwise cargo check -p a -p b -F a,b complains that a does not have feature named b. The same code already ran with all selected packages and features so if there were unknown features, it would fail.

However, simply narrowing it down does not work for resolver v1 because there are tests like other_member_from_current which check edge cases regarding a feature of an unselected package inside cwd enabling a feature of a selected package. Since resolver v1 will not be using package feature unification, I just pass all cli features as before and only narrow it down for package feature unification.

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 feature-unification = "selected" or it could be that it just didn't compile and now it does so if you want me to fixup the test commits, feel free to tell me what's optimal.

@epage
Copy link
Contributor

epage commented Jun 24, 2025

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.

@mladedav mladedav force-pushed the dm/unification-package branch from 6c7ca34 to 06c0d91 Compare June 24, 2025 19:00
@mladedav
Copy link
Author

Ok, I've added all the tests with feature-unification = "selected" and change them later to package.

Comment on lines 273 to 294
// 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)
}
Copy link
Contributor

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.

Copy link
Author

@mladedav mladedav Jun 24, 2025

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.

Copy link
Contributor

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.

Copy link
Author

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();
}

Copy link
Contributor

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();
}

@epage
Copy link
Contributor

epage commented Jun 24, 2025

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!

@mladedav mladedav force-pushed the dm/unification-package branch from 06c0d91 to cd1cff4 Compare June 24, 2025 19:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-configuration Area: cargo config files and env vars A-documenting-cargo-itself Area: Cargo's documentation A-workspaces Area: workspaces Command-fix Command-tree S-waiting-on-review Status: Awaiting review from the assignee but also interested parties.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants