implement package feature unification (#15684)
### What does this PR try to resolve?
Implements another part of feature unification (#14774,
[rfc](https://github.com/rust-lang/rfcs/blob/1c590ce05d676e72e2217845ee054758d3a6df34/text/3692-feature-unification.md)).
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 `ResolvedFeature`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 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).
diff --git a/src/cargo/core/compiler/standard_lib.rs b/src/cargo/core/compiler/standard_lib.rs
index 30f7c18..17cb4f6 100644
--- a/src/cargo/core/compiler/standard_lib.rs
+++ b/src/cargo/core/compiler/standard_lib.rs
@@ -96,7 +96,7 @@
&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 @@
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/core/workspace.rs b/src/cargo/core/workspace.rs
index cfb93a0..32e3bb9 100644
--- a/src/cargo/core/workspace.rs
+++ b/src/cargo/core/workspace.rs
@@ -1287,7 +1287,7 @@
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
diff --git a/src/cargo/ops/cargo_compile/mod.rs b/src/cargo/ops/cargo_compile/mod.rs
index 366056e..0090974 100644
--- a/src/cargo/ops/cargo_compile/mod.rs
+++ b/src/cargo/ops/cargo_compile/mod.rs
@@ -52,7 +52,7 @@
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 @@
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 @@
})
.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)?;
- }
-
- 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()
- };
-
- 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,
- &pkg_set,
+ 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::<Vec<_>>();
+ let packages = to_builds
+ .iter()
+ .filter(|package| spec_names.contains(&package.name().as_str()))
+ .cloned()
+ .collect::<Vec<_>>();
+ 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,
- &profiles,
- &target_data,
- )?
- } else {
- Default::default()
- };
+ has_dev_units,
+ };
+ let mut targeted_root_units = generator.generate_root_units()?;
- 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,
- )?;
+ 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()
+ };
+
+ unit_graph.extend(build_unit_dependencies(
+ ws,
+ &pkg_set,
+ &resolve,
+ &resolved_features,
+ std_resolve_features.as_ref(),
+ &targeted_root_units,
+ &targeted_scrape_units,
+ &std_roots,
+ build_config.intent,
+ &target_data,
+ &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 56ba396..89d6799 100644
--- a/src/cargo/ops/fix/mod.rs
+++ b/src/cargo/ops/fix/mod.rs
@@ -588,7 +588,15 @@
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 8ed6fd1..9e48f35 100644
--- a/src/cargo/ops/resolve.rs
+++ b/src/cargo/ops/resolve.rs
@@ -81,7 +81,9 @@
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 @@
/// This may be `None` for things like `cargo install` and `-Zavoid-dev-deps`.
/// This does not include `paths` overrides.
pub workspace_resolve: Option<Resolve>,
- /// 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<SpecsAndResolvedFeatures>,
+}
+
+/// Pair of package specs requested for compilation along with enabled features.
+pub struct SpecsAndResolvedFeatures {
+ /// Packages that are supposed to be built.
+ pub specs: Vec<PackageIdSpec>,
/// The features activated per package.
pub resolved_features: ResolvedFeatures,
}
@@ -145,10 +156,21 @@
force_all_targets: ForceAllTargets,
dry_run: bool,
) -> CargoResult<WorkspaceResolve<'gctx>> {
- 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)?]
+ }
+ FeatureUnification::Package => specs.iter().map(|spec| vec![spec.clone()]).collect(),
};
+ 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 +251,9 @@
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::<Vec<_>>();
pkg_set.download_accessible(
@@ -243,33 +265,70 @@
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)?;
+
+ // 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)
+ }
+ };
+
+ 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 cdc451f..977d778 100644
--- a/src/cargo/ops/tree/mod.rs
+++ b/src/cargo/ops/tree/mod.rs
@@ -5,6 +5,7 @@
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 @@
.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::<Result<Vec<PackageIdSpec>, _>>()?
- };
- 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();
- }
-
- // 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::<CargoResult<Vec<PackageIdSpec>>>()?;
-
- 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.",
+ 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,
)?;
- } else {
- print(ws, opts, root_indexes, &pkgs_to_prune, &graph)?;
+
+ let root_specs = if opts.invert.is_empty() {
+ specs
+ } else {
+ opts.invert
+ .iter()
+ .map(|p| PackageIdSpec::parse(p))
+ .collect::<Result<Vec<PackageIdSpec>, _>>()?
+ };
+ 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();
+ }
+
+ // 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::<CargoResult<Vec<PackageIdSpec>>>()?;
+
+ 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(())
}
diff --git a/src/cargo/util/context/mod.rs b/src/cargo/util/context/mod.rs
index 3560701..4b07957 100644
--- a/src/cargo/util/context/mod.rs
+++ b/src/cargo/util/context/mod.rs
@@ -2864,6 +2864,7 @@
#[derive(Copy, Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum FeatureUnification {
+ Package,
Selected,
Workspace,
}
diff --git a/src/doc/src/reference/unstable.md b/src/doc/src/reference/unstable.md
index 54fe74e..985c8ff 100644
--- a/src/doc/src/reference/unstable.md
+++ b/src/doc/src/reference/unstable.md
@@ -1899,7 +1899,7 @@
* `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
diff --git a/tests/testsuite/feature_unification.rs b/tests/testsuite/feature_unification.rs
index f533733..ea34fed 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() {
@@ -108,6 +114,732 @@
}
#[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 = "package"
+ "#,
+ )
+ .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_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_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")
+ .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 = "package"
+ "#,
+ )
+ .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_stderr_data(
+ str![[r#"
+[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
+
+"#]]
+ .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 = "package"
+ "#,
+ )
+ .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_stderr_data(
+ str![[r#"
+[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
+[CHECKING] common v0.1.0 ([ROOT]/foo/common)
+[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
+[CHECKING] b v0.1.0 ([ROOT]/foo/b)
+[CHECKING] a v0.1.0 ([ROOT]/foo/a)
+
+"#]]
+ .unordered(),
+ )
+ .run();
+ p.cargo("check --workspace --exclude common -F a,b")
+ .arg("-Zfeature-unification")
+ .masquerade_as_nightly_cargo(&["feature-unification"])
+ .with_stderr_data(
+ str![[r#"
+[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
+
+"#]]
+ .unordered(),
+ )
+ .run();
+
+ p.cargo("check -p a -p b -F a/a,b/b")
+ .arg("-Zfeature-unification")
+ .masquerade_as_nightly_cargo(&["feature-unification"])
+ .with_stderr_data(
+ str![[r#"
+[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
+
+"#]]
+ .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 = "package"
+ "#,
+ )
+ .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_stderr_data(
+ str![[r#"
+[CHECKING] common v0.1.0 ([ROOT]/foo/common)
+[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(),
+ )
+ .run();
+ p.cargo("check")
+ .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();
+
+ // 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", "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"
+│ └── 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"
+│ └── common v0.1.0 ([ROOT]/foo/common)
+├── outside feature "b"
+│ └── outside v0.1.0
+└── outside feature "default"
+ └── outside v0.1.0
+
+"#]])
+ .run();
+}
+
+#[cargo_test]
fn cargo_install_ignores_config() {
let p = project()
.file(
@@ -177,6 +909,20 @@
"#]])
.run();
+ cargo_process("install --path")
+ .arg(p.root())
+ .arg("-Zfeature-unification")
+ .masquerade_as_nightly_cargo(&["feature-unification"])
+ .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
+[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();
}
#[cargo_test]
@@ -204,3 +950,343 @@
"#]])
.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", "package")
+ .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", "package")
+ .arg("-Zfeature-unification")
+ .masquerade_as_nightly_cargo(&["feature-unification"])
+ .with_status(101)
+ .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
+[ERROR] cannot fix edition when using `feature-unification = "package"`.
+
+"#]]
+ .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();
+}