Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Summary

Allow disabling default features locally when inheriting a dependency.

[workspace]

[workspace.dependencies]
serde = "1"
[package]
name = "foo"

[dependencies]
serde = { workspace = true, default-features = false }

Motivation

Say you are trying to create a package in the above workspace:

$ cargo new default-false
$ cd default-false
$ cargo add serde --no-default-features
error: cannot override workspace dependency with `--default-features`,
either change `workspace.dependencies.serde.default-features` or
define the dependency exclusively in the package's manifest
$ vi Cargo.toml  # manually add the above dependency
$ cargo check
error: failed to parse manifest at `default-false/Cargo.toml`

Caused by:
  error inheriting `serde` from workspace root manifest's `workspace.dependencies.serde`

Caused by:
  `default-features = false` cannot override workspace's `default-features`

This gets in the way of universally recommending [workspace.dependencies], e.g.

  • #15180: cargo new should add the new package to workspace.dependencies
  • #10608: cargo add should add the dependency to workspace.dependencies and use workspace = true
  • #15578: lint if a dependency does not use workspace = true

Granted, there are other problems, including:

  • Without additional tooling support, you can’t tell from looking at git log . in a package root all of the changes that can break compatibility
  • #12546: cannot inherit packages renamed in workspace.dependencies

RFC 2906 said:

For now if a workspace = true dependency is specified then also specifying the default-features value is disallowed. The default-features value for a directive is inherited from the [workspace.dependencies] declaration, which defaults to true if nothing else is specified.

See also the tracking issue discussion at https://github.com/rust-lang/cargo/issues/8415#issuecomment-727245250

However, initial support didn’t error or even emit an “unused manifest key” warning due to bugs. In addressing this in #11409, support was added for workspace = true, default-features = false but in a surgical manner. The proposed mental model for this was that the default feature is additive like all other features though it didn’t quite accomplish that. When inheriting features the package extends but does not override the workspace. A dependency with default-features = true (implicitly or explicitly) is like a package with features = ["default"]. So if you have a workspace dependency with features = ["default"] and a package with features = [] (implicitly or explicitly), then the end result is features = ["default"].

As this left some confusing cases, Cargo produced warnings. These warnings were turned into hard errors for Edition 2024 in #13839.

This left us with:

WorkspaceMember1.64 behavior1.69 behavior2024 edition behavior
nothingnothingEnabledEnabledEnabled
nothingdf=falseEnabledEnabled, warning that it is ignoredError
nothingdf=trueEnabledEnabledEnabled
df=falsenothingDisabledDisabledDisabled
df=falsedf=falseDisabledDisabledDisabled
df=falsedf=trueDisabledEnabledEnabled
df=truenothingEnabledEnabledEnabled
df=truedf=falseEnabledEnabled, warningError
df=truedf=trueEnabledEnabledEnabled

(changes bolded)

This eventually led to #12162 being opened because the “features are additive” model prevents some valid cases from working, including:

  • workspace.dependencies.foo = "version": packages cannot disable default features
  • workspace.dependencies.foo = { version = "", default-features = false }: applies to all packages, requiring default-features = true in all packages that do not want it

When discussing whether to allow inheriting of public, we are starting with the answer of “no” (#13125). The thought process being that inheritance should be about consolidating shared requirements but public is unlikely to be inherently a shared requirement for every dependent in a workspace. This likely extends to both default-features and features and may be reason enough to deprecate inheriting them, stopping altogether in a future edition. Instead, workspace.dependencies should likely focus purely on inheriting of a dependency source. Before we even get there, it needs to be possible to not specify default-features in workspace.dependencies.

Guide-level explanation

When inheriting a dependency in Edition 2024+, instead of treating default-features as a hypothetical entry in features, layer the package dependency on top of the workspace dependency on top of the default.

Reference-level explanation

In pseudo-code, this would be:

let default_features = if Edition::E2024 <= package.edition {
    package_dep.default_features
        .or_else(|| workspace_dep.default_features)
        .unwrap_or(true)
} else {
    // ... existing behavior
};

Documentation update

From Inheriting a dependency from a workspace

Along with the workspace key, dependencies can also include these keys:

  • optional: Note that the [workspace.dependencies] table is not allowed to specify optional.
  • features: These are additive with the features declared in the [workspace.dependencies]
  • default-features: This overrides the value set in [workspace.dependencies] on Edition 2024 (requires MSRV of 1.100)

Inherited dependencies cannot use any other dependency key (such as version or default-features).

Drawbacks

More churn on the meaning of default-features. However, default-features combined with workspace.dependencies likely puts this in a minority case that we can likely gloss over this in most situations.

Rationale and alternatives

Alternatives

WorkspaceMember1.64 behavior1.69 behavior2024 edition behaviorProposed
nothingnothingEnabledEnabledEnabledEnabled
nothingdf=falseEnabledEnabled, warning that it is ignoredErrorDisabled
nothingdf=trueEnabledEnabledEnabledEnabled
df=falsenothingDisabledDisabledDisabledDisabled
df=falsedf=falseDisabledDisabledDisabledDisabled
df=falsedf=trueDisabledEnabledEnabledEnabled
df=truenothingEnabledEnabledEnabledEnabled
df=truedf=falseEnabledEnabled, warningErrorDisabled
df=truedf=trueEnabledEnabledEnabledEnabled

(changes bolded)

“Workspace always wins” model

  • 1.64 behavior
  • Forces sharing of default-features
  • Has confusing cases where what you see locally (default-features = false) is not what happens
let default_features = workspace.default_features
    .unwrap_or(true);

“Almost additive” model

  • 1.69 behavior
  • Disabling default-features in one package requires touching all packages
  • Has confusing cases where what you see locally (default-features = false) is not what happens
let default_features = match (workspace.default_features, package.default_features) {
    (Some(false), Some(true)) => Some(true),
    (Some(ws), _) => Some(ws),
    (None, _) => Some(true),
};

“Layered” model

  • Proposed behavior
  • Allows package-level control of default-features without using workspace-level control, as if support doesn’t exist at the workspace
let default_features = package_dep.default_features
    .or_else(|| workspace_dep.default_features)
    .unwrap_or(true);

“Package always wins” model

  • Potential behavior if we remove dependency feature inheritance in a later edition
let default_features = package.default_features.unwrap_or(true);

Prior art

Unresolved questions

Future possibilities

Deprecate dependency feature inheritance, removing it in a future edition.