From 9ffcf69093fe82a16a29d71101be80e9c1cefa5b Mon Sep 17 00:00:00 2001 From: Eric Huss Date: Sun, 25 Oct 2020 16:34:03 -0700 Subject: [PATCH] Implement weak dependency features. --- src/bin/cargo/cli.rs | 1 + src/cargo/core/features.rs | 2 + src/cargo/core/resolver/dep_cache.rs | 4 + src/cargo/core/resolver/features.rs | 175 +++++++-- src/cargo/core/summary.rs | 54 ++- src/cargo/ops/cargo_compile.rs | 9 + src/cargo/ops/tree/graph.rs | 5 + src/cargo/sources/registry/index.rs | 7 +- src/cargo/util/toml/mod.rs | 3 +- src/doc/src/reference/unstable.md | 23 ++ tests/testsuite/main.rs | 1 + tests/testsuite/weak_dep_features.rs | 566 +++++++++++++++++++++++++++ 12 files changed, 800 insertions(+), 50 deletions(-) create mode 100644 tests/testsuite/weak_dep_features.rs diff --git a/src/bin/cargo/cli.rs b/src/bin/cargo/cli.rs index 70bc511608e..e24f583f17f 100644 --- a/src/bin/cargo/cli.rs +++ b/src/bin/cargo/cli.rs @@ -43,6 +43,7 @@ Available unstable (nightly-only) flags: -Z doctest-xcompile -- Compile and run doctests for non-host target using runner config -Z terminal-width -- Provide a terminal width to rustc for error truncation -Z namespaced-features -- Allow features with `dep:` prefix + -Z weak-dep-features -- Allow `dep_name?/feature` feature syntax Run with 'cargo -Z [FLAG] [SUBCOMMAND]'" ); diff --git a/src/cargo/core/features.rs b/src/cargo/core/features.rs index 5d3aa4db8cf..f2e5d957728 100644 --- a/src/cargo/core/features.rs +++ b/src/cargo/core/features.rs @@ -358,6 +358,7 @@ pub struct CliUnstable { pub rustdoc_map: bool, pub terminal_width: Option>, pub namespaced_features: bool, + pub weak_dep_features: bool, } fn deserialize_build_std<'de, D>(deserializer: D) -> Result>, D::Error> @@ -464,6 +465,7 @@ impl CliUnstable { "rustdoc-map" => self.rustdoc_map = parse_empty(k, v)?, "terminal-width" => self.terminal_width = Some(parse_usize_opt(v)?), "namespaced-features" => self.namespaced_features = parse_empty(k, v)?, + "weak-dep-features" => self.weak_dep_features = parse_empty(k, v)?, _ => bail!("unknown `-Z` flag specified: {}", k), } diff --git a/src/cargo/core/resolver/dep_cache.rs b/src/cargo/core/resolver/dep_cache.rs index 5cc57781fa3..1f6c49ca0fb 100644 --- a/src/cargo/core/resolver/dep_cache.rs +++ b/src/cargo/core/resolver/dep_cache.rs @@ -494,6 +494,10 @@ impl Requirements<'_> { dep_name, dep_feature, dep_prefix, + // Weak features are always activated in the dependency + // resolver. They will be narrowed inside the new feature + // resolver. + weak: _, } => self.require_dep_feature(*dep_name, *dep_feature, *dep_prefix)?, }; Ok(()) diff --git a/src/cargo/core/resolver/features.rs b/src/cargo/core/resolver/features.rs index 58e1703ac82..8b0b73e762e 100644 --- a/src/cargo/core/resolver/features.rs +++ b/src/cargo/core/resolver/features.rs @@ -177,6 +177,10 @@ impl FeatureOpts { if let ForceAllTargets::Yes = force_all_targets { opts.ignore_inactive_targets = false; } + if unstable_flags.weak_dep_features { + // Force this ON because it only works with the new resolver. + opts.new_resolver = true; + } Ok(opts) } } @@ -311,6 +315,15 @@ pub struct FeatureResolver<'a, 'cfg> { /// This has to be separate from `FeatureOpts.decouple_host_deps` because /// `for_host` tracking is also needed for `itarget` to work properly. track_for_host: bool, + /// `dep_name?/feat_name` features that will be activated if `dep_name` is + /// ever activated. + /// + /// The key is the `(package, for_host, dep_name)` of the package whose + /// dependency will trigger the addition of new features. The value is the + /// set of `(feature, dep_prefix)` features to activate (`dep_prefix` is a + /// bool that indicates if `dep:` prefix was used). + deferred_weak_dependencies: + HashMap<(PackageId, bool, InternedString), HashSet<(InternedString, bool)>>, } impl<'a, 'cfg> FeatureResolver<'a, 'cfg> { @@ -353,6 +366,7 @@ impl<'a, 'cfg> FeatureResolver<'a, 'cfg> { activated_dependencies: HashMap::new(), processed_deps: HashSet::new(), track_for_host, + deferred_weak_dependencies: HashMap::new(), }; r.do_resolve(specs, requested_features)?; log::debug!("features={:#?}", r.activated_features); @@ -399,6 +413,7 @@ impl<'a, 'cfg> FeatureResolver<'a, 'cfg> { for_host: bool, fvs: &[FeatureValue], ) -> CargoResult<()> { + log::trace!("activate_pkg {} {}", pkg_id.name(), for_host); // Add an empty entry to ensure everything is covered. This is intended for // finding bugs where the resolver missed something it should have visited. // Remove this in the future if `activated_features` uses an empty default. @@ -446,56 +461,28 @@ impl<'a, 'cfg> FeatureResolver<'a, 'cfg> { for_host: bool, fv: &FeatureValue, ) -> CargoResult<()> { + log::trace!("activate_fv {} {} {}", pkg_id.name(), for_host, fv); match fv { FeatureValue::Feature(f) => { self.activate_rec(pkg_id, for_host, *f)?; } FeatureValue::Dep { dep_name } => { - // Mark this dependency as activated. - self.activated_dependencies - .entry((pkg_id, self.opts.decouple_host_deps && for_host)) - .or_default() - .insert(*dep_name); - // Activate the optional dep. - for (dep_pkg_id, deps) in self.deps(pkg_id, for_host) { - for (dep, dep_for_host) in deps { - if dep.name_in_toml() != *dep_name { - continue; - } - let fvs = self.fvs_from_dependency(dep_pkg_id, dep); - self.activate_pkg(dep_pkg_id, dep_for_host, &fvs)?; - } - } + self.activate_dependency(pkg_id, for_host, *dep_name)?; } FeatureValue::DepFeature { dep_name, dep_feature, dep_prefix, + weak, } => { - // Activate a feature within a dependency. - for (dep_pkg_id, deps) in self.deps(pkg_id, for_host) { - for (dep, dep_for_host) in deps { - if dep.name_in_toml() != *dep_name { - continue; - } - if dep.is_optional() { - // Activate the dependency on self. - let fv = FeatureValue::Dep { - dep_name: *dep_name, - }; - self.activate_fv(pkg_id, for_host, &fv)?; - if !dep_prefix { - // To retain compatibility with old behavior, - // this also enables a feature of the same - // name. - self.activate_rec(pkg_id, for_host, *dep_name)?; - } - } - // Activate the feature on the dependency. - let fv = FeatureValue::new(*dep_feature); - self.activate_fv(dep_pkg_id, dep_for_host, &fv)?; - } - } + self.activate_dep_feature( + pkg_id, + for_host, + *dep_name, + *dep_feature, + *dep_prefix, + *weak, + )?; } } Ok(()) @@ -509,6 +496,12 @@ impl<'a, 'cfg> FeatureResolver<'a, 'cfg> { for_host: bool, feature_to_enable: InternedString, ) -> CargoResult<()> { + log::trace!( + "activate_rec {} {} feat={}", + pkg_id.name(), + for_host, + feature_to_enable + ); let enabled = self .activated_features .entry((pkg_id, self.opts.decouple_host_deps && for_host)) @@ -539,6 +532,110 @@ impl<'a, 'cfg> FeatureResolver<'a, 'cfg> { Ok(()) } + /// Activate a dependency (`dep:dep_name` syntax). + fn activate_dependency( + &mut self, + pkg_id: PackageId, + for_host: bool, + dep_name: InternedString, + ) -> CargoResult<()> { + // Mark this dependency as activated. + let save_for_host = self.opts.decouple_host_deps && for_host; + self.activated_dependencies + .entry((pkg_id, save_for_host)) + .or_default() + .insert(dep_name); + // Check for any deferred features. + let to_enable = self + .deferred_weak_dependencies + .remove(&(pkg_id, for_host, dep_name)); + // Activate the optional dep. + for (dep_pkg_id, deps) in self.deps(pkg_id, for_host) { + for (dep, dep_for_host) in deps { + if dep.name_in_toml() != dep_name { + continue; + } + if let Some(to_enable) = &to_enable { + for (dep_feature, dep_prefix) in to_enable { + log::trace!( + "activate deferred {} {} -> {}/{}", + pkg_id.name(), + for_host, + dep_name, + dep_feature + ); + if !dep_prefix { + self.activate_rec(pkg_id, for_host, dep_name)?; + } + let fv = FeatureValue::new(*dep_feature); + self.activate_fv(dep_pkg_id, dep_for_host, &fv)?; + } + } + let fvs = self.fvs_from_dependency(dep_pkg_id, dep); + self.activate_pkg(dep_pkg_id, dep_for_host, &fvs)?; + } + } + Ok(()) + } + + /// Activate a feature within a dependency (`dep_name/feat_name` syntax). + fn activate_dep_feature( + &mut self, + pkg_id: PackageId, + for_host: bool, + dep_name: InternedString, + dep_feature: InternedString, + dep_prefix: bool, + weak: bool, + ) -> CargoResult<()> { + for (dep_pkg_id, deps) in self.deps(pkg_id, for_host) { + for (dep, dep_for_host) in deps { + if dep.name_in_toml() != dep_name { + continue; + } + if dep.is_optional() { + let save_for_host = self.opts.decouple_host_deps && for_host; + if weak + && !self + .activated_dependencies + .get(&(pkg_id, save_for_host)) + .map(|deps| deps.contains(&dep_name)) + .unwrap_or(false) + { + // This is weak, but not yet activated. Defer in case + // something comes along later and enables it. + log::trace!( + "deferring feature {} {} -> {}/{}", + pkg_id.name(), + for_host, + dep_name, + dep_feature + ); + self.deferred_weak_dependencies + .entry((pkg_id, for_host, dep_name)) + .or_default() + .insert((dep_feature, dep_prefix)); + continue; + } + + // Activate the dependency on self. + let fv = FeatureValue::Dep { dep_name }; + self.activate_fv(pkg_id, for_host, &fv)?; + if !dep_prefix { + // To retain compatibility with old behavior, + // this also enables a feature of the same + // name. + self.activate_rec(pkg_id, for_host, dep_name)?; + } + } + // Activate the feature on the dependency. + let fv = FeatureValue::new(dep_feature); + self.activate_fv(dep_pkg_id, dep_for_host, &fv)?; + } + } + Ok(()) + } + /// Returns Vec of FeatureValues from a Dependency definition. fn fvs_from_dependency(&self, dep_id: PackageId, dep: &Dependency) -> Vec { let summary = self.resolve.summary(dep_id); diff --git a/src/cargo/core/summary.rs b/src/cargo/core/summary.rs index 8c564ed18ae..eb7a80e041a 100644 --- a/src/cargo/core/summary.rs +++ b/src/cargo/core/summary.rs @@ -86,7 +86,11 @@ impl Summary { /// Returns an error if this Summary is using an unstable feature that is /// not enabled. - pub fn unstable_gate(&self, namespaced_features: bool) -> CargoResult<()> { + pub fn unstable_gate( + &self, + namespaced_features: bool, + weak_dep_features: bool, + ) -> CargoResult<()> { if !namespaced_features { if self.inner.has_namespaced_features { bail!( @@ -101,6 +105,22 @@ impl Summary { ) } } + if !weak_dep_features { + for (feat_name, features) in self.features() { + for fv in features { + if matches!(fv, FeatureValue::DepFeature{weak: true, ..}) { + bail!( + "optional dependency features with `?` syntax are only \ + allowed on the nightly channel and requires the \ + `-Z weak-dep-features` flag on the command line\n\ + Feature `{}` had feature value `{}`.", + feat_name, + fv + ); + } + } + } + } Ok(()) } @@ -293,7 +313,7 @@ fn build_feature_map( ); } } - DepFeature { dep_name, .. } => { + DepFeature { dep_name, weak, .. } => { // Validation of the feature name will be performed in the resolver. if !is_any_dep { bail!( @@ -303,6 +323,12 @@ fn build_feature_map( dep_name ); } + if *weak && !is_optional_dep { + bail!("feature `{}` includes `{}` with a `?`, but `{}` is not an optional dependency\n\ + A non-optional dependency of the same name is defined; \ + consider removing the `?` or changing the dependency to be optional", + feature, fv, dep_name); + } } } } @@ -346,6 +372,10 @@ pub enum FeatureValue { /// If this is true, then the feature used the `dep:` prefix, which /// prevents enabling the feature named `dep_name`. dep_prefix: bool, + /// If `true`, indicates the `?` syntax is used, which means this will + /// not automatically enable the dependency unless the dependency is + /// activated through some other means. + weak: bool, }, } @@ -360,10 +390,16 @@ impl FeatureValue { } else { (dep, false) }; + let (dep, weak) = if let Some(dep) = dep.strip_suffix('?') { + (dep, true) + } else { + (dep, false) + }; FeatureValue::DepFeature { dep_name: InternedString::new(dep), dep_feature: InternedString::new(dep_feat), dep_prefix, + weak, } } None => { @@ -393,13 +429,13 @@ impl fmt::Display for FeatureValue { DepFeature { dep_name, dep_feature, - dep_prefix: true, - } => write!(f, "dep:{}/{}", dep_name, dep_feature), - DepFeature { - dep_name, - dep_feature, - dep_prefix: false, - } => write!(f, "{}/{}", dep_name, dep_feature), + dep_prefix, + weak, + } => { + let dep_prefix = if *dep_prefix { "dep:" } else { "" }; + let weak = if *weak { "?" } else { "" }; + write!(f, "{}{}{}/{}", dep_prefix, dep_name, weak, dep_feature) + } } } } diff --git a/src/cargo/ops/cargo_compile.rs b/src/cargo/ops/cargo_compile.rs index 58e4fbc6a6d..68ed8625e91 100644 --- a/src/cargo/ops/cargo_compile.rs +++ b/src/cargo/ops/cargo_compile.rs @@ -1082,11 +1082,20 @@ fn validate_required_features( target_name ); } + FeatureValue::DepFeature { weak: true, .. } => { + anyhow::bail!( + "invalid feature `{}` in required-features of target `{}`: \ + optional dependency with `?` is not allowed in required-features", + fv, + target_name + ); + } // Handling of dependent_crate/dependent_crate_feature syntax FeatureValue::DepFeature { dep_name, dep_feature, dep_prefix: false, + weak: false, } => { match resolve .deps(summary.package_id()) diff --git a/src/cargo/ops/tree/graph.rs b/src/cargo/ops/tree/graph.rs index f69f924f655..3c81320f849 100644 --- a/src/cargo/ops/tree/graph.rs +++ b/src/cargo/ops/tree/graph.rs @@ -568,6 +568,11 @@ fn add_feature_rec( dep_name, dep_feature, dep_prefix, + // `weak` is ignored, because it will be skipped if the + // corresponding dependency is not found in the map, which + // means it wasn't activated. Skipping is handled by + // `is_dep_activated` when the graph was built. + weak: _, } => { let dep_indexes = match graph.dep_name_map[&package_index].get(dep_name) { Some(indexes) => indexes.clone(), diff --git a/src/cargo/sources/registry/index.rs b/src/cargo/sources/registry/index.rs index cc611fb970e..f7690f3d652 100644 --- a/src/cargo/sources/registry/index.rs +++ b/src/cargo/sources/registry/index.rs @@ -272,6 +272,7 @@ impl<'cfg> RegistryIndex<'cfg> { let source_id = self.source_id; let config = self.config; let namespaced_features = self.config.cli_unstable().namespaced_features; + let weak_dep_features = self.config.cli_unstable().weak_dep_features; // First up actually parse what summaries we have available. If Cargo // has run previously this will parse a Cargo-specific cache file rather @@ -299,7 +300,11 @@ impl<'cfg> RegistryIndex<'cfg> { } }, ) - .filter(move |is| is.summary.unstable_gate(namespaced_features).is_ok())) + .filter(move |is| { + is.summary + .unstable_gate(namespaced_features, weak_dep_features) + .is_ok() + })) } fn load_summaries( diff --git a/src/cargo/util/toml/mod.rs b/src/cargo/util/toml/mod.rs index 49a58edaaa9..b2d11528421 100644 --- a/src/cargo/util/toml/mod.rs +++ b/src/cargo/util/toml/mod.rs @@ -1197,7 +1197,8 @@ impl TomlManifest { me.features.as_ref().unwrap_or(&empty_features), project.links.as_deref(), )?; - summary.unstable_gate(config.cli_unstable().namespaced_features)?; + let unstable = config.cli_unstable(); + summary.unstable_gate(unstable.namespaced_features, unstable.weak_dep_features)?; let metadata = ManifestMetadata { description: project.description.clone(), diff --git a/src/doc/src/reference/unstable.md b/src/doc/src/reference/unstable.md index 4664a1f96f0..70989637edc 100644 --- a/src/doc/src/reference/unstable.md +++ b/src/doc/src/reference/unstable.md @@ -906,3 +906,26 @@ error[E0308]: mismatched types error: aborting due to previous error ``` + +### Weak dependency features +* Tracking Issue: [#8832](https://github.com/rust-lang/cargo/issues/8832) + +The `-Z weak-dep-features` command-line options enables the ability to use +`dep_name?/feat_name` syntax in the `[features]` table. The `?` indicates that +the optional dependency `dep_name` will not be automatically enabled. The +feature `feat_name` will only be added if something else enables the +`dep_name` dependency. + +Example: + +```toml +[dependencies] +serde = { version = "1.0.117", optional = true, default-features = false } + +[features] +std = ["serde?/std"] +``` + +In this example, the `std` feature enables the `std` feature on the `serde` +dependency. However, unlike the normal `serde/std` syntax, it will not enable +the optional dependency `serde` unless something else has included it. diff --git a/tests/testsuite/main.rs b/tests/testsuite/main.rs index beafdd85f1a..3fc5e492b3c 100644 --- a/tests/testsuite/main.rs +++ b/tests/testsuite/main.rs @@ -120,6 +120,7 @@ mod vendor; mod verify_project; mod version; mod warn_on_failure; +mod weak_dep_features; mod workspaces; mod yank; diff --git a/tests/testsuite/weak_dep_features.rs b/tests/testsuite/weak_dep_features.rs new file mode 100644 index 00000000000..6b8ed355add --- /dev/null +++ b/tests/testsuite/weak_dep_features.rs @@ -0,0 +1,566 @@ +//! Tests for weak-dep-features. + +use cargo_test_support::project; +use cargo_test_support::registry::{Dependency, Package}; +use std::fmt::Write; + +// Helper to create lib.rs files that check features. +fn require(enabled_features: &[&str], disabled_features: &[&str]) -> String { + let mut s = String::new(); + for feature in enabled_features { + write!(s, "#[cfg(not(feature=\"{feature}\"))] compile_error!(\"expected feature {feature} to be enabled\");\n", + feature=feature).unwrap(); + } + for feature in disabled_features { + write!(s, "#[cfg(feature=\"{feature}\")] compile_error!(\"did not expect feature {feature} to be enabled\");\n", + feature=feature).unwrap(); + } + s +} + +#[cargo_test] +fn gated() { + // Need -Z weak-dep-features to enable. + Package::new("bar", "1.0.0").feature("feat", &[]).publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + + [dependencies] + bar = { version = "1.0", optional = true } + + [features] + f1 = ["bar?/feat"] + "#, + ) + .file("src/lib.rs", "") + .build(); + p.cargo("check") + .with_status(101) + .with_stderr( + "\ +error: failed to parse manifest at `[ROOT]/foo/Cargo.toml` + +Caused by: + optional dependency features with `?` syntax are only allowed on the nightly \ + channel and requires the `-Z weak-dep-features` flag on the command line + Feature `f1` had feature value `bar?/feat`. +", + ) + .run(); +} + +#[cargo_test] +fn dependency_gate_ignored() { + // Dependencies with ? features in the registry are ignored in the + // registry if not on nightly. + Package::new("baz", "1.0.0").feature("feat", &[]).publish(); + Package::new("bar", "1.0.0") + .add_dep(Dependency::new("baz", "1.0").optional(true)) + .feature("feat", &["baz?/feat"]) + .publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + + [dependencies] + bar = "1.0" + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check") + .masquerade_as_nightly_cargo() + .with_status(101) + .with_stderr( + "\ +[UPDATING] [..] +[ERROR] no matching package named `bar` found +location searched: registry `https://github.com/rust-lang/crates.io-index` +required by package `foo v0.1.0 ([..]/foo)` +", + ) + .run(); + + // Publish a version without the ? feature, it should ignore 1.0.0 + // an use this instead. + Package::new("bar", "1.0.1") + .add_dep(Dependency::new("baz", "1.0").optional(true)) + .feature("feat", &["baz"]) + .publish(); + p.cargo("check") + .masquerade_as_nightly_cargo() + .with_stderr( + "\ +[UPDATING] [..] +[DOWNLOADING] crates ... +[DOWNLOADED] bar [..] +[CHECKING] bar v1.0.1 +[CHECKING] foo v0.1.0 [..] +[FINISHED] [..] +", + ) + .run(); +} + +#[cargo_test] +fn simple() { + Package::new("bar", "1.0.0") + .feature("feat", &[]) + .file("src/lib.rs", &require(&["feat"], &[])) + .publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + + [dependencies] + bar = { version = "1.0", optional = true } + + [features] + f1 = ["bar?/feat"] + "#, + ) + .file("src/lib.rs", &require(&["f1"], &[])) + .build(); + + // It's a bit unfortunate that this has to download `bar`, but avoiding + // that is extremely difficult. + p.cargo("check -Z weak-dep-features --features f1") + .masquerade_as_nightly_cargo() + .with_stderr( + "\ +[UPDATING] [..] +[DOWNLOADING] crates ... +[DOWNLOADED] bar v1.0.0 [..] +[CHECKING] foo v0.1.0 [..] +[FINISHED] [..] +", + ) + .run(); + + p.cargo("check -Z weak-dep-features --features f1,bar") + .masquerade_as_nightly_cargo() + .with_stderr( + "\ +[CHECKING] bar v1.0.0 +[CHECKING] foo v0.1.0 [..] +[FINISHED] [..] +", + ) + .run(); +} + +#[cargo_test] +fn deferred() { + // A complex chain that requires deferring enabling the feature due to + // another dependency getting enabled. + Package::new("bar", "1.0.0") + .feature("feat", &[]) + .file("src/lib.rs", &require(&["feat"], &[])) + .publish(); + Package::new("dep", "1.0.0") + .add_dep(Dependency::new("bar", "1.0").optional(true)) + .feature("feat", &["bar?/feat"]) + .publish(); + Package::new("bar_activator", "1.0.0") + .feature_dep("dep", "1.0", &["bar"]) + .publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + + [dependencies] + dep = { version = "1.0", features = ["feat"] } + bar_activator = "1.0" + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Z weak-dep-features") + .masquerade_as_nightly_cargo() + .with_stderr( + "\ +[UPDATING] [..] +[DOWNLOADING] crates ... +[DOWNLOADED] dep v1.0.0 [..] +[DOWNLOADED] bar_activator v1.0.0 [..] +[DOWNLOADED] bar v1.0.0 [..] +[CHECKING] bar v1.0.0 +[CHECKING] dep v1.0.0 +[CHECKING] bar_activator v1.0.0 +[CHECKING] foo v0.1.0 [..] +[FINISHED] [..] +", + ) + .run(); +} + +#[cargo_test] +fn not_optional_dep() { + // Attempt to use dep_name?/feat where dep_name is not optional. + Package::new("dep", "1.0.0").feature("feat", &[]).publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + + [dependencies] + dep = "1.0" + + [features] + feat = ["dep?/feat"] + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Z weak-dep-features") + .masquerade_as_nightly_cargo() + .with_status(101) + .with_stderr("\ +error: failed to parse manifest at `[ROOT]/foo/Cargo.toml` + +Caused by: + feature `feat` includes `dep?/feat` with a `?`, but `dep` is not an optional dependency + A non-optional dependency of the same name is defined; consider removing the `?` or changing the dependency to be optional +") + .run(); +} + +#[cargo_test] +fn optional_cli_syntax() { + // --features bar?/feat + Package::new("bar", "1.0.0") + .feature("feat", &[]) + .file("src/lib.rs", &require(&["feat"], &[])) + .publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + + [dependencies] + bar = { version = "1.0", optional = true } + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check --features bar?/feat -Z weak-dep-features") + .masquerade_as_nightly_cargo() + .with_stderr( + "\ +[UPDATING] [..] +[DOWNLOADING] crates ... +[DOWNLOADED] bar v1.0.0 [..] +[CHECKING] foo v0.1.0 [..] +[FINISHED] [..] +", + ) + .run(); + + p.cargo("check --features bar?/feat,bar -Z weak-dep-features") + .masquerade_as_nightly_cargo() + .with_stderr( + "\ +[CHECKING] bar v1.0.0 +[CHECKING] foo v0.1.0 [..] +[FINISHED] [..] +", + ) + .run(); +} + +#[cargo_test] +fn required_features() { + // required-features doesn't allow ? + Package::new("bar", "1.0.0").feature("feat", &[]).publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + + [dependencies] + bar = { version = "1.0", optional = true } + + [[bin]] + name = "foo" + required-features = ["bar?/feat"] + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + + p.cargo("check -Z weak-dep-features") + .masquerade_as_nightly_cargo() + .with_status(101) + .with_stderr( + "\ +[UPDATING] [..] +[ERROR] invalid feature `bar?/feat` in required-features of target `foo`: \ +optional dependency with `?` is not allowed in required-features +", + ) + .run(); +} + +#[cargo_test] +fn weak_with_host_decouple() { + // -Z weak-opt-features with -Z features=host + // + // foo v0.1.0 + // └── common v1.0.0 + // └── bar v1.0.0 <-- does not have `feat` enabled + // [build-dependencies] + // └── bar_activator v1.0.0 + // └── common v1.0.0 + // └── bar v1.0.0 <-- does have `feat` enabled + Package::new("bar", "1.0.0") + .feature("feat", &[]) + .file( + "src/lib.rs", + r#" + pub fn feat() -> bool { + cfg!(feature = "feat") + } + "#, + ) + .publish(); + + Package::new("common", "1.0.0") + .add_dep(Dependency::new("bar", "1.0").optional(true)) + .feature("feat", &["bar?/feat"]) + .file( + "src/lib.rs", + r#" + #[cfg(feature = "bar")] + pub fn feat() -> bool { bar::feat() } + #[cfg(not(feature = "bar"))] + pub fn feat() -> bool { false } + "#, + ) + .publish(); + + Package::new("bar_activator", "1.0.0") + .feature_dep("common", "1.0", &["bar", "feat"]) + .file( + "src/lib.rs", + r#" + pub fn feat() -> bool { + common::feat() + } + "#, + ) + .publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + + [dependencies] + common = { version = "1.0", features = ["feat"] } + + [build-dependencies] + bar_activator = "1.0" + "#, + ) + .file( + "src/main.rs", + r#" + fn main() { + assert!(!common::feat()); + } + "#, + ) + .file( + "build.rs", + r#" + fn main() { + assert!(bar_activator::feat()); + } + "#, + ) + .build(); + + p.cargo("run -Z weak-dep-features -Z features=host_dep") + .masquerade_as_nightly_cargo() + .with_stderr( + "\ +[UPDATING] [..] +[DOWNLOADING] crates ... +[DOWNLOADED] [..] +[DOWNLOADED] [..] +[DOWNLOADED] [..] +[COMPILING] bar v1.0.0 +[COMPILING] common v1.0.0 +[COMPILING] bar_activator v1.0.0 +[COMPILING] foo v0.1.0 [..] +[FINISHED] [..] +[RUNNING] `target/debug/foo[EXE]` +", + ) + .run(); +} + +#[cargo_test] +fn deferred_with_namespaced() { + // Interaction with -Z namespaced-features using dep: syntax. + // + // `bar` is deferred with bar?/feat + // `bar2` is deferred with dep:bar2?/feat + Package::new("bar", "1.0.0") + .feature("feat", &[]) + .file("src/lib.rs", &require(&["feat"], &[])) + .publish(); + Package::new("bar2", "1.0.0") + .feature("feat", &[]) + .file("src/lib.rs", &require(&["feat"], &[])) + .publish(); + Package::new("bar_includer", "1.0.0") + .add_dep(Dependency::new("bar", "1.0").optional(true)) + .add_dep(Dependency::new("bar2", "1.0").optional(true)) + .feature("feat", &["bar?/feat", "dep:bar2?/feat"]) + .feature("feat2", &["dep:bar2"]) + .file("src/lib.rs", &require(&["bar"], &["bar2"])) + .publish(); + Package::new("bar_activator", "1.0.0") + .feature_dep("bar_includer", "1.0", &["bar", "feat2"]) + .publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + + [dependencies] + bar_includer = { version = "1.0", features = ["feat"] } + bar_activator = "1.0" + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check -Z weak-dep-features -Z namespaced-features") + .masquerade_as_nightly_cargo() + .with_stderr_unordered( + "\ +[UPDATING] [..] +[DOWNLOADING] crates ... +[DOWNLOADED] [..] +[DOWNLOADED] [..] +[DOWNLOADED] [..] +[DOWNLOADED] [..] +[CHECKING] bar v1.0.0 +[CHECKING] bar2 v1.0.0 +[CHECKING] bar_includer v1.0.0 +[CHECKING] bar_activator v1.0.0 +[CHECKING] foo v0.1.0 [..] +[FINISHED] [..] +", + ) + .run(); +} + +#[cargo_test] +fn tree() { + Package::new("bar", "1.0.0") + .feature("feat", &[]) + .file("src/lib.rs", &require(&["feat"], &[])) + .publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + + [dependencies] + bar = { version = "1.0", optional = true } + + [features] + f1 = ["bar?/feat"] + "#, + ) + .file("src/lib.rs", &require(&["f1"], &[])) + .build(); + + p.cargo("tree -Z weak-dep-features --features f1") + .masquerade_as_nightly_cargo() + .with_stdout("foo v0.1.0 ([ROOT]/foo)") + .run(); + + p.cargo("tree -Z weak-dep-features --features f1,bar") + .masquerade_as_nightly_cargo() + .with_stdout( + "\ +foo v0.1.0 ([ROOT]/foo) +└── bar v1.0.0 +", + ) + .run(); + + p.cargo("tree -Z weak-dep-features --features f1,bar -e features") + .masquerade_as_nightly_cargo() + .with_stdout( + "\ +foo v0.1.0 ([ROOT]/foo) +└── bar feature \"default\" + └── bar v1.0.0 +", + ) + .run(); + + p.cargo("tree -Z weak-dep-features --features f1,bar -e features -i bar") + .masquerade_as_nightly_cargo() + .with_stdout( + "\ +bar v1.0.0 +├── bar feature \"default\" +│ └── foo v0.1.0 ([ROOT]/foo) +│ ├── foo feature \"bar\" (command-line) +│ │ └── foo feature \"f1\" (command-line) +│ ├── foo feature \"default\" (command-line) +│ └── foo feature \"f1\" (command-line) +└── bar feature \"feat\" + └── foo feature \"f1\" (command-line) +", + ) + .run(); +}