Hi! Thanks for working on this - there's a clear need for some kind of exclusive feature model which isn't currently being met by Cargo.
One comment on the examples - the TOML doesn't seem to make sense from a data model POV:
[features]
json = []
and
[features.json]
...
are referring to the same entity with different types, so this either shouldn't parse, or if it does it loses one or the other. The more detailed description later on clarifies this to some extent, but it doesn't help with making the initial explanation any clearer.
I like the use of the term "capabilities", but I'm not sure about this design and their interaction with features. Features are already fairly awkward to use at scale (its hard to tell where a feature has been enabled when analyzing a large dependency graph), and this seems to make the problem worse.
More generally, you don't really frame what problems exclusive features are needed to solve. As I see it there are two cases:
- The two features are intrinsically incompatible - if a single binary contained both then it would be non-viable (it either wouldn't build, or would behave badly on execution). An example of this would be linking to two versions of a foreign library with conflicting symbols (though this is what the
link
metadata is intended to address).
- The features change either the API or the functionality of the package in incompatible ways which affect its dependents. This is currently completely unaddressed, however I think it can be handled more gracefully than this proposal.
The former is something that should be flagged at build time as an error, but it needn't necessarily prevent dependency resolution (this is a subtle point, but for my use-case distinguishing resolution from build is important). This is currently a limitation with link
, especially as it doesn't distinguish build script dependencies from library dependencies (though -Zavoid-dev-deps
looks like it could help with this).
For the latter case, rather than failing at either resolution or build time, I think it should just split the dependency graph. In other words, if both "A with capability X" and "A with capability Y" appear, then treat them as completely separate dependencies, in the same way that two distinct versions of A currently are.
As @withoutboats mentioned above, non-brittleness is one of the ecosystem's good properties we'd like to preserve. If you have an executable A depending on B and C, and B currently depends on D+X, but then you rev C and it depends on D+Y, you don't want the build to break. You just want B and C to link with the variants of D which satisfy their requirements.
Of course this gets complicated if B and C expose definitions from their D variants as part of their public API, but that's no worse than having version skew on D.
Perhaps another way of putting it, you could consider capabilities as being extra flags on the version which are never semver compatible, so 1.0+X is never compatible with 1.0+Y, but would be compatible with 1.1+X.