Skip to content

Instantly share code, notes, and snippets.

@wycats
Created August 28, 2014 23:20
Show Gist options
  • Save wycats/199f09a3ff1c8e6dcebd to your computer and use it in GitHub Desktop.
Save wycats/199f09a3ff1c8e6dcebd to your computer and use it in GitHub Desktop.

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, the postgres-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.

Features

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

Rules

  1. Feature names must not conflict with package names. This is because they are opted into via features = [...], which only has a single namespace
  2. With the exception of the default feature, all features are opt-in. To opt out of the default feature, use default-features = false and cherry-pick individual features.
  3. 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")]

General Usage

In End Products

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 Packages

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment