Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Give users control over feature unification #3692

Open
wants to merge 35 commits into
base: master
Choose a base branch
from

Conversation

epage
Copy link
Contributor

@epage epage commented Sep 11, 2024

This adds resolver.feature-unification to .cargo/config.toml to allow workspace unfication (cargo-workspace-hack) or per-package unification (cargo hack).

Rendered

@epage epage added the T-cargo Relevant to the Cargo team, which will review and decide on the RFC. label Sep 11, 2024
@joshtriplett
Copy link
Member

Thank you so much for turning the RustConf discussions into RFC prose so quickly!

@joshtriplett
Copy link
Member

I'm going to go ahead and start the asynchronous process of checking for consensus or concerns on this idea. People should feel free to read and consider at their leisure, and we can always discuss it further; just want to give a place for folks who have had a chance to review it to register either consensus or concerns or both.

@rfcbot merge

@rfcbot
Copy link
Collaborator

rfcbot commented Sep 12, 2024

Team member @joshtriplett has proposed to merge this. The next step is review by the rest of the tagged team members:

No concerns currently listed.

Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

See this document for info about what commands tagged team members can give me.

@rfcbot rfcbot added proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. labels Sep 12, 2024
epage and others added 2 commits September 11, 2024 22:35
Co-authored-by: Josh Triplett <[email protected]>
Co-authored-by: Josh Triplett <[email protected]>
text/3692-feature-unification.md Outdated Show resolved Hide resolved
text/3692-feature-unification.md Outdated Show resolved Hide resolved
3. Resolve features
4. Filter for selected packages

**Features will be evaluated for each package in isolation**
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main situation I encounter this with is a workspace that has a single package, but the different cargo targets have different dependencies and that affects feature resolution in a way that cargo build vs cargo test leads to rebuilds. Will this RFC help there? If yes, then the text should be clarified, because it seems to talk entirely about packages but not about targets within a package. If not -- what would it take to help in that situation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for calling this out! I had considered this but forgot it and didn't want to hold it up until I could remember.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sunshowers says cargo-hakari will unify normal and dev-dependencies. Platforms are not unified.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sunshowers says that is also unifies the features that workspace member features enable

Copy link
Contributor Author

@epage epage Sep 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some cases with unifying dev-dependencies include

  • fail being used for dev-dependencies but not production
  • Tests enabling std but not wanting them for normal build
  • Some use case related to private keys where the impl Clone is only provided on debug builds and not release builds

There are likely other cases of this same sort of problem

Copy link
Member

@RalfJung RalfJung Sep 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Miri's case it's dev-dependencies enabling different features for libc or some other fundamental crate, which means that even after cargo test, doing cargo build does an almost complete re-build of Miri -- that's waiting time I'd rather avoid. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've summarized this under "Future possibilities"

@weiznich
Copy link
Contributor

weiznich commented Sep 12, 2024

I'm not a team member of any of the relevant teams here at all, but I nevertheless want to raise some concerns. The proposed text is very light on details what should happen regarding feature unification between host and target crates, especially for shared dependencies of them. The last time the cargo team performed changed on how feature unification worked with the 2021 edition they broke existing crates in an incompatible way. (The argumentation for that essentially was: It's required for other use-cases to work, which is really not that great if you part of the number of crates that broke due to this.)
I raised similar concerns back then a bit later in the process. It was ignored back then, so I figured that it might be worth to raise this just in the beginning this time. I would be very grateful if some of the official team members could list this as concern in the FCP comment.

As additional observation: The resolver = "2" feature back then was already forced onto upstream crates, this now introduces another potentially equally breaking feature in another location again controlled by downstream crates. I'm not 100% sure if it's really a good idea to have controls for the resolver behavior in various different places and also I'm still not sure if it's the correct solution to introduce these potential upstream package breaking changes at all.

That written: It would be surely helpful to have some sort of control over how feature unification works, but maybe that should not be done at a workspace level, but rather be up to control of who puts there the actual dependency? That would allow me as a crate author of some inter-dependent crates to correctly control how cargo treats features for these interconnected crates.

@weiznich
Copy link
Contributor

weiznich commented Oct 2, 2024

Thanks for your responses.

Medium or large projects that aren't using hakari or some kind of similar workspace-hack unification are wasting massive amounts of developer time and resources. (Hakari makes common build workflows up to twice as fast! We usually invest large amounts of effort towards even a 5% improvement in compile times, and there's a 2x improvement just sitting there waiting to happen.)

If that's so widespread it shouldn't be too hard to provide some exact numbers that say: "We measure a speedup of x in this cases. You can reproduce this by doing y." I do not doubt that this is the case, it would just make the discussion much more transparent than: Someone says this might speedup xyz.

Having run hakari in production for years across a variety of setups (including Diesel within Omicron), I can say with some confidence that feature unification just hasn't caused issues in practice. Hakari does have a config mechanism to exclude particular crates from unification, but I haven't had to reach for that in a while.

I wouldn't go as far as confidently stating that this will not cause issues in practice, as at least I see real uses cases that might have a setup as shown in the "broken" example, although I agree that these are likely not common. That's the reason why I repeatably wrote that I don't think the RFC should be cancelled because of that, it only needs to make sure that the communication around the feature is careful. In my opinion it needs to at least communicate:

  • That it is possible that things break if you change this settings
  • Ideally cargo needs to detect that this setting is not the default and put up a rather explicit message that this is likely the issue is caused by this setting and that the user should try it without that setting. Given that cargo has access to the dependency tree it can even try to detect setups like the example. I would expect that the majority of such issues is around feature unification between shared host/target dependencies. It should be possible to just detect this case.
  • Maybe consider not promoting this as the "ultimate" performance booster for compile times, as that might result in people coming to crates that cannot support this and complain about that as it "costs" performance. (That wouldn't be necessary if cargo is able to reliable detect the problem on it's own)
  • Possibly it also helps to change the scope of the RFC and directly address the outlined issue as part of the RFC. Yes that will require more work, but it would result in a better feature.

I appreciate this perspective and I'd be hesitant to support this change if the improvements are minor. But they're not minor -- I believe that feature unification might be the single most impactful improvement to Rust compile times since 1.0 -- and I think it's worth making the ecosystem adapt to it.

Again: On a fundamental level I need to disagree here. A breaking change is a breaking change, no matter how useful it is or not. If you believe that it is really that useful and it is worth a breaking change you should be at least honest and say: Let's bump the major version. My main point here is: It's up to the RFC author to provide reasonable evidence that this is not a breaking change. Given the provided example and the communication from the author I must say that I cannot see that evidence yet.
On a practical level I agree in so far that this change (given proper communication/documentation/implementation) might be a worthwhile improvement and not a breaking change.

It sounds like your concern is not a concern about the merit of this change in isolation, but assuming we do want this change to go through whether that should be blocked on some improvement to rust-lang/cargo#14415 or not.

Yes, my concern around this is not only around this particular change. As written before: I'm not requesting that this isn't accepted. My main concern here is around the ability of the cargo team to decide whether or not something is a breaking change, given the history of rust-lang/cargo#14415 (which was a breaking change, with a very similar scope). Given how the team ignored my concerns back then I just want to make sure that this time this issue:

  • Is registered as official concern
  • Discussed with proper arguments
  • Maybe some adjustments are made to make it clearer that changing this setting is entirely up to the user that changes it and that there are situations where certain settings may break things.
  • Ideally: Concentrating on fixing regressions before introducing new features

@pacak
Copy link

pacak commented Oct 2, 2024

If that's so widespread it shouldn't be too hard to provide some exact numbers that say

Those are figures I got from a sizeable workspace with no hack, a different hack manager and something similar to hackari:

https://crates.io/crates/cargo-hackerman#user-content-hackerman-vs-no-hack-vs-single-hack-crate

Example is somewhat artificial, but it shows 2x difference in total compilation time because dependencies are recompiled multiple times with different sets of features.

Lack of feature unification hurts workspaces that use nix to compile dependencies even worse - I saw cases where a single crate was compiled 20-25 times, with total compilation time increasing from 10-15 minutes to 1-2 hours on a CI machine. No exact measurements, but the order of magnitude is correct.

@sunshowers
Copy link

If that's so widespread it shouldn't be too hard to provide some exact numbers that say: "We measure a speedup of x in this cases. You can reproduce this by doing y." I do not doubt that this is the case, it would just make the discussion much more transparent than: Someone says this might speedup xyz.

It will take a bit of time to measure this, but the speedup is absolutely real and is certainly not a "might". But before I invest my time into this, I would like an assurance from you that you will respect the effort that I put in.

On a fundamental level I need to disagree here. A breaking change is a breaking change, no matter how useful it is or not.

No, this is not a breaking change. The sense of breaking that matters here is a strict sense, and this is very clearly non-breaking. Existing workspaces will continue to compile with no changes—it is explicitly opt-in.

Since it is not a breaking change in the strict sense, it can be evaluated as an engineering decision based on the merits. This change would make life better for the vast majority of people who opt into it, but unfortunately result in more work for some. That is just part of life as an engineer.

@weiznich
Copy link
Contributor

weiznich commented Oct 3, 2024

It will take a bit of time to measure this, but the speedup is absolutely real and is certainly not a "might". But before I invest my time into this, I would like an assurance from you that you will respect the effort that I put in.

You are free to do whatever you want. After all I'm not a member of the cargo team, so strictly speaking my comments here are "not relevant". I merely point out that the RFC only mentions that there are speedups, but does not provide numbers or evidence for that. Coming from a scientific background that's just bad practice as you "claim" something without providing at least some evidence.

Please note: I do not doubt the speedups, it's just bad practice to make such claims without having numbers.

No, this is not a breaking change. The sense of breaking that matters here is a strict sense, and this is very clearly non-breaking. Existing workspaces will continue to compile with no changes—it is explicitly opt-in.

I'm sorry, but I need to disagree here again. You are in so far correct that this is no breaking change for the workspace owner itself as they can opt into this feature. The matter is not so clear for any crate that is a dependency of said workspace, as their authors do not opt into something as far as I can read out of the RFC and their code can break due to this setting. Given that I find it not useful that you (and others) claim this is clearly not breaking as you opt in to this behavior. I might be wrong here, but in this case again: Please point out for the broken example workspace described above: At which point I as diesel author opt into this behavior? If you cannot do that: Please point to a RFC or other official team decision that states it's OK to break code of dependencies.

Now you might still argue that this is not a breaking change for other reasons, e.g. because it's already broken in different, but similar situations. That's a different argument to made. I honestly do not see a complete evaluation of that argument yet.

Since it is not a breaking change in the strict sense, it can be evaluated as an engineering decision based on the merits. This change would make life better for the vast majority of people who opt into it, but unfortunately result in more work for some. That is just part of life as an engineer.

Again it seems like you just claim things here by saying it's more work for other people (in this case me). Let's put it the other way around: What exactly can I as diesel author do to fix this situation so that this breakage does not happen? As outlined in rust-lang/cargo#14415 it's not possible to fix that in diesel due to fundamental limitations in how feature unification currently works. If you disagree with that I'm looking forward to see your solution.
From that point of view you do not only expect me to spend more work for you, but you literally request me to do something that's not possible for all I know. Again that does not feel like a honest argument.

Finally to take up the argument: The broken example is already broken if you compile both crates from the workspace root. To play the devils advocate here: If it's already broken from there, that means you don't need to have the suggested feature as you already can have the same behavior by issuing the right command from the right place. At that point: What's the reason for having a setting for this at all? After all you can get the "same" speedup today without that setting (and without any helper tool), by just carefully writing the build/test command.


On a more fundamental side: I spend some more time thinking about whether or not that could be useful for my use cases. While doing that I noticed another rather fundamental point that I do not see addressed or discussed yet. I wouldn't call this necessarily a blocker, but I personally would see that as serve limitation of the proposed feature.

For our internal code we have a large workspace that includes client and server code, to share some types between both. The server code depends on diesel with various database backends enabled, which in turn pulls in various native libraries. The client code also uses diesel, but for different use cases and only with the sqlite backend enabled. Now the big argument for this feature is: It speeds up compile times, which I do not question in its own. You cite CI run times here as critical point. Now consider the described workspace, for test runs we do care about compile times as we want to have test results as quickly as possible, so you would enable the resolver.feature-unification = "workspace" flag for that. For release builds things are completely different: We do care much more about binary size there, so we want to build the client dependencies with a minimal set of features, especially to not include the native dependencies pulled in by diesel on server side, so we would want to enable resolver.feature-unification = "package" there. I would expect that there are quite a large number of workspaces out there with a similar setup (having client + server code in the same workspace + that care about different things for different compilation modes). That makes me wonder whether it's really a good idea to have this a workspace level setting or if this feature should be a command line flag instead. The later would make it much easier to choose different behavior for different situations.

@epage
Copy link
Contributor Author

epage commented Oct 3, 2024

This is RFC is not for workspace-level config (Cargo.toml [workspace]) but environment/transient config (.cargo/config.toml) which includes CLI support (--config) and environment variable support. Putting it in manifests is a future possibility.

@eric-seppanen
Copy link

What's the reason for having a setting for this at all? After all you can get the "same" speedup today without that setting (and without any helper tool), by just carefully writing the build/test command.

This relevant text in the RFC is:

If a user is building an application, they may be jumping around the application's components which are packages within the workspace. The final artifact is the same but Cargo will select different features depending on which package they are currently building, causing build churn for the same set of dependencies that, in the end, will only be used with the same set of features. (...)

In case that wasn't clear, it's describing a situation where a workspace contains a large number of crates, that aren't meant to be published on their own, but are sub-components of a big monolithic application. During development, it's fairly common for an individual developer to be editing only a small number of crates, and that developer may hop between workspace crate directories running e.g. cargo check. Today, switching between crate directories (or running -p some-component) triggers a unique feature unification, which in large projects (thousands of dependencies) can mean minutes of waiting. This is what the RFC means by "build churn". "Just build the whole workspace" doesn't resolve the problem because it also involves a lot of waiting, depending on where in the dependency graph changes were made.

This situation has led to various "hacks" (see the RFC for links) that are a bit unsatisfying and consume developer time that might be put to better use.

There is no setting today, and no carefully written command, that allows this developer to say "build just crate X, while using the feature unification that would have been used, if I were building the whole application".

Also note that this situation doesn't appear in other build systems, which leads to pressure to move away from Cargo toward systems that always build components the same way.

@epage
Copy link
Contributor Author

epage commented Oct 3, 2024

In case that wasn't clear, it's describing a situation where a workspace contains a large number of crates, that aren't meant to be published on their own, but are sub-components of a big monolithic application.

Really, the user is asking "build package X as if its dependency of Y". I've added this to the Alternatives and why we aren't proposing that at this time and instead going with this.

@eric-seppanen
Copy link

Really, the user is asking "build package X as if its dependency of Y".

I don't understand. What is Y in this description?

In my workspace there are a number of top-level packages/binaries that collectively contribute to the dependency feature set, so I don't really want to name one of them specifically.

@eric-seppanen
Copy link

I'm also not sure what is meant by "The workspace setting breaks down if there are more than one "application" in a workspace".

That could describe my workspace: lots of output binaries, though I guess if you squint you could just call them one application collectively. Perhaps the language could be adjusted to say something like "the workspace setting may not be compatible with a workspace containing multiple applications, if those applications require require different dependency feature sets"?

@epage
Copy link
Contributor Author

epage commented Oct 3, 2024

I'm also not sure what is meant by "The workspace setting breaks down if there are more than one "application" in a workspace".

That could describe my workspace: lots of output binaries, though I guess if you squint you could just call them one application collectively. Perhaps the language could be adjusted to say something like "the workspace setting may not be compatible with a workspace containing multiple applications, if those applications require require different dependency feature sets"?

You left off an important qualifier

particularly if there are shared dependencies with intentionally disjoint feature sets.

This is talking to the use case of "move to a subcomponent and build" and not "let me optimize my build and force two applications to the same feature set.

@eric-seppanen
Copy link

I think that sentence could be improved then. It reads to me like "This setting is broken if you have more than one application. It's more broken if you need disjoint feature sets".

@epage
Copy link
Contributor Author

epage commented Oct 3, 2024

Really, the user is asking "build package X as if its dependency of Y".

I don't understand. What is Y in this description?

Y would most likely be an application.

In my workspace there are a number of top-level packages/binaries that collectively contribute to the dependency feature set, so I don't really want to name one of them specifically.

Under the "build package X as if its a dependency of Y", that would be "build package X as if its a dependency of Y, Z".

This is the more generalized form that more closely maps to the problem and gives people the flexibility to adopt to their specific needs. I was thinking about this as I considered ways to give people flexibility within the RFC's design, like defining package groups or having individual packages opt-in or out of workspace unification. Going down those routes is complicated because its trying to generalize but in a way that doesn't map the users actual problem.

Keep in mind, I put this up as an alternative. I just wanted to highlight what problem we are modeling.

@epage
Copy link
Contributor Author

epage commented Oct 3, 2024

I think that sentence could be improved then. It reads to me like "This setting is broken if you have more than one application. It's more broken if you need disjoint feature sets".

In the RFC, the listed motivation is not to artificially unifying features when building applications for the sake of build optimization but of keeping the same build configuration as you build different parts of the same application. From that perspective, it is breaking down because applications are affecting each other when before they wouldn't. For example, your build could break by moving an application to another workspace because you accidentally relied on the features it activated. You could get unexpected behavior from features that don't show up in the Cargo.toml files. Now, this can become a showstopper if they are intentionally disjoint.

While this RFC doesn't preclude using this to unify unrelated feature graphs, that is really a topic of the RFC for the future possibilities when we discuss unifying host/target, normal/dev dependencies, etc. As the updated text calls out, all we are doing is allowing users to do what they can today but with less overhead.

@sunshowers
Copy link

sunshowers commented Oct 3, 2024

I'm sorry, but I need to disagree here again. You are in so far correct that this is no breaking change for the workspace owner itself as they can opt into this feature.

This is not a breaking change, period. Cargo adding an opt-in feature that is incompatible with some dependencies is not a breaking change by any reasonable definition.

The matter is not so clear for any crate that is a dependency of said workspace

The act of using a dependency is a decision made by the maintainers of a workspace. If a maintainer uses a dependency it is their problem to ensure compatibility. The buck stops with the maintainer.

as their authors do not opt into something as far as I can read out of the RFC and their code can break due to this setting.

Maintainers can do all sorts of things that are not compatible with their dependencies. Trivially, they can use a dependency's API incorrectly. More substantially, they can use a [patch] directive to replace a transitive dependency in an unexpected way. They can attempt to perform a build on a platform not supported by a dependency, or with a version of Rust that's below a dependency's MSRV, and so on.

Similarly, they can opt into this feature that may cause their use of a particular dependency to fail. Then the onus is on them to address it.

Given that I find it not useful that you (and others) claim this is clearly not breaking as you opt in to this behavior. I might be wrong here, but in this case again: Please point out for the broken example workspace described above: At which point I as diesel author opt into this behavior?

This is controlled at the top level, not within dependencies, so there's no behavior for a dependency to opt into. If a workspace opts into this, then ensuring that cargo build still works is their problem.

If you cannot do that: Please point to a RFC or other official team decision that states it's OK to break code of dependencies.

This opt-in change does not break code of dependencies, any more than Cargo adding support for a target platform which is incompatible with that dependency would.


In your case, given that the issue occurs with

[dependencies]
diesel = { version = "=2.2.2", features = ["postgres"] }

[build-dependencies]
diesel = { version = "=2.2.1", features = ["sqlite"] }

You can tell your users to unify their feature sets manually. Does this not solve the problem?

[dependencies]
diesel = { version = "=2.2.2", features = ["postgres", "sqlite"] }

[build-dependencies]
diesel = { version = "=2.2.1", features = ["postgres", "sqlite"] }

One of the future work items in this RFC (and something supported by hakari today) is about automatically performing target/host feature set unification (essentially undoing that component of resolver v2) which would also address your issue.

After all you can get the "same" speedup today without that setting (and without any helper tool), by just carefully writing the build/test command.

No, you cannot -- if you're in the middle of a refactor, you'll be forced to get the entire workspace to compile rather than the component you're working on.

On a more fundamental side: I spend some more time thinking about whether or not that could be useful for my use cases. While doing that I noticed another rather fundamental point that I do not see addressed or discussed yet. I wouldn't call this necessarily a blocker, but I personally would see that as serve limitation of the proposed feature.

Your concern is valid, but it is handled by passing in the appropriate --config CLI option.

That makes me wonder whether it's really a good idea to have this a workspace level setting or if this feature should be a command line flag instead.

Having to pass in a command-line flag during every invocation of cargo build is not realistic. No one's going to remember to do that. Please consider empathy for the user.

The later would make it much easier to choose different behavior for different situations.

--config addresses that need.

@weiznich
Copy link
Contributor

weiznich commented Oct 4, 2024

I'm sorry, but I'm really disappointed how much you seem to care about my concerns. I do not feel that you even try to response to what I'm asking for. Instead I read the same not meaningful responses again and again. I honestly had no such bad interaction with any of the other rust teams yet.

This is not a breaking change, period. Cargo adding an opt-in feature that is incompatible with some dependencies is not a breaking change by any reasonable definition.

You (and epage) claim that again and again, but as pointed out above my code stops compiling without that I opt into something. How else than "breaking" would you call this? It worked before and stopped working after this change out of the perspective of the dependency maintainer. This RFC changes some fundamental assumptions how feature unification works again and again in incompatible way.

And to repeat that again (as you all seem to ignore that): I can fully understand that there are valid reasons for doing that, but you need to be really aware that this is a complicated topic and "It's opt-in so it's no breaking change" is really not a good argument here. I would really wish you either find a better argument or a better solution for the same impact or at least make clear in the RFC itself that there will be breakage and that the implementation must deal with it. See the suggestions made before if you want concrete solutions.

Maintainers can do all sorts of things that are not compatible with their dependencies. Trivially, they can use a dependency's API incorrectly. More substantially, they can use a [patch] directive to replace a transitive dependency in an unexpected way. They can attempt to perform a build on a platform not supported by a dependency, or with a version of Rust that's below a dependency's MSRV, and so on.

There are still a set of rules that are considered to be supported or not by cargo. For example it is commonly expected that features are additive. Mutually exclusive features are not supported. I cannot remember that this set of rules changed in an incompatible way before (outside the already breaking resolver = "2" change).

More importantly I would argue that these are all different matters:

Trivially, they can use a dependency's API incorrectly

That break their crate, not the dependency. If it can break the dependency I would consider that a bug there.

More substantially, they can use a [patch] directive to replace a transitive dependency in an unexpected way.

That feature exists on it's own. As it was introduced it did not change the behavior of an existing feature like it happens here. Additionally it was not promoted as something that can help speeding up compile times, which in turn makes it more likely that people harass dependency maintainers about this. It's explicit around the feature naming + description and documentation that this feature can break something. I cannot see something like that in this RFC. (Again: That's one of the things I've suggested several times to do to address my concerns!)

They can attempt to perform a build on a platform not supported by a dependency

That's something that was possible since rust 1.0. Importantly it does not introduce a new way to break things!

or with a version of Rust that's below a dependency's MSRV

error: package `diesel v2.2.4` cannot be built because it requires rustc 1.78.0 or newer, while the currently active rustc version is 1.64.0

There is an explicit error/warning here that this is not supported if the dependency correctly declares in MSRV in their Cargo.toml file. Again that gives me as a crate author the possibility to point out: You are on your own here. That's not possible with the proposed feature! Or to word it differently: Give me a similar setting to report incompatible feature unification settings (or any other way to resolve the issue on my end) and I'm happy.

This is controlled at the top level, not within dependencies, so there's no behavior for a dependency to opt into. If a workspace opts into this, then ensuring that cargo build still works is their problem.

That sounds nice in theory, but won't work in practice. The users will still complain in the dependencies repo that something is broken in that crate and request a fix there. Especially for features that are promoted to drastically improve compile times.
Or to word is sarcastically: On the one hand you claim that this is not a breaking change, on the other hand you request from me to deal with the effects of the breakage?

You can tell your users to unify their feature sets manually. Does this not solve the problem?

No it doesn't as the user needs to do something. I'm looking for a solution that can fix the problem in diesel itself. At that point I might as well put into the documentation that this kind of setup is unsupported and they should go to complain to the cargo team if they want to see it supported. I would like to avoid that, but we might up in a situation where this is the best possible solution for me.

One of the future work items in this RFC (and something supported by hakari today) is about automatically performing target/host feature set unification (essentially undoing that component of resolver v2) which would also address your issue.

If it's up on the table to have this at some point I suggest that you include it in the first version of the RFC. After all the RFC process is for defining a meaningful set of features. Given the provided examples, it seems like this extension is necessary for a minimal working feature set. And if that's too much work, well then it seems like the whole feature is too much work.

No, you cannot -- if you're in the middle of a refactor, you'll be forced to get the entire workspace to compile rather than the component you're working on.

That's not true for local workflows as you have an existing target directory there. Yes cargo will build a dependency again if it doesn't have a cache with the same set of feature flags, but that happens exactly once for each feature set. After that the cache still exists, even if you build a crate that uses the same dependency with a different feature set in the mean time.

Your concern is valid, but it is handled by passing in the appropriate --config CLI option.

That's correct, I missed that. Thanks for pointing out.

Having to pass in a command-line flag during every invocation of cargo build is not realistic. No one's going to remember to do that. Please consider empathy for the user.

You surely can use aliases for that so that no one needs to remember the correct flag. You certainly can also ensure that people use the aliases by making it much easier to use them instead of the normal cargo check/build/test commands.


There is no setting today, and no carefully written command, that allows this developer to say "build just crate X, while using the feature unification that would have been used, if I were building the whole application".

I would like to see an example for that, because you always can specify the required features from the other crates, often even via command line flags. If that's not possible it can be made possible by adding the relevant sub dependencies to the crates Cargo.toml which shouldn't change the default build, but would enable specifying features for that subfeature. Sure I agree that this requires some work to get it working, but it's not impossible as claimed.

@hanna-kruppe
Copy link

hanna-kruppe commented Oct 4, 2024

As a long-time user of cargo and more recently (past 8 months) cargo-hakari, I find the ongoing subthread along the lines of "can't users just do X and Y to get the same speedups today" confusing to say the least. Is that a serious position or still the "devil's advocate" thing it apparently started as?

I would consider myself to have above-average experience in dissecting Cargo builds and reshaping the dependency graph to get more favorable behavior. I can understand and act on the long-distance interactions that cargo hakari generate catches automatically, but I can't predict them reliably. If I didn't use cargo-hakari for automatically maintaining "workspace-hack" crates, I'd still be entirely dependent on tools for finding out what has to go into the workspace-hack and what should be taken out because other changes have made it unnecessary. Even if it's technically possible to do all of that manually, it's firmly in the realm of "such a chore that I would never bother". Similarly, my muscle memory of cargo {check,build,test,run} could technically be overridden, but realistically that's not going to happen. Especially if the "new and improved" spelling would differ across projects/workspaces. Cargo-hakari works pretty well for me because it delivers good value while staying on the periphery: I have to set it up, I have to run a command to update the workspace-hack Cargo.toml when my pre-commit hook tells me to, and sometimes I have to fiddle with configuration to make something work, but 95% of the cargo commands I run are not affected by it (except finishing faster than without workspace-hack). Integrating similar functionality into cargo proper could fix the remaining rough edges. Having less automation than I get today or making more invasive changes to my most common workflows does not sound appealing at all.

@weiznich
Copy link
Contributor

weiznich commented Oct 4, 2024

Is that a serious position or still the "devil's advocate" thing it apparently started as?

At least from my side it's only half-serious, but I consider that proposal as similar useful as everything that I heard from the cargo team as response to my real concerns.

That written: I don't believe that it is that much trouble to do a basic feature unification on your own. The simplest way to do that would be to put all your dependencies in your workspace Cargo.toml and then only reference them via dependency.workspace = true from the actual crates. In that way you cannot end up with different feature sets per crate. If you need something more fine granular than that, you can always track it down via cargo tree (which gives you a way to inspect which features are enabled for a certain crate) + cargo check --verbose (which gives a reason why a dependency was rebuild). Additionally as also pointed out above: I don't see how that would even a problem for local dev workflows as cargo just builds dependencies per feature set once as long as you don't clear the target folder. Even if you have the same dependency with different feature sets, in that case cargo will keep all variants. So from my point of view this mostly concerns from scratch build times, not incremental build times.

@epage
Copy link
Contributor Author

epage commented Oct 4, 2024

That written: I don't believe that it is that much trouble to do a basic feature unification on your own. The simplest way to do that would be to put all your dependencies in your workspace Cargo.toml and then only reference them via dependency.workspace = true from the actual crates. In that way you cannot end up with different feature sets per crate.

That works for all features activated at the top level but not by those activated by dependencies. You'd need your entire set of dependencies shared between all packages as well.

@weiznich
Copy link
Contributor

weiznich commented Oct 4, 2024

That works for all features activated at the top level but not by those activated by dependencies. You'd need your entire set of dependencies shared between all packages as well.

You can always add these sub dependencies as explicit dependencies as well. (As written before: I just say: That's possible to do, not the most easy thing to do.)

@hanna-kruppe
Copy link

hanna-kruppe commented Oct 4, 2024

In my experience, workspace members directly requesting different feature flags from dependencies is rarely the issue. When they do occur, it's often intentional and shouldn't be unified in every build of the workspace members (e.g., when some libraries are no_std). Transitive dependency edges cause more and worse problems, especially for foundational crates like syn or log or serde that have a ton of reverse dependencies, because all of those are rebuilt once per feature flag combination across all crates they depend on. As @epage says, addressing that requires spelling out dependencies on crates you otherwise wouldn't mention in your Cargo.toml files. This is the part that cargo-hakari automates and centralizes in a single place. Manually doing that is not a good use of mine or anyone else's time. It's also not a good use of my time to wait for Cargo to build a bunch of crates a fifth time because I just discovered a way to enable yet another combination of features for some central dependency. The distinction between from-scratch and incremental builds becomes blurry when you repeatedly experience incremental builds not being very incremental for no good reason. Reducing such surprises is one reason why I've become a big fan of workspace-wide feature unification.

At least from my side it's only half-serious, but I consider that proposal as similar useful as everything that I heard from the cargo team as response to my real concerns.

Well, please don't take that out on me by giving me only-half-serious advice on how to manage my workspaces. I hope the above explanations are useful or at least interesting for the part of you that's serious about this.

@pacak
Copy link

pacak commented Oct 4, 2024

You can always add these sub dependencies as explicit dependencies as well.

By hand this is very fragile once we get into hundreds of dependencies. hackari, hackerman and similar tools automate it but you need to set them up and periodically run / commit hack changes, etc.

Unless I'm mistaken commands like cargo check --workspace or cargo build --workspace are causing exactly the same effect on feature unification as proposed by this RFC, except extended for individual workspace members. Are they problematic too?

@sunshowers
Copy link

You (and epage) claim that again and again, but as pointed out above my code stops compiling without that I opt into something. How else than "breaking" would you call this? It worked before and stopped working after this change out of the perspective of the dependency maintainer. This RFC changes some fundamental assumptions how feature unification works again and again in incompatible way.

The RFC proposes opt-in behavior that changes how feature unification works. The exact way feature unification works is not frozen in amber, and saying that every opt-in change to it is a breaking change is not a reasonable view.

That sounds nice in theory, but won't work in practice. The users will still complain in the dependencies repo that something is broken in that crate and request a fix there. Especially for features that are promoted to drastically improve compile times. Or to word is sarcastically: On the one hand you claim that this is not a breaking change, on the other hand you request from me to deal with the effects of the breakage?

The definition of a breaking change is not "any change that imposes any kind of burden on anyone". Non-breaking changes impose burdens on people all the time. As I said, it's just a normal part of life as an engineer.

As a directly pertinent example: when weak features came out I needed to update hakari to handle them. I had to spend several hours coming up with a good data structure and an algorithm for that purpose. I didn't want to spend my time doing that; it certainly wasn't my favorite thing in the world. But as a professional I recognize the value in weak features, and I don't complain about the burden. I accept that that's part of the deal with maintaining a way to do Cargo graph traversals.

The needs of the many outweigh the needs of the few.

No it doesn't as the user needs to do something.

So to confirm, if the user does manually unify features, this is no longer an issue?

The user already needs to do something to opt into this behavior. It is not unreasonable to ask them to do this other thing.

I'm looking for a solution that can fix the problem in diesel itself.

That would be nice, but given that there is a relatively straightforward workaround, surely that's good enough.

At that point I might as well put into the documentation that this kind of setup is unsupported and they should go to complain to the cargo team if they want to see it supported. I would like to avoid that, but we might up in a situation where this is the best possible solution for me.

All you need to do is to say "if you're in this situation, unify your feature sets". You already have to do that to enable cargo build --workspace to work with resolver v2, do you not? It can't be that hard to have an FAQ page where you provide guidance.

You surely can use aliases for that so that no one needs to remember the correct flag. You certainly can also ensure that people use the aliases by making it much easier to use them instead of the normal cargo check/build/test commands.

One of the key benefits of Cargo is to provide a uniform interface across many different projects. Every project coming up with its own aliases is going to make the world significantly worse.


That written: I don't believe that it is that much trouble to do a basic feature unification on your own.

Yes it is. It certainly is a lot of trouble to keep this up-to-date.

That written: I don't believe that it is that much trouble to do a basic feature unification on your own. The simplest way to do that would be to put all your dependencies in your workspace Cargo.toml and then only reference them via dependency.workspace = true from the actual crates. In that way you cannot end up with different feature sets per crate.

Yes, you can, due to transitive dependencies. Saying that you cannot indicates to me that you have not spent enough time with this problem.

Please spend more time with this problem. Try out hakari on one of your own projects. Experiment with it, and note the upsides and downsides. It might help you understand why I'm so excited about it.

If you need something more fine granular than that, you can always track it down via cargo tree (which gives you a way to inspect which features are enabled for a certain crate) + cargo check --verbose (which gives a reason why a dependency was rebuild).

Sounds miserable, and ripe for automation to handle. As someone with a lot of experience writing developer tools, this would never meet my bar for an acceptable workaround.

Additionally as also pointed out above: I don't see how that would even a problem for local dev workflows as cargo just builds dependencies per feature set once as long as you don't clear the target folder. Even if you have the same dependency with different feature sets, in that case cargo will keep all variants.

Another indicator that you may need to spend more time thinking about this problem. The number of variants is absolutely massive. For example, syn has a number of features which can be enabled in all kinds of ways. Changing the feature set of syn will not only cause that to be rebuilt, but also everything dependent on it.

So from my point of view this mostly concerns from scratch build times, not incremental build times.

  1. This is not quite true. I consider cargo build --workspace followed by cargo build -p my-crate to be an incremental build, and that is directly impacted by this feature.
  2. From-scratch builds happen much more often than you think. For example, if you rebase your work in progress on the main branch and syn is updated, then almost all of your cached builds get invalidated and you're starting over mostly from scratch.

@eric-seppanen
Copy link

As far as I can tell diesel was partially broken by resolver v2 (cargo#14415) in 2021 (rust#88903). I'm sympathetic to the pain that has caused, and would support the argument that more should be done to fix it or reduce its impact. But I agree with the consensus that this RFC doesn't really change that situation: at worst it adds another method to reveal that existing problem.

@weiznich
Copy link
Contributor

weiznich commented Oct 4, 2024

First of all: Again as you seem to ignore that all the time: I still not saying that this RFC is a bad idea and shouldn't happen. I'm merely asking for addressing the raised concern, which again is mainly on a social level. That social concern is mostly caused by the resolver="2" change a few years back, that was technically not allowed and shouldn't have happened in that way. The technical part is interesting to discuss and I really would like to see that fixed first, but it is not blocking from my point of view. Addressing the social part of the concern can be as simple as adding a statement to the RFC that says the implementation needs to make sure to warn about these situations and make sure to not put that work onto the affected crate maintainers. Maybe even include that you don't want to repeat the resolver="2". How to do that can be an open question, after all these are evaluated before stabilisation. I cannot see why it's so complicated to just do that. I even suggested that above before. Instead you keep repeating the same (in my opinion partially wrong) arguments again and again. I mean those arguments are interesting to discuss, but I cannot see that we will ever agree there and they are in my opinion only tangentially relevant for moving the RFC forward.


The RFC proposes opt-in behavior that changes how feature unification works. The exact way feature unification works is not frozen in amber, and saying that every opt-in change to it is a breaking change is not a reasonable view.

Can you point to an RFC or any official team decision that says that feature unification is not considered to be stable? If not I would reasonable expect that this is covered by rust stability guarantee. If it's considered to be not stable I really wonder how any crate would be able to guarantee anything about its api.

As a directly pertinent example: when weak features came out I needed to update hakari to handle them. I had to spend several hours coming up with a good data structure and an algorithm for that purpose. I didn't want to spend my time doing that; it certainly wasn't my favorite thing in the world. But as a professional I recognize the value in weak features, and I don't complain about the burden. I accept that that's part of the deal with maintaining a way to do Cargo graph traversals.

I would assume that you parse the Cargo.toml files for that? If that's the case, it's not comparable as that format always had the possibility to be extended in some way.

The needs of the many outweigh the needs of the few.

That is a really dangerous argument. You shouldn't use that in any discussion at all in my opinion.
(Also it's always easy to claim that without backing it up with evidence)

No it doesn't as the user needs to do something.
So to confirm, if the user does manually unify features, this is no longer an issue?

My main point is that it adds a new way to break things. The old ways are unfortunate but cannot be changed. Ideally we shouldn't add new ways before we solved the old issue at all.

All you need to do is to say "if you're in this situation, unify your feature sets". You already have to do that to enable cargo build --workspace to work with resolver v2, do you not? It can't be that hard to have an FAQ page where you provide guidance.

That does not help in some situations as you often do not want to have both features enabled as the both pull in native requirements. That's something I've already written before a few times.

Yes it is. It certainly is a lot of trouble to keep this up-to-date.

Interestingly that seems to work well for me for rather large projects with ~700 dependencies, so either you all are changing dependencies much more often that I do or my workflow is otherwise different.

This is not quite true. I consider cargo build --workspace followed by cargo build -p my-crate to be an incremental build, and that is directly impacted by this feature.
From-scratch builds happen much more often than you think. For example, if you rebase your work in progress on the main branch and syn is updated, then almost all of your cached builds get invalidated and you're starting over mostly from scratch.

  • For the update we talk about one/a few slow builds. I'm not sure if it's really reasonable to bring that as argument. Yes you need to wait a bit there, but that's once. Or do you rebase your branch every hour?

  • As for the from scratch builds: Sure if you really want to use only these commands you have that problem. If you adjust them to be more in line with what your feature set requires you won't have them. And to bring up a point you previously used as argument: That command sequence won't work if you cannot build the whole workspace.

@sunshowers
Copy link

Can you point to an RFC or any official team decision that says that feature unification is not considered to be stable?

I said "frozen in amber", i.e. you can't even opt into different behavior. The behavior is stable, it's just not frozen in amber.

If not I would reasonable expect that this is covered by rust stability guarantee. If it's considered to be not stable I really wonder how any crate would be able to guarantee anything about its api.

It is stable. An opt-in flag changing behavior does not impact stability.

I would assume that you parse the Cargo.toml files for that? If that's the case, it's not comparable as that format always had the possibility to be extended in some way.

No, the data comes from cargo metadata, something that is supposed to be a stable format. The only way to not impose any burden on me would be to never have shipped weak features. But it wouldn't have been reasonable for me to ask for that.

That is a really dangerous argument. You shouldn't use that in any discussion at all in my opinion.

As a engineer I've always worked under this maxim, and I will keep making this argument for the rest of my career. Sometimes the cost of a large improvement for most people is some extra burdens on a few. That's part of living in society.

For the update we talk about one/a few slow builds. I'm not sure if it's really reasonable to bring that as argument. Yes you need to wait a bit there, but that's once. Or do you rebase your branch every hour?

I rebase at least once a day, often 3-4x a day, because I work on fast-moving collaborative projects where several of us work on the same component -- and we all want to pull in each other's fixes. And no, it's not once by default -- each time you run a cargo build -p <something> you have to rebuild from scratch in practice. What makes it so that it's once is feature unification.

As for the from scratch builds: Sure if you really want to use only these commands you have that problem. If you adjust them to be more in line with what your feature set requires you won't have them.

No, this is not a reasonable thing to expect people to do.

And to bring up a point you previously used as argument: That command sequence won't work if you cannot build the whole workspace.

But after this sequence of operations:

  1. cargo build --workspace
  2. edit some files in my-crate

It is often the case that cargo build --workspace no longer works, but cargo build -p my-crate continues to work. This is an exceedingly common situation, and one that only feature unification can make better.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. T-cargo Relevant to the Cargo team, which will review and decide on the RFC.
Projects
Status: FCP merge
Development

Successfully merging this pull request may close these issues.