Workspace dependency resolution is a footgun

Cargo workspace projects are built using separate independent crates that can be tested and compiled individually, while belonging to the same project (git repository, for example). This makes it easy to isolate components, identify dependencies between sub-crates, and more.

This also comes with rough edges, as features are no longer resolved per-crate, and are instead shared across the workspace. This is documented in the Cargo book:

When building multiple packages in a workspace (such as with --workspace or multiple -p flags), the features of the dependencies of all of those packages are unified.

With a disclaimer:

If you have a circumstance where you want to avoid that unification for different workspace members, you will need to build them via separate cargo invocations.

The described behavior is simple to reproduce: a simple workspace project with two crates (crate-a, crate-b) triggers this bug. Crate A uses serde and requires the derive feature (but doesn't use it), Crate B has a dependency (direct or not) on serde, and its derive feature.

  • Running cargo check / cargo test works.
  • Running cargo check -p crate-b works.
  • Running cargo check -p crate-a doesn't work.
  • Removing crate-b breaks crate-a, yet they don't share a dependency relationship.
  • Removing the unused serde dependency from crate-b breaks crate-a.

In a typical setup, CI will run tests using cargo check/test, and won't notice that crate-a is misconfigured. In my opinion, this is a foot gun, and should be highlighted on the check / test pages. This allows the creation of "bad" crates that seem to work well, but break when unrelated changes happen.

A developer cloning a repository should expect the tests to run and pass, both globally (cargo test), and when run individually (cargo test -p .... and cargo test --lib ... --manifest-path ...). This is, currently, not guaranteed due to the feature unification.

A library with examples sub-crates (eg: syn) could easily have misconfigured dependencies, as the features used by examples pollute the other crates – unless the author is careful enough to test each crate individually. (Note: I'm not saying syn is impacted, I trust dtolnay! :))

As a developer, I expect cargo check and cargo test to throw errors when a crate or its dependencies are misconfigured, if doesn't require the right features from its dependencies. Having a way to toggle this behavior (either command flag, or Cargo.toml config?) would work, I believe.

2 Likes

It's not just workspaces, you can have feature leakage between your normal and dev-dependencies and between different subsets of your dependencies. The only way to really test for feature sets is to use tooling like cargo-hack that isolates these different situations, cargo hack check --workspace --feature-powerset --no-dev-deps is what I normally use which runs 256 different builds in one of my crates to ensure no features leak from one feature to another.

This is only the case when not using the newer feature resolver (which is default in the 2021 edition).

AFAIK the newer feature resolver only affected merging of build and runtime deps features, not dev or transitive ones.

The new resolver handles both build and dev dependencies, as well as dependencies for non-enabled targets. See:

1 Like