The goal of "features" is to make it possible to express:
- Optional dependencies, which enhance a package, but are not required
- Clusters of optional dependencies, such as "postgres", that
would include the
postgres
package, thepostgres-macros
package, and possibly other packages (such as development-time mocking libraries, debugging tools, etc.)
We originally thought that a related goal, the ability to specify a list of "pick exactly one" options to choose from, was in scope for "features", but have since come to believe that those choices are better expressed as "Provides"-style virtual dependencies.
To flesh out the design, we used Rails, which has a combination of mandatory dependencies, optional opt-in dependencies and optional opt-out dependencies.
Rails uses a Bundler-specific structure to express these
dependencies (using a generated Gemfile
), but we were able
to use it as a real-world example of the constraints.
The Rails package, with optional dependencies expressed as features.
[package]
name = "rails"
[features]
# The "default" set of optional packages. Most people will want
# to use these packages, but they are strictly optional
default = ["sass-rails", "uglifier", "jquery-rails", "sdoc"]
# The "omakase" set of optional packages. These are packages
# curated by DHH as desirable for normal usage. Some developers
# will leave the "default" packages, but disable the hand-curated
# Omakase packages.
omakase = ["coffee-rails", "turbolinks", "jbuilder"]
# The "secure-password" feature depends on the bcrypt gem. This
# aliasing will allow people to talk about the feature in a
# higher-level way and allow Rails to add more requirements to
# the feature in the future.
secure-password = ["bcrypt"]
[dependencies]
# These packages are mandatory, and form the core of the Rails
# distribution. They are locked to particular versions to make
# sure that Cargo will only include one, precise version of
# Rails in the compiled binary.
actionmailer = "=4.1.5"
actionpack = "=4.1.5"
actionview = "=4.1.5"
activemodel = "=4.1.5"
activerecord = "=4.1.5"
activesupport = "=4.1.5"
railties = "=4.1.5"
# A few other mandatory dependencies that do not have strict
# version dependencies
bundler = "1.3.0"
sprockets-rails = "2.0.0"
# A list of all of the optional dependencies, some of which
# are included in the above "features". They can be opted
# into by apps.
sass-rails = { version = "4.0.3", optional = true }
uglifier = { version = "1.3.0", optional = true }
coffee-rails = { version = "4.0.0", optional = true }
bcrypt = { version = "3.1.7", optional = true }
unicorn = { version = "*", optional = true }
turbolinks = { version = "*", optional = true }
jbuilder = { version = "*", optional = true }
[dev-dependencies]
sdoc = { version = "0.4.0", optional = true }
debugger = { version = "*", optional = true }
spring = { version = "*", optional = true }
To use Rails,
[package]
name = "my-app"
[dependencies.rails]
version = "4.1.5"
features = ["omakase", "unicorn"]
# do not include the default features, and optionally
# cherry-pick individual features
# default-features = false
- Feature names must not conflict with package names. This
is because they are opted into via
features = [...]
, which only has a single namespace - With the exception of the
default
feature, all features are opt-in. To opt out of the default feature, usedefault-features = false
and cherry-pick individual features. - When a feature is selected, Cargo will call
rustc
with--cfg cargo_feature=${feature_name}
. If a feature group is included, both the group and all of its individual features will be included. This can be tested in code via#[cfg(cargo_feature = "turbolinks")]
One major use-case for this feature is specifying optional features in end-products. For example, the Servo project may want to include optional features that people can enable or disable when they build it.
In that case, Servo will describe features in its Cargo.toml
and they can be enabled using command-line flags:
$ cargo build --release --features "shumway pdf"
Default features could be excluded using --no-default-features
.
In most cases, the concept of "optional dependency" in a library is best expressed as a separate package that the top-level application depends on.
However, high-level packages, like Iron or Piston, may want the ability to curate a number of packages for easy installation. The current Cargo system allows them to curate a number of mandatory dependencies into a single package for easy installation.
In some cases, packages may want to provide additional curation for optional dependencies:
- Grouping a number of low-level optional dependencies together into a single high-level "feature".
- Specifying packages that are recommended (or suggested) to be included by users of the package (the "default" and "omakase" features in the motivating example).
- Including a feature (like
secure-password
in the motivating example) that will only work if an optional dependency is available, and would be difficult to implement as a separate package. For example, it may be overly difficult to design an IO package to be completely decoupled from OpenSSL, with opt-in via the inclusion of a separate package.
In almost all cases, it is an antipattern to use these features outside of high-level packages that are designed for curation. If a feature is optional, it can almost certainly be expressed as a separate package.