Feature unification opt-out in workspace crate

Motivation

Let's say we have a workspace with two crates, one compiled with the default target (x86_64-unknown-linux-gnu) and the second one for SGX (x86_64-fortanix-unknown-sgx). Both depend on the same dependency (in my case it was subxt), but the first requires a feature, while the second one must not have it, otherwise it won't compile for its target. Feature unification resulted in both crates trying to use this feature, even though they're using different targets (also see Cargo Workspace and the Feature Unification Pitfall). As shown in that link this can be solved by specifying the crate/package when building: cargo build -p my-package --bin my-bin --target x86_64-fortanix-unknown-sgx, which apparently does not do this feature unification with other packages from the workspace. This is a good and working solution, but it would be nice if we could tell Cargo to do this automatically with a line in Cargo.toml (see below).

Second Situation: We have one workspace with two crates that do not depend on each other but both may use the same (external to the workspace) dependency. Due to the feature unification the binary of one crate may change if a feature is added to the other crate. While this speeds up compilation (due to not needing to compile the dependency twice), it isn't really desirable when reproducibility is required or when the binary should not change unless it actually changed. This can also be solved by specifying -p or not using a workspace.

Description

It would be nice to have an option in a crates Cargo.toml to opt-out of feature unification with other crates in the workspace (working similarly to specifying -p when building). Feature unification would still exist of course (inside the crate and its dependencies), but changing other crates in the workspace (or even adding another crate) will not change the compilation of this crate or the resulting binary/library (unless a dependency in the workspace Cargo.toml itself is changed of course).

Example how this could look

[package]
name = "my-package"
version = "0.1.0"
edition = "2021"
isolated = true

Alternative

For more flexibility (or in addition to that) we could tell cargo for each dependency if feature unification with the workspace is allowed. While this is likely more complex than effectively doing the same as -p my-package, it would allow re-using other dependencies where the feature unification is not critical. Personally I think doing it on the entire crate is more useful (as this one doesn't solve the second entry in the motivation), but having both available could be useful (the one at the crate level effectively adding this to all dependency entries).

[dependencies]
subxt = { version = "0.34.0", default-features = false, isolated = true }

What do you think about this? I think it'd be worth having, even if we can achieve it already by specifying -p my-package when building, because it clearly marks that this is desired/needed, whereas using -p my-package for the purpose of not unifying features with other crates in the workspace is not that well documented/known.

For reference rust-lang/cargo#4463 is the issue for feature unification.

For the first case, how are you building for two different targets without already running the command you specified? Are you using the unstable package.forced-target? If so, that might be good feedback on the tracking issue. If not, it seems like you could move the activation of that feature to a target.cfg().dependencies so it only activates when that one target-platform is built.

For the second,

As for the article you linked, they use cfg dependencies and resolver = "2" which has been around for quite a while now. There is also Pre-RFC discussion for a design for mutually exclusive, global features.

For the first case, how are you building for two different targets without already running the command you specified?

By specifying the binary (which does not have a name conflict, I guess I should've mentioned that the crates contain both a library and a binary, I've just left out the -p because it wasn't needed for specifying the bin I want to compile):

cargo build --bin name-of-binary-in-crate-a
cargo build --bin name-of-binary-in-crate-b --target x86_64-fortanix-unknown-sgx

I think this isn't possible with --lib (as you said), but it works with binaries.

If not, it seems like you could move the activation of that feature to a target.cfg().dependencies so it only activates when that one target-platform is built.

I thought about doing that, but I don't think it's a good solution (though it should work), as it requires changing the Cargo.toml of crate A to get crate B to compile, even though B does not depend on A.

This starts to feel like "work associated with final artifacts" which is bigger than this and I suspect any non-trivial final artifact needs a build orchestrator around cargo.

True, I am currently using a makefile for that, which is why having to specify -p my-package isn't a big issue either. Though it was really confusing when Cargo attempted to unify features across different targets.

Sounds like you are running into rust-lang/cargo#8157.

1 Like