While developing my framework historia, I faced a need to make a binary crate run with exactly one feature from a group. Cargo doesn't have any built-in options for mutually exclusive features — and enforcing this yourself breaks the --all-features flag, which I used for running tests and coverage reports. This post is about how I did it, what problems I faced, and how I solved them.
i think the idiomatic rust approach to this kind of incompatible backends would be to convert those features into other crates and allow the user to select which to use that way but i get that it could be a needless overcomplication of things over some basic feature flags
Yep, the approach you describe is absolutely valid, and I believe that in cases where you have a small, finite number of mutually exclusive features, it would probably be even better. But it still keeps the problem with the broken --all-features flag (in case the binaries are in the same directory as the library, rather than completely separate crates in the same workspace which could just be excluded), so I suppose this post might be useful even in that case.
However my situation is a bit different. I have multiple groups of mutually exclusive features, and these groups keep growing. I support multiple event brokers and databases, and I'm adding new ones periodically. If I added a separate binary for every possible broker-database combination, I'd have to add multiple new binaries each time I add any new feature, so for my specific case that seems too complicated. That's why I ended up with the approach described in the post.
An interesting approach!
A bit beside the point, but I'm not convinced you strictly need exclusive features here: WGPU and the underlying Naga compiler are comparable examples where you can selectively turn on any of a set of supported backends, and then at runtime it will pick the first enabled backend by default, but also lets you specifically select from the enabled features at runtime. This sounds kind of complicated, but it's actually not too fiddly!
From the description given, I could see a similar approach working here for you with some small additional value to the user, but perhaps I'm missing something.
Yeah, that absolutely can work, and I'm pretty sure that in some cases it's also a better approach. It could work in my case too: for example, to enable one backend or the other, I could just try to load the respective configs (a Kafka config or a NATS config would enable the respective backend). But with WGPU's approach all enabled backends still get compiled in, which is exactly what I'm trying to avoid — this might increase the binary size significantly (I'm planning to have at least 3 broker and at least 4 database features which would mean 12 combinations compiled while only 1 will be used), and it also might (very theoretically, though) lead to a mess in cases where the user has configs for both.
Also, the main way I expect the binary to be used is a Docker container, so I have an idea to build Docker containers for each possible broker-database combination in CI and pack them into some minimal scratch base. It should improve the user experience of picking up the required set of features (at least I suppose so).
I guess the question is which is more annoying for the user: enabling exactly one in each of a set of features, or having too much code compiled. If anyone thinks that sounds obviously weighted towards the first, they obviously hasn't spent much time compiling Rust ![]()
It seems to me that rust features being additive is in fact a very intentional design decision. When using a crate, transitive dependencies adding features to a crate should by design be a non-issue, because ideally all features are additive. To say it again, by design, enabling a feature should always be safe, in the ideal case.
In my opinion, there is no need to make these features mutually exclusive. If a user decides to compile in multiple providers or consumers, why are you stopping them? Shouldn't your library be designed to allow full flexibility from the user's perspective? It is sometimes tempting to use compile-time feature flags to strip away a layer of explicit context at runtime, but requiring the user to explicitly name a type is often better imo.
I'm (almost) completely agree with you. In my case the crate contains both library and binary. Library has no limitations for enabled features: you can enable all, none or some of them in any combinations. All is up to you. The mutual-exclusive restriction is presented only in binary crate. In the vast majority of use cases, the user will need exactly one database and exactly one broker to use and in order to simplify running the service I made a binary. In case when the user needs to adjust the logic or use multiple databases/brokers, they're free to use the library and make their own binary which will solve his specific problem. To make things clearer, here's the source code, you can find that lib has no restrictions or limitation but only main.rs checks features mutual-exclusivity.
Why "almost" completely agree with you. There are still specific cases when you may need mutually exclusive features even in libraries. One of such examples is rustls crate, but they solve it in a different way. The idea is that you pick one of two cryptography providers: aws-lc-rs or ring. You still can have both features enabled (which is useful when it comes to dependencies in other libs) but in application (binary) in case if you have both of them enabled, you have to specify which one to use, here's their docs about it.
I guess I must misunderstand features. I thought enabling a feature such as a db dependency should only allow configuring it explicitly -- always. Otherwise, when features are enabled by dependencies (in more complex situations), it is impossible to avoid conflicts between features that are mutually exclusive. Is this no longer true? I'm referring to the case where a dependency also uses the crate with the features.