Description
Background
Features in a Cargo crate setup can currently be created in two ways. Features can be manually added:
[features]
foo = []
bar = ["a/b", "c"]
Manually added features can depend on other features. bar
above depends on feature c
from its own crate, and feature b
from crate a
.
In addition, all dependencies have an implicit associated feature with the same name. However, these features cannot depend on other features.
The Problem
The current setup misses some important use cases. For example, rust-postgres
has a uuid
feature which adds support for the Postgres UUID
type by linking to the uuid
crate and generating trait implementations for the uuid::Uuid
type. A separate crate, rust-postgres-array
adds support for array types. Here, we would like to have the same setup - a uuid
feature that adds support for the Postgres UUID[]
type by linking to the uuid
crate and adding the appropriate trait implementations. The uuid
feature of rust-postgres-array
needs to depend on functionality provided by the uuid
feature of rust-postgres
, but this isn't currently possible since uuid
is a feature implicitly created by the uuid
dependency.
There exists a kind of workaround here, where you define a feature with a different name that depends on uuid
:
[features]
uuid_support = ["uuid", "postgres/uuid"]
That's a bit unfortunate, though. Having the feature name match the crate name avoids a bit of verbosity and makes it explicit that it's using that crate specifically (for example, support for the Postgres JSON
type is provided via rustc-serialize::Json
, and naming the feature rustc-serialize
makes this point more clear than naming it json
would, as well as keeping room for support via some other crate as well in the future).
I was initially going to change Cargo to add support for an optional dependency depending on features of other dependencies like so:
[dependencies]
postgres = "0.6"
[dependencies.uuid]
optional = true
version = "0.1"
features = ["postgres/uuid"]
After some more thought, I came to the conclusion that this was the wrong way to go about it, and deeper changes to Cargo's feature API would solve both this issue and some other issues:
Backwards compatibility hazards
The fact that all dependencies are also features poses backwards compatibility hazards for a crate author when making changes that would not otherwise be user facing. Imagine Alice makes a crate foo
:
[package]
name = "foo"
version = "0.1.0"
authors = []
[dependencies]
bar = "0.1"
And Bob makes a crate baz
that depends on foo
:
[package]
name = "baz"
version = "0.0.1"
authors = []
[dependencies.foo]
version = "0.1"
features = ["bar"]
Note that he's enabled the bar
feature on the foo
crate. This is basically a no-op (it defines the feature = "bar"
cfg when compiling foo
, but let's assume that foo
doesn't use that anywhere).
Now imagine Alice does some internal refactoring that allows her to remove the dependency on bar
. She publishes version 0.1.1, but suddenly a bug report is filed on foo
. The new release broke Bob's crate, since the bar
feature no longer exists! Bob should never have enabled that "feature" in the first place, but that doesn't make the situation any better for the people that depend on the baz
crate.
So why not forbid the use of features named after non-optional dependencies?
Features matching the name of a non-optional dependency are another important use case! rust-postgres
currently depends on time
for internal use that doesn't touch the public API. It also implements some traits for time::Timespec
to enable support for the Postgres TIMESTAMP
type. However, I want to make the TIMESTAMP
support opt-in via a feature to enable me to drop the dependency on time
if future work on rust-postgres
removes its internal usage. If the user facing part (the trait implementations) is behind a feature, I can make the time
dependency optional without breaking backwards compatibility.
Proposal
As discussed above, the current feature implementation is both too restrictive to enable some uses and too eager to enable dependency flexibility. Both of these problems boil down to Cargo's intertwining of features and dependencies. It seems like the solution here is to remove that interconnectedness.
Dependencies, mandatory or optional, will no longer automatically have associated features. Features can be defined with two forms. The short form matches visually with current feature syntax, though the meaning will be adjusted:
[features]
bar = ["a", "b/c"]
[dependencies.a]
optional = true
[dependencies.b]
optional = true
This defines a feature bar
. While in current Cargo, bar
would also activate the a
and b/c
features, it would now activate the optional dependency on crate a
, as well as crate b
, activating crate b
's feature c
. Note that the behavior here is actually identical between current Cargo and Cargo after the proposed changes. It will make a difference in a case like this:
[features]
bar = ["a"]
a = []
In current Cargo, this is valid, and activating feature bar
causes feature a
to be activated as well. In this proposal, this would be an error, since a
is a feature and not a dependency.
If a feature needs to activate another feature, a more verbose method is allowed:
[features]
a = []
[features.b]
features = ["a"]
dependencies = ["foo"]
[dependencies.foo]
optional = true
Having this extended form could also be useful in the future to allow the addition of things like descriptions which could be displayed on crates.io:
[features.foo]
description = "Adds support for the foo thingy"