|
| 1 | +- Feature Name: `feature-unification` |
| 2 | +- Start Date: 2024-09-11 |
| 3 | +- RFC PR: [rust-lang/rfcs#3692](https://github.com/rust-lang/rfcs/pull/3692) |
| 4 | +- Tracking Issue: [rust-lang/cargo#14774](https://github.com/rust-lang/cargo/issues/14774) |
| 5 | + |
| 6 | +# Summary |
| 7 | +[summary]: #summary |
| 8 | + |
| 9 | +Give users control over the feature unification that happens based on the packages they select. |
| 10 | +- A way for `cargo check -p foo -p bar` to build like `cargo check -p foo && cargo check -p bar` |
| 11 | +- A way for `cargo check -p foo` to build `foo` as if `cargo check --workspace` was used |
| 12 | + |
| 13 | +Related issues: |
| 14 | +- [#5210: Resolve feature and optional dependencies for workspace as a whole](https://github.com/rust-lang/cargo/issues/5210) |
| 15 | +- [#4463: Feature selection in workspace depends on the set of packages compiled](https://github.com/rust-lang/cargo/issues/4463) |
| 16 | +- [#8157: --bin B resolves features differently than -p B in a workspace](https://github.com/rust-lang/cargo/issues/8157) |
| 17 | +- [#13844: The cargo build --bins re-builds binaries again after cargo build --all-targets](https://github.com/rust-lang/cargo/issues/13844) |
| 18 | + |
| 19 | +# Motivation |
| 20 | +[motivation]: #motivation |
| 21 | + |
| 22 | +Today, when Cargo is building, features in dependencies are enabled based on the set of packages selected to build. |
| 23 | +This is an attempt to balance |
| 24 | +- Build speed: we should reuse builds between packages within the same invocation |
| 25 | +- Ability to verify features for a given package |
| 26 | + |
| 27 | +This isn't always ideal. |
| 28 | + |
| 29 | +If a user is building an application, they may be jumping around the application's components which are packages within the workspace. |
| 30 | +The final artifact is the same but Cargo will select different features depending on which package they are currently building, |
| 31 | +causing build churn for the same set of dependencies that, in the end, will only be used with the same set of features. |
| 32 | +The "cargo-workspace-hack" is a pattern that has existed for years |
| 33 | +(e.g. [`rustc-workspace-hack`](https://crates.io/crates/rustc-workspace-hack)) |
| 34 | +where users have all workspace members that depend on a generated package that depends on direct-dependencies in the workspace along with their features. |
| 35 | +Tools like [`cargo-hakari`](https://crates.io/crates/cargo-hakari) automate this process. |
| 36 | +To allow others to pull in a package depending on a workspace-hack package as a git dependency, you then need to publish the workspace-hack as an empty package with no dependencies |
| 37 | +and then locally patch in the real instance of it. |
| 38 | + |
| 39 | +This also makes testing of features more difficult because a user can't just run `cargo check --workspace` to verify that the correct set of features are enabled. |
| 40 | +This has led to the rise of tools like [cargo-hack](https://crates.io/crates/cargo-hack) which de-unify packages. |
| 41 | + |
| 42 | +# Guide-level explanation |
| 43 | +[guide-level-explanation]: #guide-level-explanation |
| 44 | + |
| 45 | + |
| 46 | +# Reference-level explanation |
| 47 | +[reference-level-explanation]: #reference-level-explanation |
| 48 | + |
| 49 | +We'll add two new modes to feature unification: |
| 50 | + |
| 51 | +**Unify features across the workspace, independent of the selected packages** |
| 52 | + |
| 53 | +This would be built-in support for "cargo-workspace-hack". |
| 54 | + |
| 55 | +This would require effectively changing from |
| 56 | +1. Resolve dependencies |
| 57 | +2. Filter dependencies down for current build-target and selected packages |
| 58 | +3. Resolve features |
| 59 | + |
| 60 | +To |
| 61 | +1. Resolve dependencies |
| 62 | +2. Filter dependencies down for current build-target |
| 63 | +3. Resolve features |
| 64 | +4. Filter for selected packages |
| 65 | + |
| 66 | +The same result can be achieved with `cargo check --workspace`, |
| 67 | +but with fewer packages built. |
| 68 | +Therefore, no fundamentally new "mode" is being introduced. |
| 69 | + |
| 70 | +**Features will be evaluated for each package in isolation** |
| 71 | + |
| 72 | +This will require building duplicate copies of build units when there are disjoint sets of features. |
| 73 | + |
| 74 | +For example, this could be implemented as either |
| 75 | +- Loop over the packages, resolving, and then run a build plan for that package |
| 76 | +- Resolve for each package and generate everything into the same build plan |
| 77 | + |
| 78 | +This is not prescriptive of the implementation but to illustrate what the feature does. |
| 79 | +The initial implementation may be sub-optimal. |
| 80 | +Likely, the implementation could be improved over time. |
| 81 | + |
| 82 | +The same result can be achieved with `cargo check -p foo && cargo check -p bar`, |
| 83 | +but with the potential for optimizing the build further. |
| 84 | +Therefore, no fundamentally new "mode" is being introduced. |
| 85 | + |
| 86 | +**Note:** these features do not need to be stabilized together. |
| 87 | + |
| 88 | +##### `resolver.feature-unification` |
| 89 | + |
| 90 | +*(update to [Configuration](https://doc.rust-lang.org/cargo/reference/config.html))* |
| 91 | + |
| 92 | +* Type: string |
| 93 | +* Default: "selected" |
| 94 | +* Environment: `CARGO_RESOLVER_FEATURE_UNIFICATION` |
| 95 | + |
| 96 | +Specify which packages participate in [feature unification](https://doc.rust-lang.org/cargo/reference/features.html#feature-unification). |
| 97 | + |
| 98 | +* `selected`: merge dependency features from all package specified for the current build |
| 99 | +* `workspace`: merge dependency features across all workspace members, regardless of which packages are specified for the current build |
| 100 | +* `package`: dependency features are only considered on a package-by-package basis, preferring duplicate builds of dependencies when different sets of feature are activated by the packages. |
| 101 | + |
| 102 | +# Drawbacks |
| 103 | +[drawbacks]: #drawbacks |
| 104 | + |
| 105 | +This increases entropy within Cargo and the universe at large. |
| 106 | + |
| 107 | +As `workspace` unifcation builds dependencies the same way as `--workspace`, it has the same drawbacks as `--workspace`, including |
| 108 | +- If a build would fail with `--workspace`, then it will fail with `workspace` unification as well. |
| 109 | + - For example, if two packages in a workspace enable mutually exclusive features, builds will fail with both `--workspace` and `workspace` unification. |
| 110 | + Officially, features are supposed to be additive, making mutually exclusive features officially unsupported. |
| 111 | + Instead, effort should be put towards [official mutually exclusive globals](https://internals.rust-lang.org/t/pre-rfc-mutually-excusive-global-features/19618). |
| 112 | +- If `--workspace` would produce an invalid binary for your requirements, then it will do so with `workspace` unification as well. |
| 113 | + - For example, if you have regular packages and a `no_std` package in the same workspace, the `no_std` package may end up with dependnencies built with `std` features. |
| 114 | + |
| 115 | +# Rationale and alternatives |
| 116 | +[rationale-and-alternatives]: #rationale-and-alternatives |
| 117 | + |
| 118 | +This is done in the config instead of the manifest: |
| 119 | +- As this can change from run to run, this covers more use cases. |
| 120 | +- As this fits easily into the `resolver` table, there is less design work. |
| 121 | + |
| 122 | +We could extend this with configuration to exclude packages for the various use cases mentioned. |
| 123 | +Supporting excludes adds environment/project configuration complexity as well as implementation complexity. |
| 124 | + |
| 125 | +This field will not apply to `cargo install` to match the behavior of `resolver.incompatible-rust-versions`. |
| 126 | + |
| 127 | +The `workspace` setting breaks down if there are more than one "application" in |
| 128 | +a workspace, particularly if there are shared dependencies with intentionally |
| 129 | +disjoint feature sets. |
| 130 | +What this use case is really modeling is being able to tell Cargo "build package X as if its a dependency of package Y". |
| 131 | +There are many similar use cases to this (e.g. [cargo#2644](https://github.com/rust-lang/cargo/issues/2644), [cargo#14434](https://github.com/rust-lang/cargo/issues/14434)). |
| 132 | +While a solution that targeted this higher-level need would cover more uses cases, |
| 133 | +there is a lot more work to do within the design space and it could end up being more unwieldy. |
| 134 | +The solution offered in this RFC is simple in that it is just a re-framing of what already happens on the command line. |
| 135 | + |
| 136 | +# Prior art |
| 137 | +[prior-art]: #prior-art |
| 138 | + |
| 139 | +[`cargo-hakari`](https://crates.io/crates/cargo-hakari) is a "cargo-workspace-hack" generator that builds a graph off of `cargo metadata` and re-implements feature unification. |
| 140 | + |
| 141 | +[cargo-hack](https://crates.io/crates/cargo-hack) can run each selected package in a separate `cargo` invocation to prevent unification. |
| 142 | + |
| 143 | +# Unresolved questions |
| 144 | +[unresolved-questions]: #unresolved-questions |
| 145 | + |
| 146 | +- How to name the config field to not block the future possibilities |
| 147 | + |
| 148 | +# Future possibilities |
| 149 | +[future-possibilities]: #future-possibilities |
| 150 | + |
| 151 | +### Support in manifests |
| 152 | + |
| 153 | +Add a related field to manifests that the config can override. |
| 154 | + |
| 155 | +### Dependency version unification |
| 156 | + |
| 157 | +Unlike feature unification, dependency versions are always unified across the |
| 158 | +entire workspace, making `Cargo.lock` the same regardless of which package you |
| 159 | +select or how you build. |
| 160 | + |
| 161 | +This can mask minimal-version bugs. |
| 162 | +If a version-req is lower than it needs, `-Zminimal-versions` won't resolve down to that to show the problem if another version req in the workspace is higher. |
| 163 | +We have `-Zdirect-minimal-versions` which will error if workspace members do not have the lowest version reqs of all of the workspace but that is brittle. |
| 164 | + |
| 165 | +If you have a workspace with multiple MSRVs, you can't verify your MSRV if you |
| 166 | +set a high-MSRV package's version req for a dependency that invalidates the |
| 167 | +MSRV-requirements of a low-MSRV package. |
| 168 | + |
| 169 | +We could offer an opt-in to per-package `Cargo.lock` files. For builds, this |
| 170 | +could be implemented similar to `resolver.feature-unification = "package"`. |
| 171 | + |
| 172 | +This could run into problems with |
| 173 | +- `cargo update` being workspace-focused |
| 174 | +- third-party updating tools |
| 175 | + |
| 176 | +As for the MSRV-case, this would only help if you develop with the latest |
| 177 | +versions locally and then have a job that resolves down to your MSRVs. |
| 178 | + |
| 179 | +### Unify features in other settings |
| 180 | + |
| 181 | +[`workspace.resolver = "2"`](https://doc.rust-lang.org/cargo/reference/resolver.html#features) removed unification from the following scenarios |
| 182 | +- Cross-platform build-target unification |
| 183 | +- `build-dependencies` / `dependencies` unification |
| 184 | +- `dev-dependencies` / `dependencies` unification unless a dev build-target is enabled |
| 185 | + |
| 186 | +Depending on how we design this, the solution might be good enough to |
| 187 | +re-evaluate |
| 188 | +[build-target features](https://github.com/rust-lang/rfcs/pull/3374) as we |
| 189 | +could offer a way for users to opt-out of build-target unification. |
| 190 | + |
| 191 | +Like with `resolver.incompatible-rust-version`, a solution for this would override the defaults of `workspace.resolver`. |
| 192 | + |
| 193 | +`cargo hakari` gives control over `build-dependencies` / `dependencies` unification with |
| 194 | +[`unify-target-host`](https://docs.rs/cargo-hakari/latest/cargo_hakari/config/index.html#unify-target-host): |
| 195 | +- [`none`](https://docs.rs/hakari/0.17.4/hakari/enum.UnifyTargetHost.html#variant.None): Perform no unification across the target and host feature sets. |
| 196 | + - The same as `resolver = "2"` |
| 197 | +- [`unify-if-both`](https://docs.rs/hakari/0.17.4/hakari/enum.UnifyTargetHost.html#variant.UnifyIfBoth): Perform unification across target and host feature sets, but only if a dependency is built on both the platform-target and the host. |
| 198 | +- [`replicate-target-on-host`](https://docs.rs/hakari/0.17.4/hakari/enum.UnifyTargetHost.html#variant.ReplicateTargetOnHost): Perform unification across platform-target and host feature sets, and also replicate all target-only lines to the host. |
| 199 | +- [`auto`](https://docs.rs/hakari/0.17.4/hakari/enum.UnifyTargetHost.html#variant.Auto) (default): select `replicate-target-on-host` if a workspace member may be built for the host (used as a proc-macro or build-dependency) |
| 200 | + |
| 201 | +`unify-target-host` might be somewhat related to [`-Ztarget-applies-to-host`](https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#target-applies-to-host) |
| 202 | + |
| 203 | +For Oxide `unify-target-host` reduced build units from 1900 to 1500, dramatically improving compile times, see https://github.com/oxidecomputer/omicron/pull/4535 |
| 204 | +If integrated into cargo, there would no longer be a use case for the current maintainer of `cargo-hakari` to continue maintenance. |
| 205 | + |
| 206 | +If we supported `dev-dependencies` / `dependencies` like `resolver = "1"`, it |
| 207 | +could help with cases like `cargo miri` where through `dev-dependencies` a |
| 208 | +`libc` feature is enabled. preventing reuse of builds between `cargo build` and |
| 209 | +`cargo test` for local development. |
| 210 | + |
| 211 | +In helping this case, we should make clear that this can also break people |
| 212 | +- `fail` injects failures into your production code, only wanting it enabled for tests |
| 213 | +- Tests generally enabled `std` on dependencies for `no_std` packages |
| 214 | +- We were told of use cases around private keys where `Clone` is only provided when testing but not for production to help catch the leaking of secrets |
0 commit comments