Federico
Federico

Reputation: 2133

How to prevent indirect feature dependencies with cargo?

When compiling a Rust workspace composed of multiple packages, is there a way to make cargo error when the succesfull compilation of a package foo depends on features that are not enabled by foo nor its dependencies?

Let me give a concrete example. With the workspace described below cargo build succeeds, but cargo build -p foo gives a compilation error because a tokio feature is missing in foo/Cargo.toml:

error[E0432]: unresolved import `tokio::io::AsyncWriteExt`
 --> foo/src/lib.rs:1:5
  |
1 | use tokio::io::AsyncWriteExt;
  |     ^^^^^^^^^^^-------------
  |     |          |
  |     |          help: a similar name exists in the module: `AsyncWrite`
  |     no `AsyncWriteExt` in `io`

For more information about this error, try `rustc --explain E0432`.
error: could not compile `foo` due to previous error

This is bad! cargo build happily accepts usages of indirect dependencies. That is, changing code that is not in foo nor in its dependencies (i.e. removing the io-util feature from bar/Cargo.toml) can introduce compilation errors in foo. I would like to detect and prevent this without having to try each time to compile each single package individually.


Structure of the workspace:

.
├── foo
│   ├── src
│   │   └── lib.rs
│   └── Cargo.toml
├── bar
│   ├── src
│   │   └── main.rs
│   └── Cargo.toml
└── Cargo.toml

./foo/src/lib.rs:

use tokio::io::AsyncWriteExt;

pub fn dummy(_: impl AsyncWriteExt) {
    unimplemented!()
}

./foo/Cargo.toml:

[package]
name = "foo"
version = "0.1.0"
edition = "2021"

[dependencies]
"tokio" = "1.21.2"

./bar/src/main.rs:

fn main() {
    println!("Hello, world!");
}

./bar/Cargo.toml:

[package]
name = "bar"
version = "0.1.0"
edition = "2021"

[dependencies]
"tokio" = { version = "1.21.2", features = ["io-util"] }
"foo" = { path = "../foo" }

./Cargo.toml:

[workspace]
members = [
    "foo",
    "bar",
]

Upvotes: 6

Views: 592

Answers (1)

Anders Evensen
Anders Evensen

Reputation: 881

Unfortunately, cargo build --workspace isn't sufficient for this, as it builds all package members at once, performing feature unification.

To build every package in a workspace individually, you can use the cargo-hack subcommand.

To install the subcommand, run:

cargo install cargo-hack

Once it's installed, you can run the following command to build all workspace packages individually:

cargo hack build --workspace

The output is as you expect:

error[E0432]: unresolved import `tokio::io::AsyncWriteExt`
 --> foo/src/lib.rs:1:5
  |
1 | use tokio::io::AsyncWriteExt;
  |     ^^^^^^^^^^^-------------
  |     |          |
  |     |          help: a similar name exists in the module: `AsyncWrite`
  |     no `AsyncWriteExt` in `io`

For more information about this error, try `rustc --explain E0432`.
error: could not compile `foo` due to previous error

This is the equivalent of building each package individually, but can be done in a single command. See the README entry for the --workspace flag for more details.

Upvotes: 1

Related Questions