Did feature unification bit me hard?

Just an FYI for other hacky programmers like myself :slight_smile:

Our CI, which calls a bunch of cargo x commands (clippy, test, udeps, audit etc.) on the root workspace worked fine, but then building an individual crate, which was included and built successfully by CI failed.

It failed because it depended on a feature in an external crate but hadn't declared that feature.

NOTE: the actually buggy crate (nhg-std-web) is a library crate depended upon by the crate I'm building (reference-data). Both crates are in the same workspace.

Running cargo check at the workspace root (irrelevance snipped):

    Blocking waiting for file lock on build directory
    Checking reference-data v0.1.0 (/Users/coliny/Dev/com.qfi.health-CLEAN/src/rust/common-crates/reference-data)
    Checking dashboard_backend_server v0.1.0 (/Users/coliny/Dev/com.qfi.health-CLEAN/src/rust/common-crates/dashboard_backend_server)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 11.84s

cargo check in the failing crate:

cargo check
    Checking nhg-std-web v0.1.0 (/Users/coliny/Dev/com.qfi.health-CLEAN/src/rust/nhg-std/nhg-std-web)
error[E0432]: unresolved import `tower_http::cors`
   --> nhg-std/nhg-std-web/src/lib.rs:16:21
    |
16  |     use tower_http::cors::{Any, CorsLayer};
    |                     ^^^^ could not find `cors` in `tower_http`
    |
note: found an item that was configured out
   --> /Users/coliny/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tower-http-0.5.2/src/lib.rs:316:9
    |
316 | pub mod cors;
    |         ^^^^
    = note: the item is gated behind the `cors` feature

For more information about this error, try `rustc --explain E0432`.
error: could not compile `nhg-std-web` (lib) due to 1 previous error

~/Dev/com.qfi.health-CLEAN/sr/rust/c/reference-data on   573-ude-brok…t-through-ci !1 ?1
❯

Adding the missing feature to the failing crate's Cargo.toml works and everything works at the root level and the individual crate level.

Feature unification (Features - The Cargo Book) suggests that this can happen if another crate requires that feature. However, no other crate in my workspace does. Many other crates in my workspace are using the external crate in question, but none are adding that feature:

 rg -g Cargo.toml cors

~/Dev/com.qfi.health-CLEAN/src/rust

What the heck? Is it possible an external crate is adding the feature? I'd "ass u me"d unification would only consider crates in my workspace. Is that right?

So I don't understand:

  • if this is feature unification - where is the cor feature being activated
  • does feature unification consider flags set by external dependencies (e.g. "my-crate -> external-crate->(features=["abc"])->external-crate-b; my-crate -> external-crate-b" will cause external-crate-b to be compiled with feature "abc" for any of my crates that depend on external-crate-b)?
  • how can I find out why and where a feature is activated? grep might not be sufficient if one feature (e.g. feature-a) activates another feature (e.g. feature-a) and I'm only depending on one (e.g. feature-a)

If this is feature unification, isn't this, like, horrendously dangerous/fragile? I know features are supposed to be discrete and atomic but hey, we're all human(ish). Isn't this a perfect recipe for "works on my machine/specific crate but fails on your machine/crate root"?

Of course, the "bug" here is that my crate didn't specify its requirements and should have listed cors as the feature it needs, but heck, I'm glad it did, as it forced me to confront this nightmare.

What am I missing? Isn't this, well, crazy?

Thanks!

No. The whole dependency graph is unified, including third party libraries. So this assumption:

is correct. Otherwise you'd randomly break third party libraries by ignoring the features they themselves require from their dependencies.

If you build a whole workspace, feature unification will happen across all packages, which is why you didn't spot the missing feature specification in one of the workspace members. Building each package separately is indeed the only way to spot this.

1 Like

Wow, it's build fail season!

2 Likes

thanks both. This feels whackily in conflict with Rust's "robustness". I'm sure this is my "un/conscious incompetence" not understanding the nuances around this, and I'm pretty sure this is the best-in-the-round solution, but yeah.

Oh well, TIL :-). Thanks both!

2 Likes

I wish Cargo at least had a switch to toggle separate builds. I myself was bitten by this by not realizing my no_std lib’s tests were broken with the std feature disabled because a bin crate in same workspace enabled it :<

5 Likes

It is. It's not your misunderstanding.

Unfortunately, it's not easy to fix. The proper fix would be to build each crate separately, in a clean environment, but that would mean that compiled artifacts could not be shared (at least a significant part of them), which would defeat the purpose of workspaces and blow up your target folder by the number of crates.

I'd say the best current solution is to keep existing behaviour for local development, while compiling each crate individually from scratch when doing a release.

3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.