Last active
June 30, 2025 08:51
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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