Improve conflict resolution to better deal with conflicts across different module name/group
- 'com.google.collections:google-collections' and 'com.google.guava:guava' are conflicting, prefer guava.
- 'org.jboss.netty:netty' and 'io.netty:netty*' are conflicting, prefer io.netty.
- 'org.springframework:spring' and 'org.springframework:spring-' are conflicting, prefer 'org.springframework:spring-' (for example, spring-core).
- A team in an organization decided to extract a standalone project out of a bigger project. The group id of the modules moved to a new project needed to change. Now there's a risk that consumers will have problems with conflict resolution.
There are two things we need to be able to do when traversing the graph:
- Detect whether two different modules conflict with each other.
- Decide which of a set of conflicting modules to select.
DSL mock up:
dependencies {
components.module('com.google.collections:google-collections').replacedBy('com.google.guava:guava')
}
This states that module 'com.google.collections:google-collections' was replaced/superseded/deprecated by 'com.google.guava:guava' at some point. This implies two things:
- Every version of 'google-collections' and every version of 'guava' conflicts with each other.
- Every version of 'google-collections' is older than any version of 'guava'.
- New DSL API (see above)
- Smarter conflict resolution, for example: no more guava and google-collections together in the classpaths.
- A replaces B - if both modules exist in the graph, only A is included in the graph
- ensure jar B is not included in the result
- A does not replace B when only B module exists in the graph
- A replaces B when both are transitive dependencies
- A replaces B given traversal order:
- A, B
- B, A
- A replaces B:1.0 and B:2.0 given traversal order:
- B:1.0, B:2.0, resolve conflict, A
- A, B:1.0, B:2.0, resolve conflict
- A:2.0 replaces B:1.0, B:2.0, A:1.0 given traversal order:
- A:2.0, B:1.0, B:2.0, resolve conflict, A:1.0
- A:2.0, B:1.0, B:2.0, A:1.0, resolve conflict
- A:1.0, B:1.0, B:2.0, resolve conflict, A:2.0
- A:1.0, B:1.0, B:2.0, A:2.0, resolve conflict
- A:1.0, A:2.0, resolve conflict, B:1.0, B:2.0
- B:1.0, B:2.0, resolve conflict, A:1.0, A:2.0
- corner cases
- A replaces B replaces C, all included in the graph, A is selected
- unhappy paths
- A replaces B, B replaces A (cycle) - reasonable error is emitted
- A replaces A - reasonable error
- incorrect inputs: nulls, malformed notation, unsupported types
- the replacement target is unresolved
- the replacement source is unresolved
- A does not replace B when both are in graph but A is excluded via an exclude rule
- A replaces B, B is excluded via rule, only A ends up in the graph
- A replaces B and ResolvedConfiguration API is still happy
- A replaces B, only A artifact is included in the ResolvedConfiguration's artifacts
- A does not replace B when both are in graph but B is changed into a different module via dependency resolve rule
- A replaces B, B is turned into A via dependency resolve rule -> only A appears in the graph, selection reason for B is 'resolve rule'
- A replaces B when A is created from C via dependency resolve rule
- A replaces B when A is created from C and B from D via dependency resolve rules, selection reason for B is 'A replaces B'
Make following possible:
dependencies {
components.module('com:a').replacedBy('com:c')
components.module('com:b').replacedBy('com:c')
}
- a replaced by c (a->c), b->c, all a,b,c in graph, only c is resolved
- a->c, b->c, only a,b in graph, only c is resolved
- a->c, b->c, only a,c in graph, only c is resolved
Make it possible to declare module multiple replacement targets for single replacement source.
- modules replaced by a set of modules: spring -> spring-core, spring-aop, ...
- as above but starting from some version: groovy -> groovy, groovy-ant, groovy-xml only starting from 2.0
- a set of modules replaced by a single module (hypothetical, not covered by this story)
DSL mock-up:
dependencies {
components {
module('org.springframework:spring') { ModuleMetadataDetails details ->
details.replacedBy 'org.springframework:spring-core'
details.replacedBy 'org.springframework:spring-aop'
}
This states that 'org.springframework:spring' was replaced by both 'org.springframework:spring-core' and 'org.springframework:spring-aop' at some point. This implies:
- Every version of 'org.springframework:spring' and every version of 'org.springframework:spring-core' conflict with each other.
- Every version of 'org.springframework:spring' and every version of 'org.springframework:spring-aop' conflict with each other.
- Every version of 'org.springframework:spring-core' is newer than every version of 'org.springframework:spring'.
- Every version of 'org.springframework:spring-aop' is newer than every version of 'org.springframework:spring'.
- When graph contains:
- all 3 modules, include only 'org.springframework:spring-core' and 'org.springframework:spring-aop'
- 'spring' and 'spring-core', include only 'spring-core'
- 'spring' and 'spring-aop', include only 'spring-aop'
- A replaced by A-api, A-impl
- When dependencies on A and A-api:1.2 are present in the graph, the result should contain A-api:1.2 and A-impl:1.2
- When dependencies on A and A-api:1.2 and A-impl:1.3 are present in the graph, the result should contain A-api:1.3 and A-impl:1.3
uhappy paths:
- cycle between newly added dependency
- dependency added by the mechanism is unresolved
It's an open question whether we want to implement this story. The existing presentation in the reports treats the replacements as conflict resolution which technically is correct because the module replacement declarations are an input to conflict resolution. The current model of a single selection reason attached to the module is not enough for real world use cases. Resulting resolved module is possibly a product of multiple resolution rules and multiple replacement declarations, possibly interleaving. Down the road we need a mechanism that describes/lists all the manipulations done to the module (model rules will address that).
- a:a:1.0 replaces b:b:1.0, the 'dependencies' report shows "a:a:1.0 -> b:b:1.0"
- a:a:1.0 replaces b:b:1.0, the 'dependencyInsight' report shows "a:a:1.0 -> b:b:1.0 (a:a replaces b:b)"
- A replaces B, ResolutionResult object contains correct selection reason for B
- need to use the same version of A-api and A-impl regardless of whether A is in the graph or not. The DSL above doesn't capture this
- what do we do if 2 replacement rules match given component? Which rule should be chosen?
DSL brainstorming, other ideas:
dependencies {
components {
//use 'modules' instead of 'module'
modules(notation).all { SomeModuleMetadata details -> ... }
modules.matching(notation).all { SomeModuleMetadata details -> ... }
modules(notation) {}
//the 'modules' closure is evaluated during the configuration time
//so it declares various information about the modules, this information is consumed during project configuration and later used during resolution
//examples (some of them are far into the future)
modules('org.springframework:spring').all {
replacedBy 'org.springframework:spring-core'
replacedBy 'org.springframework:spring-aop'
}
modules('org:api', 'com:impl').all {
releasableUnit()
//or
requiresConsistentVersion()
//or
consistentVersion()
}
modules { ModuleSelector it -> it.group.startsWith('com.linkedin.') && it.hasSameGroup() }.all {
releasableUnit()
}
modules('com.linkedin.*').all {
releasableUnit()
}
modules { it.name == 'groovy-all' && it.numericVersion >= 2 }.all {
replacedBy 'groovy-core'
replacedBy 'groovy-xml'
releasableUnit() //ensures 'groovy-core' and 'groovy-xml' will have consistent version
}
More brainstorming, based on an existing API for configuring module metadata:
dependencies {
components {
eachComponent { ComponentMetadataDetails details ->
if (details.id.group == 'com.google.google-collections' && details.id.name == 'google-collections') {
details.replacedBy 'com.google.guava:guava'
}
}
eachComponent { ComponentMetadataDetails details ->
if (details.id.group == 'org.springframework' && details.id.name == 'spring') {
details.replacedBy 'org.springframework:spring-core', 'org.springframework:spring-aop'
}
}
eachComponent { ComponentMetadataDetails details ->
if (details.id.group == 'com.foo' && details.id.name == 'foo-impl') {
//api and impl will use consistent version
details.bundledWith 'com.foo:foo-api'
}
}
eachComponent { ComponentMetadataDetails details ->
if (details.id.group.startsWith('com.linkedin.')) {
//all components from the matching group will use consistent version:
details.bundledWith details.id.group
}
}
eachComponent { ComponentMetadataDetails details ->
//all components from the same 'group' will use consistent version:
details.bundledWith details.id.group
}
eachComponent { ComponentMetadataDetails details ->
if (details.id.group == 'org.springframework') {
details.bundledWith 'org.springframework'
if (details.id.name == 'spring') {
details.replacedBy { it.group == 'org.springframework' && it.name.startsWith('spring-') }
}
}
}
}
}