From 403f1e12f6469fd7ba5f2c776461a47963ea856d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Ml=C3=A1dek?= Date: Thu, 19 Jun 2025 22:40:54 +0200 Subject: [PATCH 1/5] tests: add future `package` feature unification tests --- tests/testsuite/feature_unification.rs | 1167 +++++++++++++++++++++++- 1 file changed, 1166 insertions(+), 1 deletion(-) diff --git a/tests/testsuite/feature_unification.rs b/tests/testsuite/feature_unification.rs index f5337336aac..0b9c07bee07 100644 --- a/tests/testsuite/feature_unification.rs +++ b/tests/testsuite/feature_unification.rs @@ -2,7 +2,13 @@ use crate::prelude::*; use crate::utils::cargo_process; -use cargo_test_support::{basic_manifest, project, str}; +use cargo_test_support::{ + basic_manifest, + compare::assert_e2e, + project, + registry::{Dependency, Package}, + str, +}; #[cargo_test] fn workspace_feature_unification() { @@ -107,6 +113,792 @@ fn workspace_feature_unification() { .run(); } +#[cargo_test] +fn package_feature_unification() { + Package::new("outside", "0.1.0") + .feature("a", &[]) + .feature("b", &[]) + .file( + "src/lib.rs", + r#" + #[cfg(all(feature = "a", feature = "b"))] + compile_error!("features were unified"); + #[cfg(feature = "a")] + pub fn a() {} + #[cfg(feature = "b")] + pub fn b() {} + "#, + ) + .publish(); + + let p = project() + .file( + ".cargo/config.toml", + r#" + [resolver] + feature-unification = "selected" + "#, + ) + .file( + "Cargo.toml", + r#" + [workspace] + resolver = "2" + members = ["common", "a", "b"] + "#, + ) + .file( + "common/Cargo.toml", + r#" + [package] + name = "common" + version = "0.1.0" + edition = "2021" + + [features] + a = [] + b = [] + "#, + ) + .file( + "common/src/lib.rs", + r#" + #[cfg(all(feature = "a", feature = "b"))] + compile_error!("features were unified"); + #[cfg(feature = "a")] + pub fn a() {} + #[cfg(feature = "b")] + pub fn b() {} + "#, + ) + .file( + "a/Cargo.toml", + r#" + [package] + name = "a" + version = "0.1.0" + edition = "2021" + + [dependencies] + common = { path = "../common", features = ["a"] } + outside = { version = "0.1.0", features = ["a"] } + "#, + ) + .file("a/src/lib.rs", "pub use common::a;") + .file( + "b/Cargo.toml", + r#" + [package] + name = "b" + version = "0.1.0" + edition = "2021" + + [dependencies] + common = { path = "../common", features = ["b"] } + outside = { version = "0.1.0", features = ["b"] } + "#, + ) + .file("b/src/lib.rs", "pub use common::b;") + .build(); + + p.cargo("check -p common") + .arg("-Zfeature-unification") + .masquerade_as_nightly_cargo(&["feature-unification"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[CHECKING] common v0.1.0 ([ROOT]/foo/common) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); + p.cargo("check -p a") + .arg("-Zfeature-unification") + .masquerade_as_nightly_cargo(&["feature-unification"]) + .with_stderr_data( + str![[r#" +[DOWNLOADING] crates ... +[DOWNLOADED] outside v0.1.0 (registry `dummy-registry`) +[CHECKING] outside v0.1.0 +[CHECKING] common v0.1.0 ([ROOT]/foo/common) +[CHECKING] a v0.1.0 ([ROOT]/foo/a) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]] + .unordered(), + ) + .run(); + p.cargo("check -p b") + .arg("-Zfeature-unification") + .masquerade_as_nightly_cargo(&["feature-unification"]) + .with_stderr_data( + str![[r#" +[CHECKING] outside v0.1.0 +[CHECKING] common v0.1.0 ([ROOT]/foo/common) +[CHECKING] b v0.1.0 ([ROOT]/foo/b) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]] + .unordered(), + ) + .run(); + 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") + .run(); + p.cargo("check") + .arg("-Zfeature-unification") + .masquerade_as_nightly_cargo(&["feature-unification"]) + .with_status(101) + .with_stderr_contains("[ERROR] features were unified") + .run(); + // Sanity check that compilation without package feature unification does not work + p.cargo("check -p a -p b") + .arg("-Zfeature-unification") + .masquerade_as_nightly_cargo(&["feature-unification"]) + .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "selected") + .with_status(101) + .with_stderr_contains("[ERROR] features were unified") + .run(); +} + +#[cargo_test] +fn package_feature_unification_default_features() { + let p = project() + .file( + ".cargo/config.toml", + r#" + [resolver] + feature-unification = "selected" + "#, + ) + .file( + "Cargo.toml", + r#" + [workspace] + resolver = "2" + members = ["common", "a", "b"] + "#, + ) + .file( + "common/Cargo.toml", + r#" + [package] + name = "common" + version = "0.1.0" + edition = "2021" + + [features] + default = ["a"] + a = [] + b = [] + "#, + ) + .file( + "common/src/lib.rs", + r#" + #[cfg(all(feature = "a", feature = "b"))] + compile_error!("features were unified"); + #[cfg(feature = "a")] + pub fn a() {} + #[cfg(feature = "b")] + pub fn b() {} + "#, + ) + .file( + "a/Cargo.toml", + r#" + [package] + name = "a" + version = "0.1.0" + edition = "2021" + + [dependencies] + common = { path = "../common" } + "#, + ) + .file("a/src/lib.rs", "pub use common::a;") + .file( + "b/Cargo.toml", + r#" + [package] + name = "b" + version = "0.1.0" + edition = "2021" + + [dependencies] + common = { path = "../common", features = ["b"], default-features = false } + "#, + ) + .file("b/src/lib.rs", "pub use common::b;") + .build(); + + p.cargo("check -p common") + .arg("-Zfeature-unification") + .masquerade_as_nightly_cargo(&["feature-unification"]) + .with_stderr_data(str![[r#" +[CHECKING] common v0.1.0 ([ROOT]/foo/common) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); + p.cargo("check -p a") + .arg("-Zfeature-unification") + .masquerade_as_nightly_cargo(&["feature-unification"]) + .with_stderr_data(str![[r#" +[CHECKING] a v0.1.0 ([ROOT]/foo/a) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); + p.cargo("check -p b") + .arg("-Zfeature-unification") + .masquerade_as_nightly_cargo(&["feature-unification"]) + .with_stderr_data(str![[r#" +[CHECKING] common v0.1.0 ([ROOT]/foo/common) +[CHECKING] b v0.1.0 ([ROOT]/foo/b) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); + 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 + +"#]] + .unordered(), + ) + .run(); +} + +#[cargo_test] +fn package_feature_unification_cli_features() { + Package::new("outside", "0.1.0") + .feature("a", &[]) + .feature("b", &[]) + .file( + "src/lib.rs", + r#" + #[cfg(all(feature = "a", feature = "b"))] + compile_error!("features were unified"); + #[cfg(feature = "a")] + pub fn a() {} + #[cfg(feature = "b")] + pub fn b() {} + "#, + ) + .publish(); + + let p = project() + .file( + ".cargo/config.toml", + r#" + [resolver] + feature-unification = "selected" + "#, + ) + .file( + "Cargo.toml", + r#" + [workspace] + resolver = "2" + members = ["common", "a", "b"] + "#, + ) + .file( + "common/Cargo.toml", + r#" + [package] + name = "common" + version = "0.1.0" + edition = "2021" + + [features] + a = [] + b = [] + "#, + ) + .file( + "common/src/lib.rs", + r#" + #[cfg(all(feature = "a", feature = "b"))] + compile_error!("features were unified"); + #[cfg(feature = "a")] + pub fn a() {} + #[cfg(feature = "b")] + pub fn b() {} + "#, + ) + .file( + "a/Cargo.toml", + r#" + [package] + name = "a" + version = "0.1.0" + edition = "2021" + + [dependencies] + common = { path = "../common" } + outside = "0.1.0" + + [features] + a = ["common/a", "outside/a"] + "#, + ) + .file("a/src/lib.rs", "pub use common::a;") + .file( + "b/Cargo.toml", + r#" + [package] + name = "b" + version = "0.1.0" + edition = "2021" + + [dependencies] + common = { path = "../common", features = ["b"] } + outside = "0.1.0" + + [features] + b = ["common/b", "outside/b"] + "#, + ) + .file("b/src/lib.rs", "pub use common::b;") + .build(); + + 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#" +[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... + +"#]] + .unordered(), + ) + .run(); + 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 + +"#]] + .unordered(), + ) + .run(); + + 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 + +"#]] + .unordered(), + ) + .run(); + p.cargo("check -p a -p b -F a,b,c") + .arg("-Zfeature-unification") + .masquerade_as_nightly_cargo(&["feature-unification"]) + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] none of the selected packages contains this feature: c +selected packages: a, b + +"#]]) + .run(); + p.cargo("check -p a -F b") + .arg("-Zfeature-unification") + .masquerade_as_nightly_cargo(&["feature-unification"]) + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] the package 'a' does not contain this feature: b +[HELP] packages with the missing feature: common, b + +"#]]) + .run(); + p.cargo("check -p a -F a/a,common/b") + .arg("-Zfeature-unification") + .masquerade_as_nightly_cargo(&["feature-unification"]) + .with_status(101) + .with_stderr_contains("[ERROR] features were unified") + .run(); + + p.cargo("check -p a -F a/a,outside/b") + .arg("-Zfeature-unification") + .masquerade_as_nightly_cargo(&["feature-unification"]) + .with_status(101) + .with_stderr_contains("[ERROR] features were unified") + .run(); + + // Sanity check that compilation without package feature unification does not work + p.cargo("check -p a -p b -F a,b") + .arg("-Zfeature-unification") + .masquerade_as_nightly_cargo(&["feature-unification"]) + .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "selected") + .with_status(101) + .with_stderr_contains("[ERROR] features were unified") + .run(); +} + +#[cargo_test] +fn package_feature_unification_weak_dependencies() { + let p = project() + .file( + ".cargo/config.toml", + r#" + [resolver] + feature-unification = "selected" + "#, + ) + .file( + "Cargo.toml", + r#" + [workspace] + resolver = "2" + members = ["common", "a", "b"] + "#, + ) + .file( + "common/Cargo.toml", + r#" + [package] + name = "common" + version = "0.1.0" + edition = "2021" + + [features] + a = [] + b = [] + "#, + ) + .file( + "common/src/lib.rs", + r#" + #[cfg(all(feature = "a", feature = "b"))] + compile_error!("features were unified"); + #[cfg(feature = "a")] + pub fn a() {} + #[cfg(feature = "b")] + pub fn b() {} + "#, + ) + .file( + "a/Cargo.toml", + r#" + [package] + name = "a" + version = "0.1.0" + edition = "2021" + + [dependencies] + common = { path = "../common", optional = true } + + [features] + default = ["dep:common", "common?/a"] + "#, + ) + .file("a/src/lib.rs", "pub use common::a;") + .file( + "b/Cargo.toml", + r#" + [package] + name = "b" + version = "0.1.0" + edition = "2021" + + [dependencies] + common = { path = "../common", optional = true } + + [features] + default = ["dep:common", "common?/b"] + "#, + ) + .file("b/src/lib.rs", "pub use common::b;") + .build(); + + 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 + +"#]] + .unordered(), + ) + .run(); + 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 + +"#]]) + .run(); + + // Sanity check that compilation without package feature unification does not work + p.cargo("check -p a -p b") + .arg("-Zfeature-unification") + .masquerade_as_nightly_cargo(&["feature-unification"]) + .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "selected") + .with_status(101) + .with_stderr_contains("[ERROR] features were unified") + .run(); +} + +#[cargo_test] +fn feature_unification_cargo_tree() { + Package::new("outside", "0.1.0") + .feature("a", &[]) + .feature("b", &[]) + .file( + "src/lib.rs", + r#" + #[cfg(all(feature = "a", feature = "b"))] + compile_error!("features were unified"); + #[cfg(feature = "a")] + pub fn a() {} + #[cfg(feature = "b")] + pub fn b() {} + "#, + ) + .publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [workspace] + resolver = "2" + members = ["common", "a", "b"] + "#, + ) + .file( + "common/Cargo.toml", + r#" + [package] + name = "common" + version = "0.1.0" + edition = "2021" + + [features] + a = [] + b = [] + "#, + ) + .file( + "common/src/lib.rs", + r#" + #[cfg(all(feature = "a", feature = "b"))] + compile_error!("features were unified"); + #[cfg(feature = "a")] + pub fn a() {} + #[cfg(feature = "b")] + pub fn b() {} + "#, + ) + .file( + "a/Cargo.toml", + r#" + [package] + name = "a" + version = "0.1.0" + edition = "2021" + + [dependencies] + common = { path = "../common", features = ["a"] } + outside = { version = "0.1.0", features = ["a"] } + "#, + ) + .file("a/src/lib.rs", "pub use common::a;") + .file( + "b/Cargo.toml", + r#" + [package] + name = "b" + version = "0.1.0" + edition = "2021" + + [dependencies] + common = { path = "../common", features = ["b"] } + outside = { version = "0.1.0", features = ["b"] } + "#, + ) + .file("b/src/lib.rs", "pub use common::b;") + .build(); + + p.cargo("tree -e features") + .arg("-Zfeature-unification") + .masquerade_as_nightly_cargo(&["feature-unification"]) + .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "selected") + .with_stdout_data(str![[r#" +a v0.1.0 ([ROOT]/foo/a) +├── common feature "a" +│ └── common v0.1.0 ([ROOT]/foo/common) +├── common feature "default" (command-line) +│ └── 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) (*) +├── outside feature "b" +│ └── outside v0.1.0 +└── outside feature "default" (*) + +common v0.1.0 ([ROOT]/foo/common) + +"#]]) + .run(); + + p.cargo("tree -e features") + .arg("-Zfeature-unification") + .masquerade_as_nightly_cargo(&["feature-unification"]) + .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "workspace") + .with_stdout_data(str![[r#" +a v0.1.0 ([ROOT]/foo/a) +├── common feature "a" +│ └── common v0.1.0 ([ROOT]/foo/common) +├── common feature "default" (command-line) +│ └── 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) (*) +├── outside feature "b" +│ └── outside v0.1.0 +└── outside feature "default" (*) + +common v0.1.0 ([ROOT]/foo/common) + +"#]]) + .run(); + + 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 + .with_stdout_data(str![[r#" +a v0.1.0 ([ROOT]/foo/a) +├── common feature "a" +│ └── common v0.1.0 ([ROOT]/foo/common) +├── common feature "default" (command-line) +│ └── 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) (*) +├── outside feature "b" +│ └── outside v0.1.0 +└── outside feature "default" (*) + +common v0.1.0 ([ROOT]/foo/common) + +"#]]) + .run(); +} + #[cargo_test] fn cargo_install_ignores_config() { let p = project() @@ -175,6 +967,20 @@ fn cargo_install_ignores_config() { [INSTALLED] package `a v0.1.0 ([ROOT]/foo)` (executable `a[EXE]`) [WARNING] be sure to add `[ROOT]/home/.cargo/bin` to your PATH to be able to run the installed binaries +"#]]) + .run(); + cargo_process("install --path") + .arg(p.root()) + .arg("-Zfeature-unification") + .masquerade_as_nightly_cargo(&["feature-unification"]) + .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "selected") + .with_stderr_data(str![[r#" +[INSTALLING] a v0.1.0 ([ROOT]/foo) +[FINISHED] `release` profile [optimized] target(s) in [ELAPSED]s +[REPLACING] [ROOT]/home/.cargo/bin/a[EXE] +[REPLACED] package `a v0.1.0 ([ROOT]/foo)` with `a v0.1.0 ([ROOT]/foo)` (executable `a[EXE]`) +[WARNING] be sure to add `[ROOT]/home/.cargo/bin` to your PATH to be able to run the installed binaries + "#]]) .run(); } @@ -204,3 +1010,362 @@ fn unstable_config_on_stable() { "#]]) .run(); } + +#[cargo_test] +fn cargo_fix_works() { + let p = project() + .file( + "Cargo.toml", + r#" +# Before project +[ project ] # After project header +# After project header line +name = "foo" +edition = "2021" +# After project table +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("fix --edition --allow-no-vcs") + .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "selected") + .arg("-Zfeature-unification") + .masquerade_as_nightly_cargo(&["feature-unification"]) + .with_stderr_data(str![[r#" +[MIGRATING] Cargo.toml from 2021 edition to 2024 +[FIXED] Cargo.toml (1 fix) +[CHECKING] foo v0.0.0 ([ROOT]/foo) +[MIGRATING] src/lib.rs from 2021 edition to 2024 +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); + assert_e2e().eq( + p.read_file("Cargo.toml"), + str![[r#" + +# Before project +[ package ] # After project header +# After project header line +name = "foo" +edition = "2021" +# After project table + +"#]], + ); +} + +#[cargo_test] +fn edition_v2_resolver_report() { + // Show a report if the V2 resolver shows differences. + Package::new("common", "1.0.0") + .feature("f1", &[]) + .feature("dev-feat", &[]) + .add_dep(Dependency::new("opt_dep", "1.0").optional(true)) + .publish(); + Package::new("opt_dep", "1.0.0").publish(); + + Package::new("bar", "1.0.0") + .add_dep( + Dependency::new("common", "1.0") + .target("cfg(whatever)") + .enable_features(&["f1"]), + ) + .publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [workspace] + members = ["bar"] + + [package] + name = "foo" + version = "0.1.0" + edition = "2018" + + [dependencies] + common = "1.0" + bar = "1.0" + + [build-dependencies] + common = { version = "1.0", features = ["opt_dep"] } + + [dev-dependencies] + common = { version="1.0", features=["dev-feat"] } + "#, + ) + .file("src/lib.rs", "") + .file( + "bar/Cargo.toml", + r#" + [package] + name = "bar" + version = "0.1.0" + edition = "2018" + "#, + ) + .file("bar/src/lib.rs", "") + .build(); + + p.cargo("fix --edition --allow-no-vcs --workspace") + .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "selected") + .arg("-Zfeature-unification") + .masquerade_as_nightly_cargo(&["feature-unification"]) + .with_status(0) + .with_stderr_data( + str![[r#" +[MIGRATING] Cargo.toml from 2018 edition to 2021 +[UPDATING] `dummy-registry` index +[LOCKING] 3 packages to latest compatible versions +[DOWNLOADING] crates ... +[DOWNLOADED] common v1.0.0 (registry `dummy-registry`) +[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 + +"#]] + .unordered(), + ) + .run(); +} + +#[cargo_test] +fn feature_unification_of_cli_features_within_workspace() { + let p = project() + .file( + "Cargo.toml", + r#" + [workspace] + resolver = "2" + members = ["parent", "child", "grandchild"] + "#, + ) + .file( + "grandchild/Cargo.toml", + r#" + [package] + name = "grandchild" + version = "0.1.0" + edition = "2021" + + [features] + a = [] + "#, + ) + .file( + "grandchild/src/lib.rs", + r#" + #[cfg(feature = "a")] + pub fn a() {} + "#, + ) + .file( + "child/Cargo.toml", + r#" + [package] + name = "child" + version = "0.1.0" + edition = "2021" + + [dependencies] + grandchild = { path = "../grandchild" } + "#, + ) + .file("child/src/lib.rs", "pub use grandchild::*;") + .file( + "parent/Cargo.toml", + r#" + [package] + name = "parent" + version = "0.1.0" + edition = "2021" + + [dependencies] + child = { path = "../child" } + "#, + ) + .file("parent/src/lib.rs", "pub use child::a;") + .build(); + + p.cargo("check -p parent -F grandchild/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 'parent' does not contain this feature: grandchild/a + +"#]]) + .run(); + + p.cargo("check -p parent -F grandchild/a") + .arg("-Zfeature-unification") + .masquerade_as_nightly_cargo(&["feature-unification"]) + .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "workspace") + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] the package 'parent' does not contain this feature: grandchild/a + +"#]]) + .run(); + + p.cargo("check -p parent -F grandchild/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 'parent' does not contain this feature: grandchild/a + +"#]]) + .run(); + + p.cargo("check -p child -F grandchild/a") + .arg("-Zfeature-unification") + .masquerade_as_nightly_cargo(&["feature-unification"]) + // .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) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); + + p.cargo("check -p child -F grandchild/a") + .arg("-Zfeature-unification") + .masquerade_as_nightly_cargo(&["feature-unification"]) + .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "workspace") + .with_stderr_data(str![[r#" +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); + + p.cargo("check -p child -F grandchild/a") + .arg("-Zfeature-unification") + .masquerade_as_nightly_cargo(&["feature-unification"]) + .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "selected") + .with_stderr_data(str![[r#" +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); + + p.cargo("check -F grandchild/a") + .arg("-Zfeature-unification") + .masquerade_as_nightly_cargo(&["feature-unification"]) + // .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 + +"#]]) + .run(); + + p.cargo("check -F grandchild/a") + .arg("-Zfeature-unification") + .masquerade_as_nightly_cargo(&["feature-unification"]) + .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "workspace") + .with_stderr_data(str![[r#" +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); + + p.cargo("check -F grandchild/a") + .arg("-Zfeature-unification") + .masquerade_as_nightly_cargo(&["feature-unification"]) + .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "selected") + .with_stderr_data(str![[r#" +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); + + p.cargo("check -F grandchild/a --workspace --exclude grandchild") + .arg("-Zfeature-unification") + .masquerade_as_nightly_cargo(&["feature-unification"]) + // .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "package") + .with_stderr_data(str![[r#" +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); + + p.cargo("check -F grandchild/a --workspace --exclude grandchild") + .arg("-Zfeature-unification") + .masquerade_as_nightly_cargo(&["feature-unification"]) + .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "workspace") + .with_stderr_data(str![[r#" +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); + + p.cargo("check -F grandchild/a --workspace --exclude grandchild") + .arg("-Zfeature-unification") + .masquerade_as_nightly_cargo(&["feature-unification"]) + .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "selected") + .with_stderr_data(str![[r#" +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); + + 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") + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] the package 'parent' does not contain this feature: grandchild/a + +"#]]) + .run(); + + 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", "workspace") + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] the package 'parent' does not contain this feature: grandchild/a + +"#]]) + .run(); + + 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", "selected") + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] the package 'parent' does not contain this feature: grandchild/a + +"#]]) + .run(); +} From 73b5b33e1d60f094dc3adbbb8e8a91f3de2122d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Ml=C3=A1dek?= Date: Thu, 19 Jun 2025 16:20:18 +0200 Subject: [PATCH 2/5] refactor(resolve): allow multiple resolved feature sets in workspace resolve --- src/cargo/core/compiler/standard_lib.rs | 9 +- src/cargo/ops/cargo_compile/mod.rs | 145 ++++++++++++++---------- src/cargo/ops/fix/mod.rs | 10 +- src/cargo/ops/resolve.rs | 95 +++++++++++----- src/cargo/ops/tree/mod.rs | 113 +++++++++--------- 5 files changed, 224 insertions(+), 148 deletions(-) diff --git a/src/cargo/core/compiler/standard_lib.rs b/src/cargo/core/compiler/standard_lib.rs index 30f7c1815fd..17cb4f661e0 100644 --- a/src/cargo/core/compiler/standard_lib.rs +++ b/src/cargo/core/compiler/standard_lib.rs @@ -96,7 +96,7 @@ pub fn resolve_std<'gctx>( &features, /*all_features*/ false, /*uses_default_features*/ false, )?; let dry_run = false; - let resolve = ops::resolve_ws_with_opts( + let mut resolve = ops::resolve_ws_with_opts( &std_ws, target_data, &build_config.requested_kinds, @@ -106,10 +106,15 @@ pub fn resolve_std<'gctx>( crate::core::resolver::features::ForceAllTargets::No, dry_run, )?; + debug_assert_eq!(resolve.specs_and_features.len(), 1); Ok(( resolve.pkg_set, resolve.targeted_resolve, - resolve.resolved_features, + resolve + .specs_and_features + .pop() + .expect("resolve should have a single spec with resolved features") + .resolved_features, )) } diff --git a/src/cargo/ops/cargo_compile/mod.rs b/src/cargo/ops/cargo_compile/mod.rs index 366056ef288..0090974ec54 100644 --- a/src/cargo/ops/cargo_compile/mod.rs +++ b/src/cargo/ops/cargo_compile/mod.rs @@ -52,7 +52,7 @@ use crate::core::resolver::{HasDevUnits, Resolve}; use crate::core::{PackageId, PackageSet, SourceId, TargetKind, Workspace}; use crate::drop_println; use crate::ops; -use crate::ops::resolve::WorkspaceResolve; +use crate::ops::resolve::{SpecsAndResolvedFeatures, WorkspaceResolve}; use crate::util::context::{GlobalContext, WarningHandling}; use crate::util::interning::InternedString; use crate::util::{CargoResult, StableHasher}; @@ -284,7 +284,7 @@ pub fn create_bcx<'a, 'gctx>( mut pkg_set, workspace_resolve, targeted_resolve: resolve, - resolved_features, + specs_and_features, } = resolve; let std_resolve_features = if let Some(crates) = &gctx.cli_unstable().build_std { @@ -363,72 +363,91 @@ pub fn create_bcx<'a, 'gctx>( }) .collect(); - // Passing `build_config.requested_kinds` instead of - // `explicit_host_kinds` here so that `generate_root_units` can do - // its own special handling of `CompileKind::Host`. It will - // internally replace the host kind by the `explicit_host_kind` - // before setting as a unit. - let generator = UnitGenerator { - ws, - packages: &to_builds, - spec, - target_data: &target_data, - filter, - requested_kinds: &build_config.requested_kinds, - explicit_host_kind, - intent: build_config.intent, - resolve: &resolve, - workspace_resolve: &workspace_resolve, - resolved_features: &resolved_features, - package_set: &pkg_set, - profiles: &profiles, - interner, - has_dev_units, - }; - let mut units = generator.generate_root_units()?; + let mut units = Vec::new(); + let mut unit_graph = HashMap::new(); + let mut scrape_units = Vec::new(); - if let Some(args) = target_rustc_crate_types { - override_rustc_crate_types(&mut units, args, interner)?; - } + for SpecsAndResolvedFeatures { + specs, + resolved_features, + } in &specs_and_features + { + // Passing `build_config.requested_kinds` instead of + // `explicit_host_kinds` here so that `generate_root_units` can do + // its own special handling of `CompileKind::Host`. It will + // internally replace the host kind by the `explicit_host_kind` + // before setting as a unit. + let spec_names = specs.iter().map(|spec| spec.name()).collect::>(); + let packages = to_builds + .iter() + .filter(|package| spec_names.contains(&package.name().as_str())) + .cloned() + .collect::>(); + let generator = UnitGenerator { + ws, + packages: &packages, + spec, + target_data: &target_data, + filter, + requested_kinds: &build_config.requested_kinds, + explicit_host_kind, + intent: build_config.intent, + resolve: &resolve, + workspace_resolve: &workspace_resolve, + resolved_features: &resolved_features, + package_set: &pkg_set, + profiles: &profiles, + interner, + has_dev_units, + }; + let mut targeted_root_units = generator.generate_root_units()?; - let should_scrape = build_config.intent.is_doc() && gctx.cli_unstable().rustdoc_scrape_examples; - let mut scrape_units = if should_scrape { - generator.generate_scrape_units(&units)? - } else { - Vec::new() - }; + if let Some(args) = target_rustc_crate_types { + override_rustc_crate_types(&mut targeted_root_units, args, interner)?; + } + + let should_scrape = + build_config.intent.is_doc() && gctx.cli_unstable().rustdoc_scrape_examples; + let targeted_scrape_units = if should_scrape { + generator.generate_scrape_units(&targeted_root_units)? + } else { + Vec::new() + }; + + let std_roots = if let Some(crates) = gctx.cli_unstable().build_std.as_ref() { + let (std_resolve, std_features) = std_resolve_features.as_ref().unwrap(); + standard_lib::generate_std_roots( + &crates, + &targeted_root_units, + std_resolve, + std_features, + &explicit_host_kinds, + &pkg_set, + interner, + &profiles, + &target_data, + )? + } else { + Default::default() + }; - let std_roots = if let Some(crates) = gctx.cli_unstable().build_std.as_ref() { - let (std_resolve, std_features) = std_resolve_features.as_ref().unwrap(); - standard_lib::generate_std_roots( - &crates, - &units, - std_resolve, - std_features, - &explicit_host_kinds, + unit_graph.extend(build_unit_dependencies( + ws, &pkg_set, - interner, - &profiles, + &resolve, + &resolved_features, + std_resolve_features.as_ref(), + &targeted_root_units, + &targeted_scrape_units, + &std_roots, + build_config.intent, &target_data, - )? - } else { - Default::default() - }; - - let mut unit_graph = build_unit_dependencies( - ws, - &pkg_set, - &resolve, - &resolved_features, - std_resolve_features.as_ref(), - &units, - &scrape_units, - &std_roots, - build_config.intent, - &target_data, - &profiles, - interner, - )?; + &profiles, + interner, + )?); + units.extend(targeted_root_units); + scrape_units.extend(targeted_scrape_units); + } // TODO: In theory, Cargo should also dedupe the roots, but I'm uncertain // what heuristics to use in that case. diff --git a/src/cargo/ops/fix/mod.rs b/src/cargo/ops/fix/mod.rs index 56ba396b0f1..89d67993ff6 100644 --- a/src/cargo/ops/fix/mod.rs +++ b/src/cargo/ops/fix/mod.rs @@ -588,7 +588,15 @@ fn check_resolver_change<'gctx>( feature_opts, )?; - let diffs = v2_features.compare_legacy(&ws_resolve.resolved_features); + if ws_resolve.specs_and_features.len() != 1 { + bail!(r#"cannot fix edition when using `feature-unification = "package"`."#); + } + let resolved_features = &ws_resolve + .specs_and_features + .first() + .expect("We've already checked that there is exactly one.") + .resolved_features; + let diffs = v2_features.compare_legacy(resolved_features); Ok((ws_resolve, diffs)) }; let (_, without_dev_diffs) = resolve_differences(HasDevUnits::No)?; diff --git a/src/cargo/ops/resolve.rs b/src/cargo/ops/resolve.rs index 8ed6fd1a693..7b1fdba70c1 100644 --- a/src/cargo/ops/resolve.rs +++ b/src/cargo/ops/resolve.rs @@ -81,7 +81,9 @@ use crate::util::CanonicalUrl; use anyhow::Context as _; use cargo_util::paths; use cargo_util_schemas::core::PartialVersion; +use std::borrow::Cow; use std::collections::{HashMap, HashSet}; +use std::rc::Rc; use tracing::{debug, trace}; /// Filter for keep using Package ID from previous lockfile. @@ -96,9 +98,18 @@ pub struct WorkspaceResolve<'gctx> { /// This may be `None` for things like `cargo install` and `-Zavoid-dev-deps`. /// This does not include `paths` overrides. pub workspace_resolve: Option, - /// The narrowed resolve, with the specific features enabled, and only the - /// given package specs requested. + /// The narrowed resolve, with the specific features enabled. pub targeted_resolve: Resolve, + /// Package specs requested for compilation along with specific features enabled. This usually + /// has the length of one but there may be more specs with different features when using the + /// `package` feature resolver. + pub specs_and_features: Vec, +} + +/// Pair of package specs requested for compilation along with enabled features. +pub struct SpecsAndResolvedFeatures { + /// Packages that are supposed to be built. + pub specs: Vec, /// The features activated per package. pub resolved_features: ResolvedFeatures, } @@ -145,10 +156,20 @@ pub fn resolve_ws_with_opts<'gctx>( force_all_targets: ForceAllTargets, dry_run: bool, ) -> CargoResult> { - let specs = match ws.resolve_feature_unification() { - FeatureUnification::Selected => specs, - FeatureUnification::Workspace => &ops::Packages::All(Vec::new()).to_package_id_specs(ws)?, + let feature_unification = ws.resolve_feature_unification(); + let individual_specs = match feature_unification { + FeatureUnification::Selected => vec![specs.to_owned()], + FeatureUnification::Workspace => { + vec![ops::Packages::All(Vec::new()).to_package_id_specs(ws)?] + } }; + let specs: Vec<_> = individual_specs + .iter() + .map(|specs| specs.iter()) + .flatten() + .cloned() + .collect(); + let specs = &specs[..]; let mut registry = ws.package_registry()?; let (resolve, resolved_with_overrides) = if ws.ignore_lock() { let add_patches = true; @@ -229,9 +250,9 @@ pub fn resolve_ws_with_opts<'gctx>( let pkg_set = get_resolved_packages(&resolved_with_overrides, registry)?; - let member_ids = ws - .members_with_features(specs, cli_features)? - .into_iter() + let members_with_features = ws.members_with_features(specs, cli_features)?; + let member_ids = members_with_features + .iter() .map(|(p, _fts)| p.package_id()) .collect::>(); pkg_set.download_accessible( @@ -243,33 +264,49 @@ pub fn resolve_ws_with_opts<'gctx>( force_all_targets, )?; - let feature_opts = FeatureOpts::new(ws, has_dev_units, force_all_targets)?; - let resolved_features = FeatureResolver::resolve( - ws, - target_data, - &resolved_with_overrides, - &pkg_set, - cli_features, - specs, - requested_targets, - feature_opts, - )?; + let mut specs_and_features = Vec::new(); - pkg_set.warn_no_lib_packages_and_artifact_libs_overlapping_deps( - ws, - &resolved_with_overrides, - &member_ids, - has_dev_units, - requested_targets, - target_data, - force_all_targets, - )?; + for specs in individual_specs { + let feature_opts = FeatureOpts::new(ws, has_dev_units, force_all_targets)?; + + let narrowed_features = match feature_unification { + FeatureUnification::Selected | FeatureUnification::Workspace => { + Cow::Borrowed(cli_features) + } + }; + + let resolved_features = FeatureResolver::resolve( + ws, + target_data, + &resolved_with_overrides, + &pkg_set, + &*narrowed_features, + &specs, + requested_targets, + feature_opts, + )?; + + pkg_set.warn_no_lib_packages_and_artifact_libs_overlapping_deps( + ws, + &resolved_with_overrides, + &member_ids, + has_dev_units, + requested_targets, + target_data, + force_all_targets, + )?; + + specs_and_features.push(SpecsAndResolvedFeatures { + specs, + resolved_features, + }); + } Ok(WorkspaceResolve { pkg_set, workspace_resolve: resolve, targeted_resolve: resolved_with_overrides, - resolved_features, + specs_and_features, }) } diff --git a/src/cargo/ops/tree/mod.rs b/src/cargo/ops/tree/mod.rs index cdc451f3096..977d7781b45 100644 --- a/src/cargo/ops/tree/mod.rs +++ b/src/cargo/ops/tree/mod.rs @@ -5,6 +5,7 @@ use crate::core::compiler::{CompileKind, RustcTargetData}; use crate::core::dependency::DepKind; use crate::core::resolver::{features::CliFeatures, ForceAllTargets, HasDevUnits}; use crate::core::{Package, PackageId, PackageIdSpec, PackageIdSpecQuery, Workspace}; +use crate::ops::resolve::SpecsAndResolvedFeatures; use crate::ops::{self, Packages}; use crate::util::CargoResult; use crate::{drop_print, drop_println}; @@ -179,61 +180,67 @@ pub fn build_and_print(ws: &Workspace<'_>, opts: &TreeOptions) -> CargoResult<() .map(|pkg| (pkg.package_id(), pkg)) .collect(); - let mut graph = graph::build( - ws, - &ws_resolve.targeted_resolve, - &ws_resolve.resolved_features, - &specs, - &opts.cli_features, - &target_data, - &requested_kinds, - package_map, - opts, - )?; - - let root_specs = if opts.invert.is_empty() { - specs - } else { - opts.invert - .iter() - .map(|p| PackageIdSpec::parse(p)) - .collect::, _>>()? - }; - let root_ids = ws_resolve.targeted_resolve.specs_to_ids(&root_specs)?; - let root_indexes = graph.indexes_from_ids(&root_ids); - - let root_indexes = if opts.duplicates { - // `-d -p foo` will only show duplicates within foo's subtree - graph = graph.from_reachable(root_indexes.as_slice()); - graph.find_duplicates() - } else { - root_indexes - }; - - if !opts.invert.is_empty() || opts.duplicates { - graph.invert(); - } + for SpecsAndResolvedFeatures { + specs, + resolved_features, + } in ws_resolve.specs_and_features + { + let mut graph = graph::build( + ws, + &ws_resolve.targeted_resolve, + &resolved_features, + &specs, + &opts.cli_features, + &target_data, + &requested_kinds, + package_map.clone(), + opts, + )?; - // Packages to prune. - let pkgs_to_prune = opts - .pkgs_to_prune - .iter() - .map(|p| PackageIdSpec::parse(p).map_err(Into::into)) - .map(|r| { - // Provide an error message if pkgid is not within the resolved - // dependencies graph. - r.and_then(|spec| spec.query(ws_resolve.targeted_resolve.iter()).and(Ok(spec))) - }) - .collect::>>()?; + let root_specs = if opts.invert.is_empty() { + specs + } else { + opts.invert + .iter() + .map(|p| PackageIdSpec::parse(p)) + .collect::, _>>()? + }; + let root_ids = ws_resolve.targeted_resolve.specs_to_ids(&root_specs)?; + let root_indexes = graph.indexes_from_ids(&root_ids); + + let root_indexes = if opts.duplicates { + // `-d -p foo` will only show duplicates within foo's subtree + graph = graph.from_reachable(root_indexes.as_slice()); + graph.find_duplicates() + } else { + root_indexes + }; + + if !opts.invert.is_empty() || opts.duplicates { + graph.invert(); + } - if root_indexes.len() == 0 { - ws.gctx().shell().warn( - "nothing to print.\n\n\ - To find dependencies that require specific target platforms, \ - try to use option `--target all` first, and then narrow your search scope accordingly.", - )?; - } else { - print(ws, opts, root_indexes, &pkgs_to_prune, &graph)?; + // Packages to prune. + let pkgs_to_prune = opts + .pkgs_to_prune + .iter() + .map(|p| PackageIdSpec::parse(p).map_err(Into::into)) + .map(|r| { + // Provide an error message if pkgid is not within the resolved + // dependencies graph. + r.and_then(|spec| spec.query(ws_resolve.targeted_resolve.iter()).and(Ok(spec))) + }) + .collect::>>()?; + + if root_indexes.len() == 0 { + ws.gctx().shell().warn( + "nothing to print.\n\n\ + To find dependencies that require specific target platforms, \ + try to use option `--target all` first, and then narrow your search scope accordingly.", + )?; + } else { + print(ws, opts, root_indexes, &pkgs_to_prune, &graph)?; + } } Ok(()) } 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 3/5] 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 7b1fdba70c1..9e48f35a259 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 356070157e9..4b079576b9c 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 0b9c07bee07..ea34fed42e2 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 From 0be420bb7831cd24a64a0f8903a66b354db2338e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Ml=C3=A1dek?= Date: Thu, 19 Jun 2025 20:24:39 +0200 Subject: [PATCH 4/5] docs: update unstable docs on feature unificatin --- src/doc/src/reference/unstable.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/doc/src/reference/unstable.md b/src/doc/src/reference/unstable.md index 54fe74e11c4..985c8ff5a65 100644 --- a/src/doc/src/reference/unstable.md +++ b/src/doc/src/reference/unstable.md @@ -1899,7 +1899,7 @@ Specify which packages participate in [feature unification](../reference/feature * `selected`: Merge dependency features from all packages specified for the current build. * `workspace`: Merge dependency features across all workspace members, regardless of which packages are specified for the current build. -* `package` _(unimplemented)_: Dependency features are considered on a package-by-package basis, +* `package`: Dependency features are considered on a package-by-package basis, preferring duplicate builds of dependencies when different sets of features are activated by the packages. ## Package message format From e4a616b6fde9866f57a00444928b2392fc921933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Ml=C3=A1dek?= Date: Mon, 23 Jun 2025 23:18:11 +0200 Subject: [PATCH 5/5] docs: fix outdated doc comment --- src/cargo/core/workspace.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cargo/core/workspace.rs b/src/cargo/core/workspace.rs index cfb93a00a4d..32e3bb961ee 100644 --- a/src/cargo/core/workspace.rs +++ b/src/cargo/core/workspace.rs @@ -1287,7 +1287,7 @@ impl<'gctx> Workspace<'gctx> { self.target_dir = Some(target_dir); } - /// Returns a Vec of `(&Package, RequestedFeatures)` tuples that + /// Returns a Vec of `(&Package, CliFeatures)` tuples that /// represent the workspace members that were requested on the command-line. /// /// `specs` may be empty, which indicates it should return all workspace