Skip to content

Commit

Permalink
Add generateProjectDependencyGraph Task and ProjectGenerator extensio…
Browse files Browse the repository at this point in the history
…n for proper multi project support.
  • Loading branch information
vanniktech committed Oct 1, 2018
1 parent 49f20ac commit 5ce9fc0
Show file tree
Hide file tree
Showing 14 changed files with 329 additions and 45 deletions.
10 changes: 5 additions & 5 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
buildscript {
ext.kotlinVersion = '1.2.60'
ext.kotlinVersion = '1.2.71'

repositories {
mavenCentral()
Expand Down Expand Up @@ -29,7 +29,7 @@ apply plugin: "com.vanniktech.maven.publish"

codeQualityTools {
ktlint {
toolVersion = '0.27.0'
toolVersion = '0.28.0'
}
detekt {
toolVersion = '1.0.0.RC8'
Expand Down Expand Up @@ -70,10 +70,10 @@ dependencies {
implementation localGroovy()
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
implementation gradleApi()
api "guru.nidi:graphviz-java:0.5.4"
api "guru.nidi:graphviz-java:0.6.0"

testImplementation 'junit:junit:4.12'
testImplementation 'org.assertj:assertj-core:3.10.0'
testImplementation 'org.assertj:assertj-core:3.11.1'
testImplementation 'com.android.tools.build:gradle:3.1.4'
testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
}
Expand All @@ -94,6 +94,6 @@ pluginBundle {
}

wrapper {
gradleVersion = '4.9'
gradleVersion = '4.10.2'
distributionType = Wrapper.DistributionType.ALL
}
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ import guru.nidi.graphviz.attribute.Shape
import guru.nidi.graphviz.model.Factory.graph
import guru.nidi.graphviz.model.Factory.mutGraph
import guru.nidi.graphviz.model.Factory.mutNode
import guru.nidi.graphviz.model.Factory.node
import guru.nidi.graphviz.model.MutableGraph
import guru.nidi.graphviz.model.MutableNode
import org.gradle.api.Project
import org.gradle.api.artifacts.ResolvedDependency

internal class DotGenerator(
internal class DependencyGraphGenerator(
private val project: Project,
private val generator: Generator
) {
Expand Down Expand Up @@ -92,7 +91,7 @@ internal class DotGenerator(

rootNodes.remove(identifier)

nodes[parentIdentifier]?.addLink(nodes[identifier])
nodes[parentIdentifier]?.addLink(mutated)

if (generator.children.invoke(dependency)) {
dependency.children.forEach { append(it, identifier, graph, rootNodes) }
Expand All @@ -111,5 +110,3 @@ internal class DotGenerator(
else -> moduleName
}
}

private val Project.dotIdentifier get() = "$group$name".dotIdentifier
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.vanniktech.dependency.graph.generator

import com.vanniktech.dependency.graph.generator.DependencyGraphGeneratorExtension.Generator.Companion.ALL
import guru.nidi.graphviz.attribute.Label
import guru.nidi.graphviz.engine.Format
import guru.nidi.graphviz.engine.Format.PNG
Expand All @@ -16,10 +15,19 @@ import org.gradle.api.artifacts.ResolvedDependency
* @since 0.1.0
*/
open class DependencyGraphGeneratorExtension {
var generators: List<Generator> = listOf(ALL)
/**
* Generator extensions. By default this will yield a graph showing every project and library dependencies.
* @since 0.1.0
*/
var generators: List<Generator> = listOf(Generator.ALL)

/**
* ProjectGenerator extensions. By default this will yield a graph showing every project and it's project dependencies.
*/
var projectGenerators: List<ProjectGenerator> = listOf(ProjectGenerator.ALL)

/**
* Generator allows you to customize which dependencies you want to select. Further you can tweak some of the formatting.
* Generator allows you to filter and tweak between projects- as well as library dependencies.
* @since 0.1.0
*/
data class Generator @JvmOverloads constructor(
Expand All @@ -36,7 +44,7 @@ open class DependencyGraphGeneratorExtension {
val dependencyNode: (MutableNode, ResolvedDependency) -> MutableNode = { node, _ -> node },
/** Allows to change the node for the given project. */
val projectNode: (MutableNode, Project) -> MutableNode = { node, _ -> node },
/** Optional label that can be displayed wrapped around the graph. */
/** Optional label that can be displayed wrapped around the graph. */
val label: Label? = null,
/** Return true when you want to include this configuration, false otherwise. */
val includeConfiguration: (Configuration) -> Boolean = {
Expand All @@ -61,4 +69,34 @@ open class DependencyGraphGeneratorExtension {
@JvmStatic val ALL = Generator()
}
}

/**
* ProjectGenerator allows you to filter and tweak between projects dependencies.
* @since 0.6.0
*/
data class ProjectGenerator @JvmOverloads constructor(
/**
* The name of this type of generator that should be in lowerCamelCase.
* The task name as well as the output files will use this name.
*/
val name: String = "",
/** Allows to change the node for the given project. */
val projectNode: (MutableNode, Project) -> MutableNode = { node, _ -> node },
/** Return true when you want to include this project, false otherwise. */
val includeProject: (Project) -> Boolean = { true },
/** Return the output formats you'd like to be generated. */
val outputFormats: List<Format> = listOf(PNG, SVG),
/** Allows you to mutate the graph and add things as needed. */
val graph: (MutableGraph) -> MutableGraph = { it }
) {
/** Gradle task name that is associated with this generator. */
val gradleTaskName = "generateProjectDependencyGraph${name.capitalize()}"
internal val outputFileName = "project-dependency-graph${name.toHyphenCase().nonEmptyPrepend("-")}"
internal val outputFileNameDot = "$outputFileName.dot"

companion object {
/** Default behavior which will include everything as is. */
@JvmStatic val ALL = ProjectGenerator()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,23 @@ open class DependencyGraphGeneratorPlugin : Plugin<Project> {
override fun apply(project: Project) {
val extension = project.extensions.create("dependencyGraphGenerator", DependencyGraphGeneratorExtension::class.java)

if (GradleVersion.version(project.gradle.gradleVersion) >= GradleVersion.version("4.9")) {
if (GradleVersion.current() >= GradleVersion.version("4.9")) {
extension.generators.forEach {
project.tasks.register(it.gradleTaskName, DependencyGraphGeneratorTask::class.java, it.configureTask(project))
}

extension.projectGenerators.forEach {
project.tasks.register(it.gradleTaskName, ProjectDependencyGraphGeneratorTask::class.java, it.configureTask(project))
}
} else {
project.afterEvaluate { _ ->
extension.generators.forEach {
project.tasks.create(it.gradleTaskName, DependencyGraphGeneratorTask::class.java, it.configureTask(project))
}

extension.projectGenerators.forEach {
project.tasks.create(it.gradleTaskName, ProjectDependencyGraphGeneratorTask::class.java, it.configureTask(project))
}
}
}
}
Expand All @@ -33,4 +41,16 @@ open class DependencyGraphGeneratorPlugin : Plugin<Project> {
it.outputDirectory = File(project.buildDir, "reports/dependency-graph/")
}
}

private fun DependencyGraphGeneratorExtension.ProjectGenerator.configureTask(project: Project): (ProjectDependencyGraphGeneratorTask) -> Unit {
val name = name.nonEmptyPrepend(" for ")

return {
it.projectGenerator = this
it.group = "reporting"
it.description = "Generates a project dependency graph$name"
it.inputFile = project.buildFile
it.outputDirectory = File(project.buildDir, "reports/project-dependency-graph/")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ open class DependencyGraphGeneratorTask : DefaultTask() {
@OutputDirectory lateinit var outputDirectory: File

@TaskAction fun run() {
val graph = DotGenerator(project, generator).generateGraph()
val graph = DependencyGraphGenerator(project, generator).generateGraph()
File(outputDirectory, generator.outputFileNameDot).writeText(graph.toString())

val graphviz = Graphviz.fromGraph(graph)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.vanniktech.dependency.graph.generator

import guru.nidi.graphviz.attribute.Color
import guru.nidi.graphviz.attribute.Font
import guru.nidi.graphviz.attribute.Label
import guru.nidi.graphviz.attribute.Rank
import guru.nidi.graphviz.attribute.Shape
import guru.nidi.graphviz.attribute.Style
import guru.nidi.graphviz.model.Factory.graph
import guru.nidi.graphviz.model.Factory.mutGraph
import guru.nidi.graphviz.model.Factory.mutNode
import guru.nidi.graphviz.model.Link
import guru.nidi.graphviz.model.MutableGraph
import org.gradle.api.Project
import org.gradle.api.artifacts.ProjectDependency
import java.util.Locale.US

// Based on https://github.com/JakeWharton/SdkSearch/blob/766d612ed52cdf3af9cd0728b6afd87006746ae5/gradle/projectDependencyGraph.gradle
internal class ProjectDependencyGraphGenerator(
private val project: Project,
private val projectGenerator: DependencyGraphGeneratorExtension.ProjectGenerator
) {
fun generateGraph(): MutableGraph {
val graph = mutGraph().setDirected(true)
graph.graphAttrs().add(Label.of(project.name).locate(Label.Location.TOP), Font.size(DEFAULT_FONT_SIZE))
graph.nodeAttrs().add(Font.name("Times New Roman"), Style.FILLED)

val rootProjects = project.allprojects.toMutableList()
val projects = mutableSetOf<Project>()
val dependencies = mutableListOf<ProjectDependencyContainer>()

addProjects(projects, rootProjects, dependencies, graph)
rankRootProjects(graph, projects, rootProjects)
addDependencies(dependencies, graph)

return projectGenerator.graph(graph)
}

@Suppress("Detekt.ComplexMethod") private fun addProjects(projects: MutableSet<Project>, rootProjects: MutableList<Project>, dependencies: MutableList<ProjectDependencyContainer>, graph: MutableGraph) {
project.allprojects
.filter { projectGenerator.includeProject(it) }
.flatMap { project -> project.configurations.map { project to it } }
.flatMap { (project, configuration) ->
configuration.dependencies
.withType(ProjectDependency::class.java)
.map { it.dependencyProject }
.flatMap { projectDependency ->
projects.add(project)
projects.add(projectDependency)

rootProjects.remove(projectDependency)
dependencies.add(ProjectDependencyContainer(project, projectDependency, configuration.name.toLowerCase(US).endsWith("implementation")))
listOf(project, projectDependency)
}
}
.forEach { project ->
val node = mutNode(project.path)

if (rootProjects.contains(project)) {
node.add(Shape.RECTANGLE)
} else if (project.isCommonsProject()) {
node.add(Style.DASHED, Color.BLACK)
}

when {
project.isJsProject() -> node.add(Color.rgb("#fff176").fill())
project.isAndroidProject() -> node.add(Color.rgb("#81c784").fill())
project.isKotlinProject() -> node.add(Color.rgb("#ffb74d").fill())
project.isJavaProject() -> node.add(Color.rgb("#ff8a65").fill())
else -> node.add(Color.rgb("#e0e0e0").fill())
}

graph.add(projectGenerator.projectNode(node, project))
}
}

@Suppress("Detekt.SpreadOperator") private fun rankRootProjects(graph: MutableGraph, projects: MutableSet<Project>, rootProjects: MutableList<Project>) {
graph.add(graph()
.graphAttr()
.with(Rank.SAME)
.with(*projects.filter { rootProjects.contains(it) }.map { mutNode(it.path) }.toTypedArray()))
}

private fun addDependencies(dependencies: MutableList<ProjectDependencyContainer>, graph: MutableGraph) {
dependencies
.filterNot { (from, to, _) -> !from.isCommonsProject() && to.isCommonsProject() }
.forEach { (from, to, isImplementation) ->
val fromNode = graph.nodes().find { it.name().toString() == from.path }
val toNode = graph.nodes().find { it.name().toString() == to.path }

if (fromNode != null && toNode != null) {
val link = Link.to(toNode)
graph.add(fromNode.addLink(if (isImplementation) link.with(Style.DOTTED) else link))
}
}
}

internal data class ProjectDependencyContainer(
val from: Project,
val to: Project,
val isImplementation: Boolean
)

internal companion object {
const val DEFAULT_FONT_SIZE = 35
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.vanniktech.dependency.graph.generator

import com.vanniktech.dependency.graph.generator.DependencyGraphGeneratorExtension.ProjectGenerator
import guru.nidi.graphviz.engine.Graphviz
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import java.io.File

open class ProjectDependencyGraphGeneratorTask : DefaultTask() {
lateinit var projectGenerator: ProjectGenerator // TODO does this need to be an input? Quick testing shows no.
@InputFile lateinit var inputFile: File

@OutputDirectory lateinit var outputDirectory: File

@TaskAction fun run() {
val graph = ProjectDependencyGraphGenerator(project, projectGenerator).generateGraph()
File(outputDirectory, projectGenerator.outputFileNameDot).writeText(graph.toString())

val graphviz = Graphviz.fromGraph(graph)

projectGenerator.outputFormats.forEach {
graphviz.render(it).toFile(File(outputDirectory, projectGenerator.outputFileName))
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.vanniktech.dependency.graph.generator

import org.gradle.api.Project

private val whitespaceRegex = Regex("\\s")

internal val String.dotIdentifier get() = replace("-", "")
Expand All @@ -17,3 +19,15 @@ internal fun String.toHyphenCase(): String {
.drop(1)
.joinToString(separator = "") { if (it[0].isUpperCase()) "-${it[0].toLowerCase()}" else it }
}

internal val Project.dotIdentifier get() = "$group$name".dotIdentifier

fun Project.isJavaProject() = listOf("java-library", "java", "java-gradle-plugin").any { plugins.hasPlugin(it) }

fun Project.isKotlinProject() = listOf("kotlin", "kotlin-android", "kotlin-platform-jvm").any { plugins.hasPlugin(it) }

fun Project.isAndroidProject() = listOf("com.android.library", "com.android.application", "com.android.test", "com.android.feature", "com.android.instantapp").any { plugins.hasPlugin(it) }

fun Project.isJsProject() = plugins.hasPlugin("kotlin2js")

fun Project.isCommonsProject() = plugins.hasPlugin("org.jetbrains.kotlin.platform.common")
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.vanniktech.dependency.graph.generator

import com.vanniktech.dependency.graph.generator.DependencyGraphGeneratorExtension.Generator.Companion.ALL
import com.vanniktech.dependency.graph.generator.DependencyGraphGeneratorExtension.Generator
import com.vanniktech.dependency.graph.generator.DependencyGraphGeneratorExtension.ProjectGenerator
import org.assertj.core.api.Java6Assertions.assertThat
import org.junit.Test

class DependencyGraphGeneratorExtensionTest {
@Test fun defaults() {
val defaults = DependencyGraphGeneratorExtension()
assertThat(defaults.generators).containsExactly(ALL)
assertThat(defaults.generators).containsExactly(Generator.ALL)
assertThat(defaults.projectGenerators).containsExactly(ProjectGenerator.ALL)
}
}
Loading

0 comments on commit 5ce9fc0

Please sign in to comment.