Skip to content

Commit 8426158

Browse files
authored
Merge pull request #3692 from epage/feature-unification
RFC: Give users control over feature unification
2 parents 6c1f6dc + 203e6fc commit 8426158

File tree

1 file changed

+214
-0
lines changed

1 file changed

+214
-0
lines changed

text/3692-feature-unification.md

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
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

Comments
 (0)