Dependency Graph Resolution
The output of the graph resolution phase is a fully resolved dependency graph, which is used as the input to the artifact resolution phase.
You can learn about how the graph is constructed in Understanding the Dependency Resolution Model.
The ResolutionResult
API provides access to the resolved dependency graph without triggering artifact resolution.
This API presents the resolved dependency graph, where each node in the graph is a variant of a component.
Raw access to the dependency graph can be useful for a number of use cases:
-
Visualizing the dependency graph, for example generating a
.dot
file for Graphviz. -
Exposing diagnostics about a given resolution, similar to the
dependencies
ordependencyInsight
tasks. -
Resolving a subset of the artifacts for a dependency graph when used in conjunction with the
ArtifactView
API.
Consider the following function that traverses a dependency graph, starting from the root node. Callbacks are notified for each node and edge in the graph. This function can be used as a base for any use case that requires traversing a dependency graph:
Kotlin
Groovy
void traverseGraph(
ResolvedComponentResult rootComponent,
ResolvedVariantResult rootVariant,
Consumer<ResolvedVariantResult> nodeCallback,
BiConsumer<ResolvedVariantResult, ResolvedVariantResult> edgeCallback
) {
Set<ResolvedVariantResult> seen = new HashSet<>()
seen.add(rootVariant)
nodeCallback(rootVariant)
def queue = new ArrayDeque<Tuple2<ResolvedVariantResult, ResolvedComponentResult>>()
queue.add(new Tuple2(rootVariant, rootComponent))
while (!queue.isEmpty()) {
def entry = queue.removeFirst()
def variant = entry.v1
def component = entry.v2
// Traverse this variant's dependencies
component.getDependenciesForVariant(variant).each { dependency ->
if (dependency instanceof UnresolvedDependencyResult) {
throw dependency.failure
}
if ((!dependency instanceof ResolvedDependencyResult)) {
throw new RuntimeException("Unknown dependency type: $dependency")
}
def resolved = dependency as ResolvedDependencyResult
if (!dependency.constraint) {
def toVariant = resolved.resolvedVariant
if (seen.add(toVariant)) {
nodeCallback(toVariant)
queue.add(new Tuple2(toVariant, resolved.selected))
}
edgeCallback(variant, toVariant)
}
}
}
}
This function starts at the root variant, and performs a breadth-first traversal of the graph.
The ResolutionResult
API is lenient, so it is important to check whether a visited edge is unresolved (failed) or resolved.
With this function, the node callback is always called before the edge callback for any given node.
Below, we leverage the above traversal function to transform a dependency graph into a .dot
file for visualization:
Kotlin
Groovy
abstract class GenerateDot extends DefaultTask {
@Input
abstract Property<ResolvedComponentResult> getRootComponent()
@Input
abstract Property<ResolvedVariantResult> getRootVariant()
@TaskAction
void traverse() {
println("digraph {")
traverseGraph(
rootComponent.get(),
rootVariant.get(),
node -> { println(" ${toNodeId(node)} [shape=box]") },
(from, to) -> { println(" ${toNodeId(from)} -> ${toNodeId(to)}") }
)
println("}")
}
String toNodeId(ResolvedVariantResult variant) {
return "\"${variant.owner.displayName}:${variant.displayName}\""
}
}
A proper implementation would not use println but would write to an output file. For more details on declaring task inputs and outputs, see the Writing Tasks section.
|
When we register the task, we use the ResolutionResult
API to access the root component and root variant of the runtimeClasspath
configuration:
Kotlin
Groovy
tasks.register("generateDot", GenerateDot) {
rootComponent = configurations.runtimeClasspath.incoming.resolutionResult.rootComponent
rootVariant = configurations.runtimeClasspath.incoming.resolutionResult.rootVariant
}
This example uses incubating APIs. |
Running this task, we get the following output:
digraph { "root project ::runtimeClasspath" [shape=box] "com.google.guava:guava:33.2.1-jre:jreRuntimeElements" [shape=box] "root project ::runtimeClasspath" -> "com.google.guava:guava:33.2.1-jre:jreRuntimeElements" "com.google.guava:failureaccess:1.0.2:runtime" [shape=box] "com.google.guava:guava:33.2.1-jre:jreRuntimeElements" -> "com.google.guava:failureaccess:1.0.2:runtime" "com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava:runtime" [shape=box] "com.google.guava:guava:33.2.1-jre:jreRuntimeElements" -> "com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava:runtime" "com.google.code.findbugs:jsr305:3.0.2:runtime" [shape=box] "com.google.guava:guava:33.2.1-jre:jreRuntimeElements" -> "com.google.code.findbugs:jsr305:3.0.2:runtime" "org.checkerframework:checker-qual:3.42.0:runtimeElements" [shape=box] "com.google.guava:guava:33.2.1-jre:jreRuntimeElements" -> "org.checkerframework:checker-qual:3.42.0:runtimeElements" "com.google.errorprone:error_prone_annotations:2.26.1:runtime" [shape=box] "com.google.guava:guava:33.2.1-jre:jreRuntimeElements" -> "com.google.errorprone:error_prone_annotations:2.26.1:runtime" }
Compare this to the output of the dependencies
task:
runtimeClasspath \--- com.google.guava:guava:33.2.1-jre +--- com.google.guava:failureaccess:1.0.2 +--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava +--- com.google.code.findbugs:jsr305:3.0.2 +--- org.checkerframework:checker-qual:3.42.0 \--- com.google.errorprone:error_prone_annotations:2.26.1
Notice how the graph is the same for both representations.
Next Step: Learn about Artifact Resolution >>