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.
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:
fun traverseGraph(
rootComponent: ResolvedComponentResult,
rootVariant: ResolvedVariantResult,
nodeCallback: (ResolvedVariantResult) -> Unit,
edgeCallback: (ResolvedVariantResult, ResolvedVariantResult) -> Unit
) {
val seen = mutableSetOf<ResolvedVariantResult>(rootVariant)
nodeCallback(rootVariant)
val queue = ArrayDeque(listOf(rootVariant to rootComponent))
while (queue.isNotEmpty()) {
val (variant, component) = queue.removeFirst()
// Traverse this variant's dependencies
component.getDependenciesForVariant(variant).forEach { dependency ->
val resolved = when (dependency) {
is ResolvedDependencyResult -> dependency
is UnresolvedDependencyResult -> throw dependency.failure
else -> throw AssertionError("Unknown dependency type: $dependency")
}
if (!resolved.isConstraint) {
val toVariant = resolved.resolvedVariant
if (seen.add(toVariant)) {
nodeCallback(toVariant)
queue.addLast(toVariant to resolved.selected)
}
edgeCallback(variant, toVariant)
}
}
}
}
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:
abstract class GenerateDot : DefaultTask() {
@get:Input
abstract val rootComponent: Property<ResolvedComponentResult>
@get:Input
abstract val rootVariant: Property<ResolvedVariantResult>
@TaskAction
fun traverse() {
println("digraph {")
traverseGraph(
rootComponent.get(),
rootVariant.get(),
{ node -> println(" ${toNodeId(node)} [shape=box]") },
{ from, to -> println(" ${toNodeId(from)} -> ${toNodeId(to)}") }
)
println("}")
}
fun toNodeId(variant: ResolvedVariantResult): String {
return "\"${variant.owner.displayName}:${variant.displayName}\""
}
}
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:
tasks.register<GenerateDot>("generateDot") {
rootComponent = runtimeClasspath.flatMap {
it.incoming.resolutionResult.rootComponent
}
rootVariant = runtimeClasspath.flatMap {
it.incoming.resolutionResult.rootVariant
}
}
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.