Skip to content

Instantly share code, notes, and snippets.

@marcRDZ
Last active June 30, 2025 08:51
Show Gist options
  • Save marcRDZ/01deb6ddb97aef2d3c43bce7b65a3efb to your computer and use it in GitHub Desktop.
Save marcRDZ/01deb6ddb97aef2d3c43bce7b65a3efb to your computer and use it in GitHub Desktop.
Kotlin plugin to generate project dependency graph migrated from: https://github.com/JakeWharton/SdkSearch/blob/master/gradle/projectDependencyGraph.gradle
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.ProjectDependency
import java.io.File
class ProjectDependencyGraphScriptPlugin : Plugin<Project> {
override fun apply(target: Project) {
target.tasks.register("projectDependencyGraph") {
doLast {
val rootProject = target.rootProject
val dotFile = File(rootProject.layout.buildDirectory.asFile.get(), "reports/dependency-graph/project.dot")
dotFile.parentFile.mkdirs()
dotFile.delete()
val dotContent = StringBuilder()
dotContent.append("digraph {\n")
dotContent.append(" graph [label=\"${rootProject.name}\\n \",labelloc=t,fontsize=30,ranksep=1.4];\n")
dotContent.append(" node [style=filled, fillcolor=\"#bbbbbb\"];\n")
dotContent.append(" rankdir=TB;\n")
val allProjectsInOrder = mutableListOf<Project>()
val projectQueue = mutableListOf(rootProject)
while (projectQueue.isNotEmpty()) {
val currentProject = projectQueue.removeAt(0)
allProjectsInOrder.add(currentProject)
projectQueue.addAll(currentProject.childProjects.values)
}
val projectsInGraph = mutableSetOf<Project>()
val dependenciesMap = mutableMapOf<Pair<Project, Project>, MutableList<String>>()
val androidAppProjects = mutableSetOf<Project>()
val androidLibraryProjects = mutableSetOf<Project>()
val javaProjects = mutableSetOf<Project>()
// Determine root projects (projects that are not dependencies of any other project in the graph)
// This is a bit different from the original logic which removed from a list of all projects.
// This approach identifies projects that are never a 'dependencyProject'.
val rootProjectsInGraph = mutableSetOf<Project>()
allProjectsInOrder.forEach { rootProjectsInGraph.add(it) } // Assume all are roots initially
val processingQueue = mutableListOf(rootProject)
val visitedProjects = mutableSetOf<Project>() // To avoid processing projects multiple times if childProjects has overlaps
while (processingQueue.isNotEmpty()) {
val project = processingQueue.removeAt(0)
if (visitedProjects.contains(project)) {
continue
}
visitedProjects.add(project)
processingQueue.addAll(project.childProjects.values)
if (project.plugins.hasPlugin("com.android.application")) {
androidAppProjects.add(project)
}
if (project.plugins.hasPlugin("com.android.library")) {
androidLibraryProjects.add(project)
}
if (project.plugins.hasPlugin("java-library") || project.plugins.hasPlugin("java")) { // Groovy 'java' plugin is 'org.gradle.java' or 'java-library'
javaProjects.add(project)
}
project.configurations.forEach { config ->
config.dependencies
.withType(ProjectDependency::class.java)
.map { it.dependencyProject }
.forEach { dependencyProject ->
projectsInGraph.add(project)
projectsInGraph.add(dependencyProject)
rootProjectsInGraph.remove(dependencyProject) // If it's a dependency, it's not a root
val graphKey = Pair(project, dependencyProject)
val traits = dependenciesMap.getOrPut(graphKey) { mutableListOf() }
if (config.name.lowercase().endsWith("implementation")) { // toLowerCase() in Kotlin
traits.add("style=dotted")
}
}
}
}
val sortedProjectsInGraph = projectsInGraph.sortedBy { it.path }
dotContent.append("\n # Projects\n\n")
for (project in sortedProjectsInGraph) {
val traits = mutableListOf<String>()
// Check if this project is one of the identified root projects in the graph
if (rootProjectsInGraph.contains(project) && projectsInGraph.contains(project)) {
// Only add shape=box if it's truly a root AND part of the rendered graph
traits.add("shape=box")
}
when {
androidAppProjects.contains(project) -> traits.add("fillcolor=\"#ffd2b3\"")
androidLibraryProjects.contains(project) -> traits.add("fillcolor=\"#baffc9\"")
javaProjects.contains(project) -> traits.add("fillcolor=\"#ffb3ba\"")
else -> traits.add("fillcolor=\"#eeeeee\"")
}
dotContent.append(" \"${project.path}\" [${traits.joinToString(", ")}];\n")
}
dotContent.append("\n {rank = same;")
for (project in sortedProjectsInGraph) {
// Check if this project is one of the identified root projects in the graph
if (rootProjectsInGraph.contains(project) && projectsInGraph.contains(project)) {
dotContent.append(" \"${project.path}\";")
}
}
dotContent.append("}\n")
dotContent.append("\n # Dependencies\n\n")
dependenciesMap.forEach { (key, traits) ->
dotContent.append(" \"${key.first.path}\" -> \"${key.second.path}\"")
if (traits.isNotEmpty()) {
dotContent.append(" [${traits.joinToString(", ")}]")
}
dotContent.append("\n")
}
dotContent.append("}\n")
dotFile.writeText(dotContent.toString()) // More idiomatic Kotlin for writing text to a file
// Export to png (requires Graphviz installed on OS)
val process = ProcessBuilder("dot", "-Tpng", "-O", "project.dot")
.directory(dotFile.parentFile)
.redirectErrorStream(true) // Combine stdout and stderr
.start()
val exitCode = process.waitFor() // Returns the exit code
if (exitCode != 0) {
val errors = process.inputStream.bufferedReader().readText()
throw RuntimeException("dot command failed with exit code $exitCode:\n$errors")
}
println("Project module dependency graph created at ${File(dotFile.parentFile, "project.dot.png").absolutePath}")
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment