RFC 3143: Cargo: weak dependencies and namespaced features

tools (cargo)

Summary

This RFC proposes to stabilize the weak-dep-features and namespaced-features enhancements to Cargo. These introduce the following additions to how Cargo's feature system works:

Weak dependency features adds the ability to specify that features of an optional dependency should be enabled only if the optional dependency is already enabled by another feature.

Namespaced features separates the namespaces of dependency names and feature names.

These enhancements are already implemented, but testing is limited because the syntax is only available on the nightly channel and is currently not allowed on crates.io. See Weak dependency features and Namespaced features for more information on how to use them on nightly.

Motivation

These enhancements to Cargo's feature system unlock the ability to express certain rules for features that are currently difficult or impossible to achieve today. These issues can crop up for many projects that make use of optional dependencies, and are well-known pain points. Introducing these enhancements can alleviate some of those pain points.

Weak dependency feature use cases

Sometimes a package may want to "forward" a feature to its dependencies. This can be done today with the dep_name/feat_name syntax in the [features] table. However, one drawback is that if the dependency is an optional dependency, this will implicitly enable the dependency, which may not be what you want. The weak dependency syntax provides a way to control whether or not the optional dependency is automatically enabled in that case.

For example, if your crate has optional std support, you may need to also enable std support on your dependencies. But you may not want to enable those dependencies just because std is enabled.

[dependencies]
serde = { version = "1.0", optional=true, default-features = false }

[features]
# This will also enable serde, which is probably not what you want.
std = ["serde/std"]

Namespaced features use cases

Currently, optional dependencies automatically get a feature of the same name to enable that dependency. However, this presents a compatibility hazard because the existence of that optional dependency may be an internal detail that a package may not want to expose. This can be mitigated somewhat through the use of documentation, but remains as an uncomfortable point where users may enable optional dependencies that you may not want them to have direct control over. Namespaced features provides a way to "hide" an optional dependency so that users cannot directly enable an optional dependency, but only through other explicitly defined features.

Also, removing the restriction that feature names cannot conflict with dependency names allows you to use more natural feature names. For example, if you have an optional dependency on serde, and you want to enable serde on your other dependencies at the same time, today you can't define a feature named serde, but instead are required to specify an alternate name like serde1, for example:

[features]
# This is an awkward name to use because dependencies and features
# share the same namespace.
serde1 = ["serde", "chrono/serde"]

Another example, here lazy_static is required when regex is used, but you don't want users to know about the existence of lazy_static.

[dependencies]
# This implicitly exposes both `regex` and `lazy_static` externally. However,
# enabling just `regex` will fail to compile without `lazy_static`.
regex = { version = "1.4.1", optional = true }
lazy_static = { version = "1.4.0", optional = true }

[features]
# Another circumstance where you have to pick a name that doesn't conflict,
# which may be confusing.
regexp = ["regex", "lazy_static"]

Guide-level explanation

The following is a replacement of the corresponding sections in the features guide.

Optional dependencies

Dependencies can be marked "optional", which means they will not be compiled by default. They can then be specified in the [features] table with a dep: prefix to indicate that they should be built when the given feature is enabled. For example, let's say in order to support the AVIF image format, our library needs two other dependencies to be enabled:

[dependencies]
ravif = { version = "0.6.3", optional = true }
rgb = { version = "0.8.25", optional = true }

[features]
avif = ["dep:ravif", "dep:rgb"]

In this example, the avif feature will enable the two listed dependencies.

If the optional dependency is not specified anywhere in the [features] table, Cargo will automatically define a feature of the same name. For example, let's say that our 2D image processing library uses an external package to handle GIF images. This can be expressed like this:

[dependencies]
gif = { version = "0.11.1", optional = true }

If dep:gif is not specified in the [features] table, then Cargo will automatically define a feature that looks like:

[features]
# Cargo automatically defines this if "dep:gif" is not specified anywhere else.
gif = ["dep:gif"]

This is a convenience if the name of the optional dependency is something you want to expose to the users of the package. If you don't want users to directly enable the optional dependency, then place the dep: strings in another feature that you do want exposed, such as in the avif example above.

You can then use cfg macros to conditionally use these features just like any other feature. For example, cfg(feature = "gif") or cfg(feature = "avif") can be used to conditionally include interfaces for those image formats.

Note: Another way to optionally include a dependency is to use platform-specific dependencies. Instead of using features, these are conditional based on the target platform.

Dependency features in the [features] table

Features of dependencies can also be enabled in the [features] table. This can be done with the dependency-name/feature-name syntax which says to enable the specified feature for that dependency. For example:

[dependencies]
jpeg-decoder = { version = "0.1.20", default-features = false }

[features]
# Enables parallel processing support by enabling the "rayon" feature of jpeg-decoder.
parallel = ["jpeg-decoder/rayon"]

If the dependency is an optional dependency, this syntax will also enable that dependency. If you do not want that behavior, the alternate syntax dependency-name?/feature-name with the ? character tells Cargo to only enable the given feature if the dependency is activated by another feature. For example:

[dependencies]
# Defines an optional dependency.
serde = { version = "1.0", optional=true, default-features = false }

[features]
# This "std" feature enables the "std" feature of serde, but only if serde is enabled.
std = ["serde?/std"]

Reference-level explanation

Index changes

For reference, the current index format is documented here.

A new "features2" field is added to the package description, which is an object with the same form as the "features" field. When reading the index, Cargo merges the values found in "features2" into "features". This helps prevent breaking versions of Cargo older than 1.19 (published 2017-07-20), which will return an error if they encounter the new syntax, even if there is a Cargo.lock file. These older versions will ignore the "features2" field, allowing them to behave correctly assuming there is a Cargo.lock file (or the packages they need do not use the new syntax).

During publishing, crates.io is responsible for separating the new syntax into the "features2" object before saving the entry in the index. Other registries do not need to bother as versions of Cargo older than 1.19 do not support other registries (though they may separate them if they wish).

Cargo does not add the "implicit" features for optional dependencies to the features table when publishing, that is still inferred automatically when reading the index.

Additionally, a new "v" field is added to the index package structure, which is an integer that indicates a "version" of the schema used. The default value is 1 if not specified, which indicates the schema before "features2" was added. The value 2 indicates that this package contains the new feature syntax, and possibly the "features2" key. During publishing, registries are responsible for setting the "v" field based on the presence of the new feature syntax.

The version field is added to help prevent older versions of Cargo from updating to newer versions of package that it doesn't understand. Cargo, since 1.51, already supports the "v" field, and will ignore any entries with a "v" value greater than 1. This means that running cargo update with a version older than 1.51 (published 2021-03-25) may not work correctly when updating a package that starts using the new syntax. This can have any of the following behaviors:

  1. It will update to the new version and work just fine if nothing actually uses the new feature syntax.
  2. It will skip the package if something requires one of the new features.
  3. It will update and successfully build, but build with the wrong features (because the new features aren't enabled correctly).
  4. It will update and the build will fail, because a new feature that is required isn't enabled.
  5. The update will fail if a matching version can't be found, since the required features aren't available.

Package authors that want to support versions of Cargo older than 1.51 may want to avoid using the new feature syntax.

Internal resolver changes

Internally, Cargo will switch to always using the "new" feature resolver, which can emulate the old resolver behavior if a package is using resolver="1" (which is the default for editions prior to 2021). This should not be perceptible to the user, but is a major architectural change in Cargo.

Drawbacks

Rationale and alternatives

Prior art

RFC 2957 contains a survey of other tools with systems similar to Cargo's features. Some tools treat the equivalent of "features" and "dependencies" together, and some treat them separately.

Prior issues

The following issues in Cargo's issue tracker cover the initial desires and proposals that lead to this design:

Unresolved questions

None at this time.

Future possibilities