diff --git a/crates/cargo-util-schemas/manifest.schema.json b/crates/cargo-util-schemas/manifest.schema.json index 5bc43e49d37..d5a7e16c8de 100644 --- a/crates/cargo-util-schemas/manifest.schema.json +++ b/crates/cargo-util-schemas/manifest.schema.json @@ -51,10 +51,7 @@ "null" ], "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } + "$ref": "#/$defs/FeatureDefinition" } }, "lib": { @@ -597,6 +594,38 @@ ] }, "TomlValue": true, + "FeatureDefinition": { + "description": "Definition of a feature.", + "anyOf": [ + { + "description": "Features that this feature enables.", + "type": "array", + "items": { + "type": "string" + } + }, + { + "description": "Unstable feature `feature-metadata`. Metadata of this feature.", + "$ref": "#/$defs/FeatureMetadata" + } + ] + }, + "FeatureMetadata": { + "description": "Unstable feature `feature-metadata`. Metadata of a feature.", + "type": "object", + "properties": { + "enables": { + "description": "Features that this feature enables.", + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "enables" + ] + }, "TomlTarget": { "type": "object", "properties": { diff --git a/crates/cargo-util-schemas/src/manifest/mod.rs b/crates/cargo-util-schemas/src/manifest/mod.rs index 906ff3d431f..a0fc160abb0 100644 --- a/crates/cargo-util-schemas/src/manifest/mod.rs +++ b/crates/cargo-util-schemas/src/manifest/mod.rs @@ -41,7 +41,7 @@ pub struct TomlManifest { pub package: Option>, pub project: Option>, pub badges: Option>>, - pub features: Option>>, + pub features: Option>, pub lib: Option, pub bin: Option>, pub example: Option>, @@ -110,7 +110,7 @@ impl TomlManifest { .or(self.build_dependencies2.as_ref()) } - pub fn features(&self) -> Option<&BTreeMap>> { + pub fn features(&self) -> Option<&BTreeMap> { self.features.as_ref() } @@ -1521,6 +1521,63 @@ impl TomlPlatform { } } +/// Definition of a feature. +#[derive(Clone, Debug, Serialize)] +#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))] +#[serde(untagged)] +pub enum FeatureDefinition { + /// Features that this feature enables. + Array(Vec), + /// Unstable feature `feature-metadata`. Metadata of this feature. + Metadata(FeatureMetadata), +} + +// Implementing `Deserialize` manually allows for a better error message when the `enables` key is +// missing. +impl<'de> de::Deserialize<'de> for FeatureDefinition { + fn deserialize(d: D) -> Result + where + D: de::Deserializer<'de>, + { + UntaggedEnumVisitor::new() + .seq(|seq| { + seq.deserialize::>() + .map(FeatureDefinition::Array) + }) + .map(|seq| { + seq.deserialize::() + .map(FeatureDefinition::Metadata) + }) + .deserialize(d) + } +} + +impl FeatureDefinition { + /// Returns the features that this feature enables. + pub fn enables(&self) -> &[String] { + match self { + Self::Array(features) => features, + Self::Metadata(FeatureMetadata { + enables: features, .. + }) => features, + } + } +} + +/// Unstable feature `feature-metadata`. Metadata of a feature. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))] +pub struct FeatureMetadata { + /// Features that this feature enables. + pub enables: Vec, + + /// This is here to provide a way to see the "unused manifest keys" when deserializing + #[serde(skip_serializing)] + #[serde(flatten)] + #[cfg_attr(feature = "unstable-schema", schemars(skip))] + pub _unused_keys: BTreeMap, +} + #[derive(Serialize, Debug, Clone)] #[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))] pub struct InheritableLints { diff --git a/src/cargo/core/features.rs b/src/cargo/core/features.rs index 4e17898d6e0..ed8ba617369 100644 --- a/src/cargo/core/features.rs +++ b/src/cargo/core/features.rs @@ -519,6 +519,9 @@ features! { /// Allow paths that resolve relatively to a base specified in the config. (unstable, path_bases, "", "reference/unstable.html#path-bases"), + + /// Allow to use a table for defining features. + (unstable, feature_metadata, "", "reference/unstable.html#feature_metadata"), } /// Status and metadata for a single unstable feature. diff --git a/src/cargo/ops/registry/publish.rs b/src/cargo/ops/registry/publish.rs index de0f9ac4e0a..5b4d5e01cff 100644 --- a/src/cargo/ops/registry/publish.rs +++ b/src/cargo/ops/registry/publish.rs @@ -539,7 +539,7 @@ pub(crate) fn prepare_transmit( .map(|(feat, values)| { ( feat.to_string(), - values.iter().map(|fv| fv.to_string()).collect(), + values.enables().iter().map(|fv| fv.to_string()).collect(), ) }) .collect::>>(), diff --git a/src/cargo/util/toml/mod.rs b/src/cargo/util/toml/mod.rs index bc80097b705..92164a680e8 100644 --- a/src/cargo/util/toml/mod.rs +++ b/src/cargo/util/toml/mod.rs @@ -11,7 +11,8 @@ use anyhow::{anyhow, bail, Context as _}; use cargo_platform::Platform; use cargo_util::paths; use cargo_util_schemas::manifest::{ - self, PackageName, PathBaseName, TomlDependency, TomlDetailedDependency, TomlManifest, + self, FeatureDefinition, FeatureMetadata, FeatureName, PackageName, PathBaseName, + TomlDependency, TomlDetailedDependency, TomlManifest, }; use cargo_util_schemas::manifest::{RustVersion, StringOrBool}; use itertools::Itertools; @@ -708,8 +709,8 @@ fn default_readme_from_package_root(package_root: &Path) -> Option { #[tracing::instrument(skip_all)] fn normalize_features( - original_features: Option<&BTreeMap>>, -) -> CargoResult>>> { + original_features: Option<&BTreeMap>, +) -> CargoResult>> { let Some(normalized_features) = original_features.cloned() else { return Ok(None); }; @@ -1296,6 +1297,8 @@ pub fn to_real_manifest( } } + validate_feature_definitions(&features, original_toml.features.as_ref(), warnings)?; + validate_dependencies(original_toml.dependencies.as_ref(), None, None, warnings)?; validate_dependencies( original_toml.dev_dependencies(), @@ -1497,7 +1500,7 @@ pub fn to_real_manifest( .map(|(k, v)| { ( InternedString::new(k), - v.iter().map(InternedString::from).collect(), + v.enables().iter().map(InternedString::from).collect(), ) }) .collect(), @@ -1764,6 +1767,32 @@ fn to_virtual_manifest( Ok(manifest) } +fn validate_feature_definitions( + cargo_features: &Features, + features: Option<&BTreeMap>, + warnings: &mut Vec, +) -> CargoResult<()> { + let Some(features) = features else { + return Ok(()); + }; + + for (feature, feature_definition) in features { + match feature_definition { + FeatureDefinition::Array(..) => {} + FeatureDefinition::Metadata(FeatureMetadata { _unused_keys, .. }) => { + cargo_features.require(Feature::feature_metadata())?; + warnings.extend( + _unused_keys + .keys() + .map(|k| format!("unused manifest key: `features.{feature}.{k}`")), + ); + } + } + } + + Ok(()) +} + #[tracing::instrument(skip_all)] fn validate_dependencies( original_deps: Option<&BTreeMap>, @@ -2902,16 +2931,23 @@ fn prepare_toml_for_publish( }; features.values_mut().for_each(|feature_deps| { - feature_deps.retain(|feature_dep| { - let feature_value = FeatureValue::new(InternedString::new(feature_dep)); - match feature_value { - FeatureValue::Dep { dep_name } | FeatureValue::DepFeature { dep_name, .. } => { - let k = &manifest::PackageName::new(dep_name.to_string()).unwrap(); - dep_name_set.contains(k) + let feature_array = feature_deps + .enables() + .iter() + .filter(|feature_dep| { + let feature_value = FeatureValue::new(InternedString::new(feature_dep)); + match feature_value { + FeatureValue::Dep { dep_name } + | FeatureValue::DepFeature { dep_name, .. } => { + let k = &manifest::PackageName::new(dep_name.to_string()).unwrap(); + dep_name_set.contains(k) + } + _ => true, } - _ => true, - } - }); + }) + .cloned() + .collect(); + *feature_deps = FeatureDefinition::Array(feature_array); }); } diff --git a/src/doc/src/reference/unstable.md b/src/doc/src/reference/unstable.md index 3eba4e45804..44e3a5fc13c 100644 --- a/src/doc/src/reference/unstable.md +++ b/src/doc/src/reference/unstable.md @@ -103,6 +103,7 @@ Each new feature described below should explain how to use it. * [Profile `trim-paths` option](#profile-trim-paths-option) --- Control the sanitization of file paths in build outputs. * [`[lints.cargo]`](#lintscargo) --- Allows configuring lints for Cargo. * [path bases](#path-bases) --- Named base directories for path dependencies. + * [feature-metadata](#feature-metadata) --- Table syntax for feature definitions. * Information and metadata * [Build-plan](#build-plan) --- Emits JSON information on which commands will be run. * [unit-graph](#unit-graph) --- Emits JSON for Cargo's internal graph structure. @@ -1619,6 +1620,21 @@ will prefer the value in the configuration. The allows Cargo to add new built-in path bases without compatibility issues (as existing uses will shadow the built-in name). +## feature-metadata + +* Tracking Issue: [#14157](https://github.com/rust-lang/cargo/issues/14157) + +This allows to use a table when defining features, with a required `enables` key: + +```toml +[features] +# same as `foo = []` +foo = { enables = [] } +``` + +This is equivalent to the array-of-strings syntax. +Support for other keys should be added later. + ## lockfile-path * Original Issue: [#5707](https://github.com/rust-lang/cargo/issues/5707) * Tracking Issue: [#14421](https://github.com/rust-lang/cargo/issues/14421) diff --git a/tests/testsuite/features.rs b/tests/testsuite/features.rs index 94a69cc6e11..21a10fe5f29 100644 --- a/tests/testsuite/features.rs +++ b/tests/testsuite/features.rs @@ -1,6 +1,9 @@ //! Tests for `[features]` table. +use std::fs::File; + use cargo_test_support::prelude::*; +use cargo_test_support::publish::validate_crate_contents; use cargo_test_support::registry::{Dependency, Package}; use cargo_test_support::str; use cargo_test_support::{basic_manifest, project}; @@ -2282,3 +2285,190 @@ fn invalid_feature_name_slash_error() { "#]]) .run(); } + +#[cargo_test] +fn feature_metadata() { + let p = project() + .file( + "Cargo.toml", + r#" + cargo-features = ["feature-metadata"] + + [package] + name = "foo" + edition = "2015" + + [features] + a = [] + b = [] + c = { enables = ["a", "b"] } + "#, + ) + .file( + "src/main.rs", + r#" + #[cfg(feature = "a")] + fn a() {} + #[cfg(feature = "b")] + fn b() {} + fn main() { + a(); + b(); + } + "#, + ) + .build(); + + p.cargo("check --features c") + .masquerade_as_nightly_cargo(&[]) + .run(); +} + +#[cargo_test] +fn feature_metadata_is_unstable() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + edition = "2015" + + [features] + a = { enables = [] } + "#, + ) + .file("src/main.rs", "") + .build(); + + p.cargo("check --features a") + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] failed to parse manifest at `[ROOT]/foo/Cargo.toml` + +Caused by: + feature `feature-metadata` is required + + The package requires the Cargo feature called `feature-metadata`, but that feature is not stabilized in this version of Cargo ([..]). + Consider trying a newer version of Cargo (this may require the nightly release). + See https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#feature_metadata for more information about the status of this feature. + +"#]]) + .run(); +} + +#[cargo_test] +fn feature_metadata_missing_enables() { + let p = project() + .file( + "Cargo.toml", + r#" + cargo-features = ["feature-metadata"] + + [package] + name = "foo" + edition = "2015" + + [features] + foo = {} + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check") + .masquerade_as_nightly_cargo(&[]) + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] missing field `enables` + --> Cargo.toml:[..]:23 + | +[..] | foo = {} + | ^^ + | + +"#]]) + .run(); +} + +#[cargo_test] +fn unused_keys_in_feature_metadata() { + let p = project() + .file( + "Cargo.toml", + r#" + cargo-features = ["feature-metadata"] + + [package] + name = "foo" + edition = "2015" + + [features] + foo = { enables = ["bar"], a = false, b = true } + bar = [] + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check") + .masquerade_as_nightly_cargo(&[]) + .with_stderr_data(str![[r#" +[WARNING] unused manifest key: `features.foo.a` +[WARNING] unused manifest key: `features.foo.b` +[CHECKING] foo v0.0.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn normalize_feature_metadata() { + let p = project() + .file( + "Cargo.toml", + r#" + cargo-features = ["feature-metadata"] + + [package] + name = "foo" + edition = "2015" + + [features] + a = [] + b = [] + c = { enables = ["a", "b"] } + "#, + ) + .file("src/main.rs", "") + .build(); + + p.cargo("package --no-verify") + .masquerade_as_nightly_cargo(&[]) + .run(); + let f = File::open(&p.root().join("target/package/foo-0.0.0.crate")).unwrap(); + let normalized_manifest = str![[r#" +... + +... + +... + +[features] +a = [] +b = [] +c = [ + "a", + "b", +] + +... +"#]]; + validate_crate_contents( + f, + "foo-0.0.0.crate", + &["Cargo.lock", "Cargo.toml", "Cargo.toml.orig", "src/main.rs"], + [("Cargo.toml", normalized_manifest)], + ); +}