DISCLAIMER: I don't program in Rust at all, I just read along.
I've been following along with the Rust for Linux project a bit and have been especially curious about the work around preventing use of "panic on OOM" APIs. One thing I saw come up was how to deal with gating pre-existing functionality behind new features. This was relevant to alloc: Add unstable Cfg feature `no_global_oom_handling by Ericson2314 · Pull Request #84266 · rust-lang/rust · GitHub, if I understand the issue correctly.
From what I understand, a new "global_oom_handling" default feature couldn't be used there because it would break existing upstream dependencies that have used "default-features = false" but require use of this previously non-existent feature.
I wonder if it would be possible to support this scenario by having a version number for the set of default features. It would just be a single number, which is why I put "semver" in quotes in the title.
The idea is that when upstream opts out of default features, it would do so by version number. So all existing crates using "default-features = false" would be considered to be opting out of the v1 default features set. Any time a new default feature is added in downstream that would otherwise be a breaking change, the default features set version number would be bumped. The new feature would then be marked as being a "breaking" default feature as of version X.
Upstream would then have any "breaking" default features enabled that come from a higher version number than the default features it opted out of. This ensures that upstream continues to build, as there is no way it could have knowingly opted in or out of the feature before it was created.
Nothing would change about features otherwise. E.g. there's nothing about "negative" features in here.
I haven't really gone into technical detail at all, I wanted to post the basic idea here first in case I've missed something obvious and this is a total non-starter. See my disclaimer.
Here's a rundown of what this might look like if "global_oom_handling" were added this way. The syntax is purposefully very mock, but hopefully it gets the general idea across.
Before global_oom_handling is conceived:
Upstream:
[dependencies]
std = { version = "*", default-features = false, features = [] }
Downstream:
default = []
After global_oom_handling is released:
Upstream:
[dependencies]
std = { version = "*", default-features = false, features = [] }
Downstream:
[features]
default = ["global_oom_handling"]
default_current_version = "2"
global_oom_handling = []
global_oom_handling_breaking_default_at = "2"
Upstream would now have the "global_oom_handling" feature turned on despite not opting in, because it opted out of the "v1" default features and global_oom_handling is a "breaking as of v2" default feature.
If upstream doesn't actually need the "global_oom_handling" feature and wants to opt out of it now that it exists, it just needs to opt out of the "v2" default features instead:
Upstream:
[dependencies]
std = { version = "*", default-features = "false_v2", features = [] }
Alternatively, consider if upstream adds a new default feature that gates brand new functionality that didn't exist before. This can already be done today, and nothing changes here.
Before:
Upstream:
[dependencies]
std = { version = "*", default-features = false, features = [] }
Downstream:
[features]
default = ["global_oom_handling"]
default_current_version = "2"
global_oom_handling = []
global_oom_handling_breaking_default_at = "2"
After:
Upstream:
[dependencies]
std = { version = "*", default-features = false, features = [] }
Downstream:
[features]
default = ["global_oom_handling", "non_breaking_feature"]
default_current_version = "2"
global_oom_handling = []
global_oom_handling_breaking_default_at = "2"
non_breaking_feature = []
Upstream will have "global_oom_handling" enabled as already described, but it will not have the "non_breaking_feature" feature enabled as this feature wasn't marked as a breaking change and it's not explicitly opted in to.