cargo
cargo copied to clipboard
Ability to disable individual default features
The current documentation here: http://doc.crates.io/manifest.html states:
With the exception of the default feature, all features are opt-in. To opt out of the default feature, use default-features = false and cherry-pick individual features.
This is quite rigid, since it means you can't make a small adjustment to defaults - where a program may have multiple options which aren't related.
It also means if I use a 3rd party package with modified defaults, after an update I need to check if defaults where added which I might want to enable.
Note, I asked this question on stackoverflow, it was suggested to open an issue here: http://stackoverflow.com/questions/39734321
Yeah it's true that default features are a very weighty decision and are quite difficult to reverse. I don't think we can add the ability to force-disable them, however, because how would we know whether a dependency actually needs the feature or not?
@alexcrichton, maybe this is a bigger change then I'd expected, am not very experienced using Cargo.
Could dependencies list features they require?
Dependencies already do, the default
feature is just special where it's turned on by default. If a crate has a feature that may be disabled then in general crates shouldn't move it to a default feature.
In that case wouldn't it be possible to disable a default - having the same behavior as if you explicitly listed all defaults, without the one(s) which have been requested to be disabled?
Yeah you can disable default features with default-features = false
, but Cargo also unions features requested for a crate so if any crate doesn't disable a default feature then it ends up enabled.
This is a backward-compatiblity hazard. E.g.: It seems that libcore
is getting default features, relatively harmless ones, they change some formatting. Some crate today might opt out of default features and only select the float formatting thing. This means that libcore
can never add another default feature that disables existing stuff without breaking that crate.
The better way to handle that would be that each crate has a list of default features that it does not require. This way, default features can be added in the future.
I don't know about libcore
but at an application level - it often makes sense to be able to disable dependencies (which are default since they are used in official release builds).
In for you may want to build FFMPEG with patented codecs or not. You may want to build a video editor with FFMPEG or not. This is typically the case for file formats, codecs, or optional scripting languages (as VIM has with Python, Ruby, Lua... etc)
Note that I don't talk about hard-disabling dependency feature, but rather the equivalent of today's default-features = false
and then a list of all the other features in the features = []
list. That means if some other dependency pulls in my disabled default feature, I still get it.
@tbu- the idea seems sound to me at least!
I'd especially like this to work on command line. --no-default-features
is lengthy, and requires user to repeat other defaults. A syntax sugar for it would be very helpful.
Here's a proposal:
Features prefixed with -
are removed from the set of default features, i.e.:
D
= default features
F
= user-specified features without -
prefix
N
= user-specified features with -
prefix
Currently:
features = D ∪ F
Proposed:
features = (D ∖ N) ∪ F
e.g. given
[features]
default = ["foo", "bar"]
foo = []
bar = []
quz = []
--features=-bar,quz
is desuraged to --no-default-features --features=foo,quz
This would also be very useful for testing e.g. --all-features --features -legacy-compat
.
(Though ofc you do need to worry about specifying the interaction with feature dependencies.)
Going to do a quick brain dump since this has been on my mind...
When it comes to evolving features without a breaking change, the biggest hazard is taking functionality that already existed and making a new feature from it. Existing features can already be split if you are ok with not reusing existing names though deprecation support would help a lot.
In cargo's existing model of default-features=false
, existing functionality that gets split into a feature is being split out of the "base" functionality. https://github.com/rust-lang/rfcs/pull/3283 works to solve this by adding an explicit "base" concept with a no-default-features
feature that you can add to.
If we had started from scratch inventing default features, another approach to breaking existing functionality into a feature would be if we allow subtracting features from default
. https://github.com/rust-lang/rfcs/pull/3146 called this out as an alternative but it has the following challenges
- It takes a lot of work to get people into the additive-feature mental model that this detracts from that messaging
- Issues related to other things activating the de-activated feature
- If we allowed general feature opt-out, it could likely break crates as they might check for a feature and assume its required features are available.
I think limiting the scope of feature opt-outs and ensuring design keeps the intent clear can overcome these challenges. I also think we can transition to this even if we didn't start with this.
A rough outline of my proposal
- Further entrench
default
s special status by deviating from normal feature behavior- Placeholder name for this kind of feature is "meta-feature"
- Deprecate
default
from includingdep/feat
features, possibly turning into an error in future editions. It can only include features that the dependent crate can reference - Provide a syntax for opting out of default features in manifest and on the command line
- Errors if the feature is not currently a default feature in the dependency
- This means you can't reference features that will eventually exist which would be wanted for maximum version compatibility
- Explicit non-goal to force/assert the deactivation of features
- Explicit non-goal to deactivate features from features other than default
- Errors if the feature is not currently a default feature in the dependency
- Stop providing the
default
feature to the compiler on an edition boundary, preventing people from being able to do#[cfg(feature = "default")]
- Deprecate
default-features = <bool>
from the manifest,, possibly turning into an error in future editions
It would be interesting to see if this would help with --all-features
but I would not see that as blocking on the design.
Of course, there are details to be worked out and bike shedding to be done.
Other benefits
- Help with no-std
- Help with "I want all features but...", like with clap
Open questions
- How to transition to this
- No longer supporting
default-features = false
, directly or indirectly, is a breaking change
- No longer supporting
- Adds a new type of breaking change: completely new default features
- See https://github.com/rust-lang/cargo/issues/3126#issuecomment-1299293690
If we allowed general feature opt-out, it could likely break crates as they might check for a feature and assume its required features are available.
FWIW I would expect -foo
to disable any features that have foo
as a requirement (but other features that were activated because of it to stay on).
Explicit non-goal to force/assert the deactivation of features
Yes, it's removing the requirement, not the feature itself.
I agree that opting out of future default-features
is unfortunate. But rather than move to a default
feature, it seems like we could "just" allow opting out of default-features
's specified features, and discourage/deprecate setting no-default-features
for a manifest dependency.
A default
feature then can be just a pattern for allowing opting out of a chunk in one opt-out like today's no-default-features, rather than cargo-recognized.
FWIW I would expect -foo to disable any features that have foo as a requirement (but other features that were activated because of it to stay on).
For this to work, it could only have that affect within the current crate's resolving of features. I worry these semantics are too close in semantics to asserting that a feature won't be present at all (remove requirement everywhere) that it could be confusing.
I also think it has the chance to break people. I'm going to expand on a case in RFC 3146. Say I have a dependency with
[features]
default = ["foo", "bar"]
foo = []
bar = []
and I depend on it with ["foo", "-bar"]
And later the dependency changes it to
[features]
default = ["foo", "bar"]
foo = []
bar = ["foo"]
Then according to these semantics, foo
will be disabled on upgrade, breaking dependent crates. This is why the earlier proposal was looking to special case default
's semantics into what I'm terming a meta-feature so only default
will be in a quasi-state because we only modify its requirements but no one will be allowed to observe that quasi-state, making it work out.
Then according to these semantics,
foo
will be disabled on upgrade
not by my intuitive understanding, though explaining it is a bit difficult. -bar
would disable bar
and default
, but not foo
, even if foo
was not explicitly enabled.
.... also wait we set cfg(feature = "default")
I did not know nor expect that
explaining it
- Collect feature requests by the current mechanism
- For each
-
feature- Remove the feature request
- Remove the feature requests which enable the removed feature, recursively
- (other features implied by the superfeature remain; this is only possible for
default
with the below error)
- (other features implied by the superfeature remain; this is only possible for
- If any feature removed in the previous was explicitly requested, error
- Possible exception for
--all-features
or other "soft" ways of enabling features
- Possible exception for
.... also wait we set cfg(feature = "default") I did not know nor expect that
I assumed we didn't either but I went and tested it. We only set it if the user explicitly mentions a default
feature
If any feature removed in the previous was explicitly requested, error
Wouldn't this still be a semver breakage for bar
to require foo
? Without the error, we are disabling foo
when code was written assuming it was enabled. With an error, we went from a working build to a failing build on cargo update
since foo
will have been removed because bar
was removed and foo
was explicitly requested.
At RustConf, one of the problems that was pointed out is that default-features = false
would be removed on an edition boundary of the dependent so a dependency cannot guarantee when all of their dependents are on the new edition to start relying on the new semver semantics.
Quick thoughts
- They could bump their major once and document it
- We could force old cargo versions to not see the crate like with https://github.com/rust-lang/cargo/issues/10623
- We could rely on rust-version-aware resolver and say the deprecation message is good enough
- When depending on something declaring, say 2024 edition, the dependent can't set
default-features = false
One thing that seems not possible with the proposed design is adding a new default-active std
-using feature to an optionally std
-using library.
Imagine we have a crate foo
which has features:
[features]
default = ["std", "bar"]
std = ["dep:std"] # with sysroot dependencies, otherwise `#[cfg(feature = "std")] extern crate std;`
bar = []
In a no-std project we then depend on this crate and disable the std
feature:
[dependencies]
foo.version = "1.0.0"
foo.features = ["-std"]
Now, foo
wants to introduce a new feature baz
which requires std
to implement, it also wants this to be provided by default:
[features]
default = ["std", "bar", "baz"]
std = ["dep:std"]
bar = []
baz = ["std"]
The downstream project on a non-breaking update of foo
would now have the std
dependency activated, transitively through the new default-feature baz
. This would break any build on a target which does not have std
available.
- When depending on something declaring, say 2024 edition, the dependent can't set
default-features = false
The issue with this is that the dependent can no longer update to 2024 edition without doing a semver-breaking release, right?
I've thought for a long time that the default features design including the required additivity of features is problematic. The problem here is that any crate in your dependency graph can enable a feature in a crate you depend on without you being aware. Being able to specifically request feature disabling would make a lot of this stuff easier, I think, if Cargo would guarantee that a crate would fail to build if some part of the dependency graph tries to enable a feature that I've requested be disabled.
While that's a decent departure from the current state of things, I feel it is more aligned with the Rust philosophy of builds failing early if you don't have your types lined up correctly: you can specify that you don't want some feature and if there's some "spooky action at a distance" that's enabling that feature anyway, Cargo will tell you about it.
The downstream project on a non-breaking update of
foo
would now have thestd
dependency activated, transitively through the new default-featurebaz
. This would break any build on a target which does not havestd
available.
Maybe the solution here would be extending the notion of weak dependencies to features, so it becomes expressible that a feature should only be part of the default if std
is enabled? Straw man: default = ["std", "bar", "std? ( baz )"]
. As I argued in the aforelinked internals post and also here, I think there could be a lot of value in having a richer way to express features and dependencies. (For more info, Gentoo's development manual has the complete syntax.)
One thing that seems not possible with the proposed design is adding a new default-active
std
-using feature to an optionallystd
-using library.
Correct, this is not intending to cover every form of feature evolution but is focused on splitting built-in behavior into default features.
So, the intent is not to just enable new kinds of feature evolutions, but rather change the set of allowed feature evolutions, removing ones that are assumed to be unnecessary like my example (which is possible with todays default-features = false
formulation) in favor of ones like moving ungated functionality into features?
Good point that this might be adding a new way of things breaking; I've added a note to my proposal that that needs to be looked into.
IMO it would be a major constraint that adding new features that require std
would need a semver-breaking release.
So perhaps specifying "not foo
" should mean "do not enable any default feature of this crate that results in foo
being enabled", so if there's default = ["bar"]; bar = ["foo"]
, it disables bar
as well.
The issue with this is that the dependent can no longer update to 2024 edition without doing a semver-breaking release, right?
One way to solve this could be to create a new special feature with a different name, let's call it new-defaults. If a crate has new-defaults defined, then it can't be opted out of with default-features = false
, and using the old default mechanism can be deprecated in a new edition.
A lot of people say they want negative features, but I find the idea hard to discuss because there is usually much hand-waving about what the semantics actually are. The current additive semantics are deeply part Cargo; we see the same semilattice stuff both the intersecting of version bounds and unioning of feature sets, in fact.
There seems to be two variants, I will call them "hard" and "soft"
"hard" means "no definitely do not do this" as @kornelski I think says. @epage says in https://github.com/rust-lang/cargo/issues/3126#issuecomment-1190950930 that this is out of scope and I think that is the correct decision. This sort of thing is deeply non-compositional. The first idea of package management is that we can always "add more stuff", indeed thing likes the "orphan rules" go out of their way to restrict crates so this is preserved. People will abuse the "hard" want to merely mean "I don't need this", and then we'll have endless issues with people needing to fork crates to remove artificial restrictions. A disaster.
"soft" means "I don't need this", and is what the idea from @epage is. This is compositional, because it is just sugar: the underlying model is still additive. but I think it is also just confusing. Feature dependencies can easily bring back an opted-out feature. The combination of multiple crates' depedencies, or even multiple features dependencies from the same crate as @Nemo157 points can also do the same. People will misunderstand the "soft" as the "hard", and get even more confused about how Cargo actually works.
Another thing to think about is
If this new version of the library was actually the only version, would one use soft negative features?
My guess is the answer is "no". Soft negative features, because their confusing UI, are only worth it as crude feature migrations. So over time we get a mix of positive and negative features which don't make sense in isolation, but only as an accident of history. This I think is a bad user experience, and the classic pitfull of borrowing from future users to pay of today's debt.
The only solution that allows users to both avoid compatibility issues and write issues that make sense "in the moment" and not compromises with history is some extra indirection notion of feature migrations / feature name-spacing / etc.. So I think our goal to be to get there eventually.
Recall how it is proposed that root crates should be able to violate the orphan rules. I think similarly it does make sense for root crates / virtual workspace roots to be able to have hard negative features. That satisfies the "help I need to disable this thing" problem while avoiding the huge issues of compositionality. Yay!
I think similarly it does make sense for root crates / virtual workspace roots to be able to have hard negative features.
That would introduce a backwards compatibility hazard. If Project A forbids feature X in library B, and also depends on library C, then if C adds a dependency on Feature X from B, it will break project A.
And by itself, it still doesn't solve the problem of splitting out a new feature from the base. Because doing so would still break libraries that use default-features = false.
@tmccombs I don't think compatibility of that sort matters with the workspace root. If you really don't want feature X, then you don't want any version of C that requires X.
C shouldn't feel boxed in because now (unlike before), users can forbid X, but anyone that forbids X knows the penality may be some build plans are ruled out --- just as they asked for!
Basically we have to always step back and ask what we are trying to solve. For intermediate noes in the the dependency graph the "mechanism design" is very complicated, we want Cargo.toml
s to express concepts that are compositional and maintain the health of the ecosystem as whole. But for concrete projects not reusable components we don't need to have such high-minded concerns: let them do whatever they want and alone (no contagion) suffer the consequences.