currently, if you wish to disable a crate's default features, you must disable all its default features.
i propose adding a way to list a number of default features that should be disabled, along with the existing list of features that are enabled.
the main benefit of this is (if we assume full ecosystem adoption) allowing moving always-available functionality to behind a new default feature without breaking dependents.
Arguably default features are evil (except for in bin crates, where we want sensible defaults for cargo install):
If any of my dependencies don't disable default dependencies it doesn't matter if I do, due to feature unification. And forgetting to always disable all default features when writing library code is easy.
std is often a default feature, this exhastrubates the above issue when writing no-std code (as I often do for microcontrollers).
Most crates don't need all the default features of their dependencies. And even if they do, then might not when another default feature is added down the line. So you get longer compile times (sometimes massively so if the features pull in extra dependencies) and possibly more bloated binaries (depending on how well that is optimised away)
My suggestion is to not have default features (except in bin crates):
List only what you actually need.
Disable all default features for your dependencies.
It would also be risky to let you randomly disable features of other crates. What if they depend on a specific feature for soundness (and it won't cause a compile error to remove it). Hardly ideal (nor likely) but not impossible.
I do not see a guaranteed sound way to salvage default features that is compatible with embedded development. Just don't use them is my suggestion. They do not improve ergonomics much and they are not worth being lazy.
Ah, that is a fair point, if you want to break out some previously always enabled part. Good question, hadn't thought about that. Today default features would be the only possible way to do it. But it is still a breaking change for anyone disabling default features.
btw there are a couple of related problems that we need to keep in mind when coming up with a solution
As a consumer of a package, I want to opt-out of individual features, rather than opt-out of everything and re-opt-in to the parts I want
The big question is of syntax. It needs to be clear that you aren't activating a feature but it may be activated through another path to the dependency. There has been some talk of moving away from our feature micro-DSL to explicit fields in a table.
I would love for this to apply to more than default.
As an extension of another package, I want to only explicitly enable the parts of a library that I require so I don't accidentally enable more for the people using my library than needed.
There are still use cases for default-features = false.
As a crate author, I want to take built-in functionality and move it into a feature.
Being able to remove features isn't sufficient on its own because the dependency has to know all of its users are now doing that in a way without being forced to make a breaking change
Here is an idea for how to allow default feature set evolution. I am not sure whether it's original or possibly a rewrite of something I read some time ago.
Every package that can be depended on must have one or more (possibly implicitly defined) “base features”.
Every dependency must specify one or more base feature names.
Exactly one of a package’s base features is marked as “currently recommended” for the benefit of cargo add, docs.rs, etc.; this serves the role that the default feature does today.
What this does is ensure that a package can make functionality optional as a non-breaking change, because they can introduce a new base feature (recommended or not) that excludes the now-optional functionality, while making all base features that existed in previous semver-compatible versions still include the optional feature.
As a concrete example, suppose that we have this package:
Now suppose this package wants to make its dependency on std optional, and encourage its dependents to not depend on std if they don't need to either. Its author may publish:
Now, dependents may move to full2 or minimal2 to drop the formerly required std dependency. Dependents which do nothing must have depended on either base1 or minimal1 and thus experience no changes.
For compatibility with existing packages,
Existing packages are considered to have two base features: the current default feature, and one which enables no other features (corresponding to default-features = false, features = []); let’s say we call that one no-default.[1]
If a package migrating to the new system defines both default and no-default, default must be a superset of no-default. (This ensures that they can't make default-features = false surprisingly enable any features.)
I think the system that I have described above could make the semantics of such opting out clearer: it would mean that you have to make a request shaped like "full2" - "std", which makes it more noticeable that you are specifying a set of features with some features deleted from it, rather than specifying features to disable.
(Note that the system with subtraction is different from the system without subtraction: with subtraction, package may find that none of their regular features are enabled, but without subtraction, this cannot happen unless one of their base features has an empty list.)
atm I think we need a mix of both base features and removing features to cover the use cases I mentioned.
imo base-features should live in the features table because default already does, and there is a tooling and education cost to adding a second features table. My preference would be for us to have a field to mark a feature as a base feature in the feature metadata table. You'd explicitly specify an alternative base feature through default-features = "full1". The awkward part is that the is-default field (whatever its called) would change its default depending on whether the feature name is default (and maybe no-default) or not.
As for default-features = false mapping to a no-default feature, I think that is the minimum of what we'd need if we said that we only wanted to provide subtractive features and deprecate default-features completely. We'd have the problem of trying to reserve another name in a user-control namespace.
This is something that comes up regularly in association with addressing default-features = false, and I wonder to what degree blocking on all of it is the right path, versus a simpler version.
If we intend to deprecate default-features = false in favor of dependents marking specific default features as unneeded, then we don't need to gate that on a fully general solution for multiple levels of base features. We could, instead, have a single flag on features for "default-features = false doesn't turn this off", and if a crate wants to move functionality to a feature it can mark that feature accordingly. That way, users of default-features = false don't get broken, and users of unneeded-features = ["newfeature"] (by whatever mechanism we provide for that) are specifically naming the feature so they know what specific functionality they're opting out of.
C is a fast to update dependency that adds new default features
Whenever you update your lock file you will get the new default dependencies from C that B has not yet opted out of. You then need to get B to make a new release just so they opt out of the new default features.
And because you are targeting embedded you really care about code size. Your program barely fits in flash as is. In this scenario where we opt out of specific features, it is now a breaking change to add new default features.
That's going to be true f we have any mechanism to make sure that B doesn't break because functionality of C that it depended on was moved to a default-enabled feature. Either B can opt out of features it doesn't yet know about (and B breaks if C moves functionality to a default-enabled feature), or B can't opt out of features without knowing about them (and B never breaks, but needs to be updated to opt out of new features of C).
Note that it's always possible to subdivide an existing feature, by creating new features for subsets of it and letting crates opt into the subsets. So, it'd always be possible for a crate to provide a "bases" mechanism itself if that's what it wants to present, by providing separate features. I think we can do that without having any special support in Cargo. The only case that would need special support in Cargo is "here's a thing that's part of default for backwards compatibility, but default-features = false doesn't disable it, you have to know about it to disable it".