Cargo features - spooky action at a distance?

The situation is as following:

crate A depends on L, using feature F, but not enabling the feature in Cargo.toml
crate B depends on A and L, enabling feature F in it's Cargo.toml
crate A on itself does not build succesfully
crate B builds just fine

If I understand correctly, this is a side-effect of feature unification, but I highly doubt this is intended.

2 Likes

It's worse for workspaces. A and B can be independent crates sharing a workspace. A can have feature F enabled for crate D. B may depend on D without enabling F, but if the workspace is built as a whole, B can still use F. If B is compiled separately from a clean build with cargo build --package B, compilation fails.

5 Likes

Features are a bit of weak point of Cargo/Rust. You're totally right that it's very easy to accidentally depend on a feature without enabling it explicitly. Also agreed that it's even worse in workspaces.

Some crates also break the expectation of features being additive making it pretty easy to get into situations where two parts of your dep graph end up enabling incompatible features of a crate.

I would much prefer if there was a better way to detect when a feature is accidentally depended upon or when features conflict with each other. For a long time, conditional compilation was applied too early in the Rust build and then the information was lost, but recent updates track a bit more info about features and conditional compilation to improve diagnostics. I hope that it continues in this direction with better tracking of features.

6 Likes

We actually did run into this in a monorepo workspace.

Best way to handle this atm is cargo hack as it can build each package individually to avoid feature unification.

Help uncover issues with too few features requested that is masked by feature unification · Issue #14021 · rust-lang/cargo · GitHub is abut finding some kind of way to help report these problems to the user.

4 Likes

Another fun one is if you have a macro that generates different code depending on feature flags set. And if that generated code assumes that the same feature flags are set during source compilation. This means that a build dependency enabling a feature flag for the crate with the macro can break compilation because thar feature flag is not necessarily set during source compilation.

2 Likes

Tracing hit this at some point, where it was implicitly relying on a not enabled regex feature for the env filter parser.

On a related note to features and proc-macros: Can't generate feature gated code from a proc macro in a reliable way when used by both normal/build dependencies with `resolver="2"` · Issue #14415 · rust-lang/cargo · GitHub

If we had a way to allow the generated code to know what features are enabled in the library you are generating code against, that'd be great (similarly, we need a $crate for proc macros).

1 Like

I handled this in one crate by shimming almost everything through a macro_rules! defined in the runtime crate. It works, even if it isn't the nicest to set up if you want the proc macro to directly process any more than a single feature flag.

Please :pleading_face: (you can shim it for functionlike macros but attributes have no choice but to assume and/or cheat)

1 Like

I've noticed also problem:

// In base_crate:
trait SomeTrait {
   fn do_something();
   #[cfg(feature="abc")]
   fn do_abc();
}
enum SomeEnum {
   Foo, 
   Bar,
   #[feature="abc"]
   Abc,
}

Then, i'd try in my library that depends on base_crate, and has a "abc" feature that forward to "base_crate/abc"

impl base_crate SomeTrait for MyType {
   fn do_something() { /* ... */ }
   #[cfg(feature="abc")]
   fn do_abc() { /* ... */ }
}
fn process(e: &base_crate::SomeEnum) {
    match e {
        Foo => { /* ... */ }
        Bar => { /* ... */ }
        #[feature="abc"]
        Abc => { /* ... */ }
   }
}

It somehow works, unless someone enables the feature behind my back through another path in the dependency tree. Then I'd get error in my crate that the do_abc is not implemented, or that Abc is not matched.

Suggestion

What i'd suggest would be a way to write #[cfg(feature="<crate>/<feature>")] like so:

impl base_crate SomeTrait for MyType {
   fn do_something() { /* ... */ }
   #[cfg(feature="base_crate/abc")]
   fn do_abc() { /* ... */ }
}

This would also help for #14415 above as proc-macros could generate code containing #[cfg(feature="main_crate/some_feature")]

To make it work, all the enabled features of a crate would have to be stored in the crate metadata. (as well as the possible features for check-cfg) This would have the drawback to make the cfg(feature=...) a bit special compared to other cfg in rustc, but i think this would be worth it.

2 Likes