From 70f16f448baf0abc63ae5aefe5f61253a9b60af5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Ml=C3=A1dek?= Date: Thu, 19 Jun 2025 16:21:36 +0200 Subject: [PATCH] feat: add the `package` feature unification option --- src/cargo/ops/resolve.rs | 22 ++++ src/cargo/util/context/mod.rs | 1 + tests/testsuite/feature_unification.rs | 163 +++++++------------------ 3 files changed, 65 insertions(+), 121 deletions(-) diff --git a/src/cargo/ops/resolve.rs b/src/cargo/ops/resolve.rs index 7b1fdba70..9e48f35a2 100644 --- a/src/cargo/ops/resolve.rs +++ b/src/cargo/ops/resolve.rs @@ -162,6 +162,7 @@ pub fn resolve_ws_with_opts<'gctx>( FeatureUnification::Workspace => { vec![ops::Packages::All(Vec::new()).to_package_id_specs(ws)?] } + FeatureUnification::Package => specs.iter().map(|spec| vec![spec.clone()]).collect(), }; let specs: Vec<_> = individual_specs .iter() @@ -269,7 +270,28 @@ pub fn resolve_ws_with_opts<'gctx>( for specs in individual_specs { let feature_opts = FeatureOpts::new(ws, has_dev_units, force_all_targets)?; + // 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) + } FeatureUnification::Selected | FeatureUnification::Workspace => { Cow::Borrowed(cli_features) } diff --git a/src/cargo/util/context/mod.rs b/src/cargo/util/context/mod.rs index 356070157..4b079576b 100644 --- a/src/cargo/util/context/mod.rs +++ b/src/cargo/util/context/mod.rs @@ -2864,6 +2864,7 @@ pub enum IncompatibleRustVersions { #[derive(Copy, Clone, Debug, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum FeatureUnification { + Package, Selected, Workspace, } diff --git a/tests/testsuite/feature_unification.rs b/tests/testsuite/feature_unification.rs index 0b9c07bee..ea34fed42 100644 --- a/tests/testsuite/feature_unification.rs +++ b/tests/testsuite/feature_unification.rs @@ -136,7 +136,7 @@ fn package_feature_unification() { ".cargo/config.toml", r#" [resolver] - feature-unification = "selected" + feature-unification = "package" "#, ) .file( @@ -245,14 +245,21 @@ fn package_feature_unification() { p.cargo("check -p a -p b") .arg("-Zfeature-unification") .masquerade_as_nightly_cargo(&["feature-unification"]) - .with_status(101) - .with_stderr_contains("[ERROR] features were unified") + .with_stderr_data( + str![[r#" +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]] + .unordered(), + ) .run(); p.cargo("check") .arg("-Zfeature-unification") .masquerade_as_nightly_cargo(&["feature-unification"]) - .with_status(101) - .with_stderr_contains("[ERROR] features were unified") + .with_stderr_data(str![[r#" +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) .run(); // Sanity check that compilation without package feature unification does not work p.cargo("check -p a -p b") @@ -271,7 +278,7 @@ fn package_feature_unification_default_features() { ".cargo/config.toml", r#" [resolver] - feature-unification = "selected" + feature-unification = "package" "#, ) .file( @@ -366,17 +373,9 @@ fn package_feature_unification_default_features() { p.cargo("check") .arg("-Zfeature-unification") .masquerade_as_nightly_cargo(&["feature-unification"]) - .with_status(101) .with_stderr_data( str![[r#" -[CHECKING] common v0.1.0 ([ROOT]/foo/common) -[ERROR] features were unified - --> common/src/lib.rs:3:17 - | -3 | compile_error!("features were unified"); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -[ERROR] could not compile `common` (lib) due to 1 previous error +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]] .unordered(), @@ -407,7 +406,7 @@ fn package_feature_unification_cli_features() { ".cargo/config.toml", r#" [resolver] - feature-unification = "selected" + feature-unification = "package" "#, ) .file( @@ -481,30 +480,17 @@ fn package_feature_unification_cli_features() { p.cargo("check -p a -p b -F a,b") .arg("-Zfeature-unification") .masquerade_as_nightly_cargo(&["feature-unification"]) - .with_status(101) .with_stderr_data( str![[r#" +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s [CHECKING] common v0.1.0 ([ROOT]/foo/common) -[ERROR] features were unified - --> common/src/lib.rs:3:17 - | -3 | compile_error!("features were unified"); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -[ERROR] could not compile `common` (lib) due to 1 previous error [UPDATING] `dummy-registry` index [LOCKING] 1 package to latest compatible version [DOWNLOADING] crates ... [DOWNLOADED] outside v0.1.0 (registry `dummy-registry`) [CHECKING] outside v0.1.0 -[ERROR] features were unified - --> [ROOT]/home/.cargo/registry/src/-[HASH]/outside-0.1.0/src/lib.rs:3:17 - | -3 | compile_error!("features were unified"); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -[ERROR] could not compile `outside` (lib) due to 1 previous error -[WARNING] build failed, waiting for other jobs to finish... +[CHECKING] b v0.1.0 ([ROOT]/foo/b) +[CHECKING] a v0.1.0 ([ROOT]/foo/a) "#]] .unordered(), @@ -513,26 +499,9 @@ fn package_feature_unification_cli_features() { p.cargo("check --workspace --exclude common -F a,b") .arg("-Zfeature-unification") .masquerade_as_nightly_cargo(&["feature-unification"]) - .with_status(101) .with_stderr_data( str![[r#" -[CHECKING] outside v0.1.0 -[CHECKING] common v0.1.0 ([ROOT]/foo/common) -[ERROR] features were unified - --> common/src/lib.rs:3:17 - | -3 | compile_error!("features were unified"); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -[ERROR] features were unified - --> [ROOT]/home/.cargo/registry/src/-[HASH]/outside-0.1.0/src/lib.rs:3:17 - | -3 | compile_error!("features were unified"); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -[ERROR] could not compile `outside` (lib) due to 1 previous error -[WARNING] build failed, waiting for other jobs to finish... -[ERROR] could not compile `common` (lib) due to 1 previous error +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]] .unordered(), @@ -542,26 +511,9 @@ fn package_feature_unification_cli_features() { p.cargo("check -p a -p b -F a/a,b/b") .arg("-Zfeature-unification") .masquerade_as_nightly_cargo(&["feature-unification"]) - .with_status(101) .with_stderr_data( str![[r#" -[CHECKING] outside v0.1.0 -[CHECKING] common v0.1.0 ([ROOT]/foo/common) -[ERROR] features were unified - --> common/src/lib.rs:3:17 - | -3 | compile_error!("features were unified"); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -[ERROR] features were unified - --> [ROOT]/home/.cargo/registry/src/-[HASH]/outside-0.1.0/src/lib.rs:3:17 - | -3 | compile_error!("features were unified"); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -[ERROR] could not compile `common` (lib) due to 1 previous error -[WARNING] build failed, waiting for other jobs to finish... -[ERROR] could not compile `outside` (lib) due to 1 previous error +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]] .unordered(), @@ -618,7 +570,7 @@ fn package_feature_unification_weak_dependencies() { ".cargo/config.toml", r#" [resolver] - feature-unification = "selected" + feature-unification = "package" "#, ) .file( @@ -690,17 +642,12 @@ fn package_feature_unification_weak_dependencies() { p.cargo("check -p a -p b") .arg("-Zfeature-unification") .masquerade_as_nightly_cargo(&["feature-unification"]) - .with_status(101) .with_stderr_data( str![[r#" [CHECKING] common v0.1.0 ([ROOT]/foo/common) -[ERROR] features were unified - --> common/src/lib.rs:3:17 - | -3 | compile_error!("features were unified"); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -[ERROR] could not compile `common` (lib) due to 1 previous error +[CHECKING] a v0.1.0 ([ROOT]/foo/a) +[CHECKING] b v0.1.0 ([ROOT]/foo/b) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]] .unordered(), @@ -709,16 +656,9 @@ fn package_feature_unification_weak_dependencies() { p.cargo("check") .arg("-Zfeature-unification") .masquerade_as_nightly_cargo(&["feature-unification"]) - .with_status(101) .with_stderr_data(str![[r#" [CHECKING] common v0.1.0 ([ROOT]/foo/common) -[ERROR] features were unified - --> common/src/lib.rs:3:17 - | -3 | compile_error!("features were unified"); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -[ERROR] could not compile `common` (lib) due to 1 previous error +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]]) .run(); @@ -873,27 +813,27 @@ common v0.1.0 ([ROOT]/foo/common) p.cargo("tree -e features") .arg("-Zfeature-unification") .masquerade_as_nightly_cargo(&["feature-unification"]) - .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "selected") // To be changed to package later + .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "package") .with_stdout_data(str![[r#" +common v0.1.0 ([ROOT]/foo/common) a v0.1.0 ([ROOT]/foo/a) ├── common feature "a" │ └── common v0.1.0 ([ROOT]/foo/common) -├── common feature "default" (command-line) +├── common feature "default" │ └── common v0.1.0 ([ROOT]/foo/common) ├── outside feature "a" │ └── outside v0.1.0 └── outside feature "default" └── outside v0.1.0 - b v0.1.0 ([ROOT]/foo/b) ├── common feature "b" │ └── common v0.1.0 ([ROOT]/foo/common) -├── common feature "default" (command-line) (*) +├── common feature "default" +│ └── common v0.1.0 ([ROOT]/foo/common) ├── outside feature "b" │ └── outside v0.1.0 -└── outside feature "default" (*) - -common v0.1.0 ([ROOT]/foo/common) +└── outside feature "default" + └── outside v0.1.0 "#]]) .run(); @@ -973,7 +913,7 @@ fn cargo_install_ignores_config() { .arg(p.root()) .arg("-Zfeature-unification") .masquerade_as_nightly_cargo(&["feature-unification"]) - .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "selected") + .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "package") .with_stderr_data(str![[r#" [INSTALLING] a v0.1.0 ([ROOT]/foo) [FINISHED] `release` profile [optimized] target(s) in [ELAPSED]s @@ -1029,7 +969,7 @@ edition = "2021" .build(); p.cargo("fix --edition --allow-no-vcs") - .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "selected") + .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "package") .arg("-Zfeature-unification") .masquerade_as_nightly_cargo(&["feature-unification"]) .with_stderr_data(str![[r#" @@ -1111,10 +1051,10 @@ fn edition_v2_resolver_report() { .build(); p.cargo("fix --edition --allow-no-vcs --workspace") - .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "selected") + .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "package") .arg("-Zfeature-unification") .masquerade_as_nightly_cargo(&["feature-unification"]) - .with_status(0) + .with_status(101) .with_stderr_data( str![[r#" [MIGRATING] Cargo.toml from 2018 edition to 2021 @@ -1125,26 +1065,7 @@ fn edition_v2_resolver_report() { [DOWNLOADED] bar v1.0.0 (registry `dummy-registry`) [DOWNLOADED] opt_dep v1.0.0 (registry `dummy-registry`) [MIGRATING] bar/Cargo.toml from 2018 edition to 2021 -[NOTE] Switching to Edition 2021 will enable the use of the version 2 feature resolver in Cargo. -This may cause some dependencies to be built with fewer features enabled than previously. -More information about the resolver changes may be found at https://doc.rust-lang.org/nightly/edition-guide/rust-2021/default-cargo-resolver.html -When building the following dependencies, the given features will no longer be used: - - common v1.0.0 removed features: dev-feat, f1, opt_dep - common v1.0.0 (as host dependency) removed features: dev-feat, f1 - -The following differences only apply when building with dev-dependencies: - - common v1.0.0 removed features: f1, opt_dep - -[CHECKING] opt_dep v1.0.0 -[CHECKING] bar v1.0.0 -[CHECKING] bar v0.1.0 ([ROOT]/foo/bar) -[MIGRATING] bar/src/lib.rs from 2018 edition to 2021 -[CHECKING] common v1.0.0 -[CHECKING] foo v0.1.0 ([ROOT]/foo) -[MIGRATING] src/lib.rs from 2018 edition to 2021 -[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s +[ERROR] cannot fix edition when using `feature-unification = "package"`. "#]] .unordered(), @@ -1213,7 +1134,7 @@ fn feature_unification_of_cli_features_within_workspace() { p.cargo("check -p parent -F grandchild/a") .arg("-Zfeature-unification") .masquerade_as_nightly_cargo(&["feature-unification"]) - // .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "package") + .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "package") .with_status(101) .with_stderr_data(str![[r#" [ERROR] the package 'parent' does not contain this feature: grandchild/a @@ -1246,7 +1167,7 @@ fn feature_unification_of_cli_features_within_workspace() { p.cargo("check -p child -F grandchild/a") .arg("-Zfeature-unification") .masquerade_as_nightly_cargo(&["feature-unification"]) - // .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "package") + .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "package") .with_stderr_data(str![[r#" [CHECKING] grandchild v0.1.0 ([ROOT]/foo/grandchild) [CHECKING] child v0.1.0 ([ROOT]/foo/child) @@ -1278,7 +1199,7 @@ fn feature_unification_of_cli_features_within_workspace() { p.cargo("check -F grandchild/a") .arg("-Zfeature-unification") .masquerade_as_nightly_cargo(&["feature-unification"]) - // .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "package") + .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "package") .with_stderr_data(str![[r#" [CHECKING] parent v0.1.0 ([ROOT]/foo/parent) [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s @@ -1309,7 +1230,7 @@ fn feature_unification_of_cli_features_within_workspace() { p.cargo("check -F grandchild/a --workspace --exclude grandchild") .arg("-Zfeature-unification") .masquerade_as_nightly_cargo(&["feature-unification"]) - // .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "package") + .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "package") .with_stderr_data(str![[r#" [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s @@ -1339,7 +1260,7 @@ fn feature_unification_of_cli_features_within_workspace() { p.cargo("check -F grandchild/a --workspace --exclude grandchild --exclude child") .arg("-Zfeature-unification") .masquerade_as_nightly_cargo(&["feature-unification"]) - // .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "package") + .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "package") .with_status(101) .with_stderr_data(str![[r#" [ERROR] the package 'parent' does not contain this feature: grandchild/a