Initial support for optional features
This commit introduces initial support for optional features, by
implementing a way for a dependency declaration (currently *only* in
the DSL) to request variants of the target component that provide one
or more capabilities.
Previously to this change, selection was (simplified) done like this:
1. find the target component
2. select the variant of the target component which matches the requested
Now, selection introduces another step:
1. find the target component
2. filter variants by eliminating those which do not provide the requested
3. select the variant in this list which matches the requested attributes
Several changes had to be implemented:
First, component metadata rules calling `addCapability` will now return
a component which capabilities _include_ the default capability.
Second, attribute filtering is done in a secondary step, which means that
if there are no variant matching the requested capabilities, we will immediately
Only allow more than one variant of a component if it has different capabilities
This commit significantly reworks how capabilities are handled, in particular
with regards to accepting 2 different variants of the same component to appear
in a graph. It is now allowed to have 2 variants of the same component if they
have distinct capabilities.
It shall be reminded that if a variant doesn't explicitly declare any capability,
it is assumed to carry the _implicit capability_, which corresponds to the GAV
of the component. Said differently, by default, all variants of a component
are supposed to be incompatible with each other (they cannot appear in the
same dependency graph).
If 2 variants are indeed compatible, then they _must_ have different capabilities.
This will be used to support optional dependencies, by making it possible to
express that one has to choose a specific set of compatible variants to enable
There are however a few issues that needed to be addressed in this commit. First
of all, what we've just described is only true if we use variant-aware selection,
and more specifically attribute-based variant selection. If not, then a legacy
mode is used, and it's possible to have 2 "variants" of a component on the same
graph. This is typically the case when there are dependencies on 2 different
_configurations_ of a project:
api project(path: ':foo', configuration: 'conf1')
api project(path: ':foo', configuration: 'conf2')
For backwards compatibility reason, even if the 2 configurations actually
provide the same capability, this is allowed. Another case is when a cycle is
found between a root component and a transitive dependency corresponding to
the same component in a lower version.
Last but not least, in the engine the implicit capability is treated
specially for performance reasons: capability conflict resolution is very
expensive, and adding an explicit capability to all components would trigger
conflict resolution everywhere (to find out if other components provide the
same capability). This leads to some optimizations in the code which are not
necessarily easy to follow, but properly documented.