From 6e8d8d5245adad6f79b2eb74c2fd7c888dbbb172 Mon Sep 17 00:00:00 2001 From: Tony Robalik Date: Mon, 2 Dec 2024 14:13:21 -0800 Subject: [PATCH] feat: improvements relating to generating project graphs. 1. improve output of ProjectGraphTask. 2. generate merged graph of all variants in a project. 3. generate merged graph for entire build. 4. incubating support for generating work plan. --- .gitignore | 1 + .../com/autonomousapps/AbstractProject.groovy | 3 + .../autonomousapps/jvm/WorkPlanSpec.groovy | 26 ++++++ .../jvm/projects/WorkPlanProject.groovy | 91 +++++++++++++++++++ .../autonomousapps/internal/OutputPaths.kt | 4 + .../internal/artifacts/DagpArtifacts.kt | 1 + .../internal/graph/GraphWriter.kt | 69 +++++++++++++- .../autonomousapps/internal/graph/graphs.kt | 65 +++++++++++++ .../autonomousapps/internal/utils/moshi.kt | 62 +++++++++---- .../internal/utils/project/projects.kt | 18 ++++ .../com/autonomousapps/model/Coordinates.kt | 12 +++ .../model/internal/DependencyGraphView.kt | 9 +- .../autonomousapps/subplugin/ProjectPlugin.kt | 57 ++++++++---- .../autonomousapps/subplugin/RootPlugin.kt | 29 ++++-- .../autonomousapps/tasks/ComputeAdviceTask.kt | 5 +- .../tasks/ComputeDominatorTreeTask.kt | 25 +++-- .../tasks/GenerateProjectGraphTask.kt | 78 ++++++++++++++++ .../autonomousapps/tasks/GenerateWorkPlan.kt | 75 +++++++++++++++ .../com/autonomousapps/tasks/GraphViewTask.kt | 5 +- .../tasks/MergeProjectGraphsTask.kt | 36 ++++++++ .../autonomousapps/tasks/ProjectGraphTask.kt | 70 +++++++------- .../model/internal/DependencyGraphViewTest.kt | 2 +- 22 files changed, 639 insertions(+), 104 deletions(-) create mode 100644 src/functionalTest/groovy/com/autonomousapps/jvm/WorkPlanSpec.groovy create mode 100644 src/functionalTest/groovy/com/autonomousapps/jvm/projects/WorkPlanProject.groovy create mode 100644 src/main/kotlin/com/autonomousapps/internal/graph/graphs.kt create mode 100644 src/main/kotlin/com/autonomousapps/internal/utils/project/projects.kt create mode 100644 src/main/kotlin/com/autonomousapps/tasks/GenerateProjectGraphTask.kt create mode 100644 src/main/kotlin/com/autonomousapps/tasks/GenerateWorkPlan.kt create mode 100644 src/main/kotlin/com/autonomousapps/tasks/MergeProjectGraphsTask.kt diff --git a/.gitignore b/.gitignore index 3c7ebe98d..b8fa486f0 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ local.properties .DS_STORE .bash_history +*.salive diff --git a/src/functionalTest/groovy/com/autonomousapps/AbstractProject.groovy b/src/functionalTest/groovy/com/autonomousapps/AbstractProject.groovy index 1a4e774d8..65f0e29de 100644 --- a/src/functionalTest/groovy/com/autonomousapps/AbstractProject.groovy +++ b/src/functionalTest/groovy/com/autonomousapps/AbstractProject.groovy @@ -26,6 +26,9 @@ abstract class AbstractProject extends AbstractGradleProject { /** Applies the 'java-library' and 'com.autonomousapps.dependency-analysis' plugins. */ protected static final List javaLibrary = [Plugin.javaLibrary, Plugins.dependencyAnalysisNoVersion] + /** Applies the 'java', 'application', and 'com.autonomousapps.dependency-analysis' plugins. */ + protected static final List javaApp = [Plugin.java, Plugin.application, Plugins.dependencyAnalysisNoVersion] + protected final DependencyProvider dependencies protected final PluginProvider plugins diff --git a/src/functionalTest/groovy/com/autonomousapps/jvm/WorkPlanSpec.groovy b/src/functionalTest/groovy/com/autonomousapps/jvm/WorkPlanSpec.groovy new file mode 100644 index 000000000..99fb37abf --- /dev/null +++ b/src/functionalTest/groovy/com/autonomousapps/jvm/WorkPlanSpec.groovy @@ -0,0 +1,26 @@ +package com.autonomousapps.jvm + +import com.autonomousapps.jvm.projects.WorkPlanProject +import spock.lang.PendingFeature + +import static com.autonomousapps.utils.Runner.build +import static com.google.common.truth.Truth.assertThat + +final class WorkPlanSpec extends AbstractJvmSpec { + + @PendingFeature + def "can generate work plan (#gradleVersion)"() { + given: + def project = new WorkPlanProject() + gradleProject = project.gradleProject + + when: + build(gradleVersion, gradleProject.rootDir, ':generateWorkPlan') + + then: + assertThat(true).isFalse() + + where: + gradleVersion << gradleVersions() + } +} diff --git a/src/functionalTest/groovy/com/autonomousapps/jvm/projects/WorkPlanProject.groovy b/src/functionalTest/groovy/com/autonomousapps/jvm/projects/WorkPlanProject.groovy new file mode 100644 index 000000000..04e20cb23 --- /dev/null +++ b/src/functionalTest/groovy/com/autonomousapps/jvm/projects/WorkPlanProject.groovy @@ -0,0 +1,91 @@ +package com.autonomousapps.jvm.projects + +import com.autonomousapps.AbstractProject +import com.autonomousapps.kit.GradleProject + +import static com.autonomousapps.kit.gradle.Dependency.implementation +import static com.autonomousapps.kit.gradle.Dependency.project + +final class WorkPlanProject extends AbstractProject { + + final GradleProject gradleProject + + WorkPlanProject() { + this.gradleProject = build() + } + + private GradleProject build() { + return newGradleProjectBuilder() + // alpha + .withSubproject('alpha:app') { p -> + p.withBuildScript { bs -> + bs.plugins = javaApp + plugins.javaTestFixtures + bs.dependencies = [ + project('implementation', ':alpha:lib'), + project('implementation', ':beta:app').onTestFixtures(), + ] + } + } + .withSubproject('alpha:lib') { p -> + p.withBuildScript { bs -> + bs.plugins = javaLibrary + bs.dependencies = [ + project('api', ':alpha:core'), + ] + } + } + .withSubproject('alpha:core') { p -> + p.withBuildScript { bs -> + bs.plugins = javaLibrary + bs.dependencies(implementation('com.squareup.misk:misk:2023.10.18.080259-adcfb84')) + } + } + + // beta + .withSubproject('beta:app') { p -> + p.withBuildScript { bs -> + bs.plugins = javaApp + plugins.javaTestFixtures + bs.dependencies = [ + project('implementation', ':beta:lib'), + ] + } + } + .withSubproject('beta:lib') { p -> + p.withBuildScript { bs -> + bs.plugins = javaLibrary + bs.dependencies = [ + project('api', ':beta:core'), + ] + } + } + .withSubproject('beta:core') { p -> + p.withBuildScript { bs -> + bs.plugins = javaLibrary + } + } + + // gamma + .withSubproject('gamma:app') { p -> + p.withBuildScript { bs -> + bs.plugins = javaApp + plugins.javaTestFixtures + bs.dependencies = [ + project('implementation', ':gamma:lib'), + ] + } + } + .withSubproject('gamma:lib') { p -> + p.withBuildScript { bs -> + bs.plugins = javaLibrary + bs.dependencies = [ + project('api', ':gamma:core'), + ] + } + } + .withSubproject('gamma:core') { p -> + p.withBuildScript { bs -> + bs.plugins = javaLibrary + } + } + .write() + } +} diff --git a/src/main/kotlin/com/autonomousapps/internal/OutputPaths.kt b/src/main/kotlin/com/autonomousapps/internal/OutputPaths.kt index 6bb048146..4b0efd957 100644 --- a/src/main/kotlin/com/autonomousapps/internal/OutputPaths.kt +++ b/src/main/kotlin/com/autonomousapps/internal/OutputPaths.kt @@ -80,6 +80,7 @@ internal class NoVariantOutputPaths(private val project: Project) { val locationsPath = file("$ROOT_DIR/declarations.json") val resolvedDepsPath = file("$ROOT_DIR/resolved-dependencies-report.txt") + val mergedProjectGraphPath = file("$ROOT_DIR/merged-project-graph.json") /* * Advice-related tasks. @@ -99,11 +100,14 @@ internal class NoVariantOutputPaths(private val project: Project) { internal class RootOutputPaths(private val project: Project) { private fun file(path: String): Provider = project.layout.buildDirectory.file(path) + private fun dir(path: String): Provider = project.layout.buildDirectory.dir(path) val duplicateDependenciesPath = file("$ROOT_DIR/duplicate-dependencies-report.json") val buildHealthPath = file("$ROOT_DIR/build-health-report.json") val consoleReportPath = file("$ROOT_DIR/build-health-report.txt") val shouldFailPath = file("$ROOT_DIR/should-fail.txt") + + val workPlanDir = dir("$ROOT_DIR/work-plan") } internal class RedundantSubPluginOutputPaths(private val project: Project) { diff --git a/src/main/kotlin/com/autonomousapps/internal/artifacts/DagpArtifacts.kt b/src/main/kotlin/com/autonomousapps/internal/artifacts/DagpArtifacts.kt index 723d3af99..415a683f9 100644 --- a/src/main/kotlin/com/autonomousapps/internal/artifacts/DagpArtifacts.kt +++ b/src/main/kotlin/com/autonomousapps/internal/artifacts/DagpArtifacts.kt @@ -24,6 +24,7 @@ internal interface DagpArtifacts : Named { val declarableName: String, val artifactName: String, ) { + COMBINED_GRAPH("combinedGraph", "combined-graph"), PROJECT_HEALTH("projectHealth", "project-health"), RESOLVED_DEPS("resolvedDeps", "resolved-deps"), } diff --git a/src/main/kotlin/com/autonomousapps/internal/graph/GraphWriter.kt b/src/main/kotlin/com/autonomousapps/internal/graph/GraphWriter.kt index af3b31866..27ec092e4 100644 --- a/src/main/kotlin/com/autonomousapps/internal/graph/GraphWriter.kt +++ b/src/main/kotlin/com/autonomousapps/internal/graph/GraphWriter.kt @@ -2,18 +2,25 @@ // SPDX-License-Identifier: Apache-2.0 package com.autonomousapps.internal.graph +import com.autonomousapps.graph.Graphs.root +import com.autonomousapps.graph.Graphs.shortestPaths +import com.autonomousapps.graph.Graphs.topological import com.autonomousapps.internal.utils.appendReproducibleNewLine import com.autonomousapps.model.Coordinates import com.autonomousapps.model.ProjectCoordinates import com.google.common.graph.Graph +import com.google.common.graph.Traverser @Suppress("UnstableApiUsage") -internal object GraphWriter { +internal class GraphWriter(private val buildPath: String) { - fun toDot(graph: Graph) = buildString { - val projectNodes = graph.nodes() + fun toDot(graph: Graph): String = buildString { + val projectNodes = graph.nodes().asSequence() + // Maybe transform an IncludedBuildCoordinates into its resolvedProject for more human-readable reporting + .map { it.maybeProjectCoordinates(buildPath) } .filterIsInstance() .map { it.gav() } + .toList() appendReproducibleNewLine("strict digraph DependencyGraph {") appendReproducibleNewLine(" ratio=0.6;") @@ -28,8 +35,8 @@ internal object GraphWriter { // the graph itself graph.edges().forEach { edge -> - val source = edge.nodeU() - val target = edge.nodeV() + val source = edge.nodeU().maybeProjectCoordinates(buildPath) + val target = edge.nodeV().maybeProjectCoordinates(buildPath) val style = if (source is ProjectCoordinates && target is ProjectCoordinates) " [style=bold color=\"#FF6347\" weight=8]" else "" @@ -38,4 +45,56 @@ internal object GraphWriter { } append("}") } + + /** + * Returns the [graph] sorted into topological order, ascending. Each node in the graph is paired with its depth. + * + * TODO(tsr): shortest path is wrong, I need the _longest_ path. + */ + fun topological(graph: Graph): String { + val root = graph.root() + val top = graph.topological(root) + // val paths = graph.shortestPaths(root) + + return buildString { + top.forEach { node -> + appendLine(node.maybeProjectCoordinates(buildPath).gav()) + // append(" ") + // appendLine("${paths.distanceTo(node)}") + } + } + } + + // TODO replace with Hu's algorithm (for scheduling concurrent work), and push relevant bits into Graphs.kt. + fun workPlan(graph: Graph): String { + val traverser = Traverser.forGraph(graph) + val topological = graph.topological(graph.root()).toList() + val plan = mutableListOf>() + + var list = mutableListOf(topological.first()) + topological + .drop(1) + .forEach { node -> + val hasPathTo = list.any { traverser.breadthFirst(it).contains(node) } + if (!hasPathTo) { + list.add(node) + } else { + plan.add(list) + list = mutableListOf(node) + } + } + + // add final list + plan.add(list) + + return buildString { + plan.forEachIndexed { i, batch -> + appendLine("$i (${batch.size} items)") + batch.forEach { node -> + append(" ") + appendLine(node.maybeProjectCoordinates(buildPath).gav()) + } + } + } + } } diff --git a/src/main/kotlin/com/autonomousapps/internal/graph/graphs.kt b/src/main/kotlin/com/autonomousapps/internal/graph/graphs.kt new file mode 100644 index 000000000..9cc717086 --- /dev/null +++ b/src/main/kotlin/com/autonomousapps/internal/graph/graphs.kt @@ -0,0 +1,65 @@ +@file:Suppress("UnstableApiUsage") + +package com.autonomousapps.internal.graph + +import com.autonomousapps.model.Coordinates +import com.autonomousapps.model.IncludedBuildCoordinates +import com.autonomousapps.model.ProjectCoordinates +import com.google.common.graph.ElementOrder +import com.google.common.graph.Graph +import com.google.common.graph.GraphBuilder +import com.google.common.graph.ImmutableGraph + +internal operator fun Graph.plus(other: Graph): Graph where T : Any { + val builder = newGraphBuilder() + + nodes().forEach { builder.addNode(it) } + edges().forEach { edge -> builder.putEdge(edge.source(), edge.target()) } + other.nodes().forEach { builder.addNode(it) } + other.edges().forEach { edge -> builder.putEdge(edge.source(), edge.target()) } + + return builder.build() +} + +/** + * Flattens a graph by stripping out the + * [GradleVariantIdentification][com.autonomousapps.model.GradleVariantIdentification], which essentially combines nodes + * to different variants of a module into a single node. This simplifies reporting on certain scenarios where the module + * itself is the unit of analysis, rather than the variant. + */ +internal fun Graph.stripVariants(buildPath: String): Graph { + val builder = newGraphBuilder() + + nodes().forEach { builder.addNode(it.maybeProjectCoordinates(buildPath).flatten()) } + edges().forEach { edge -> + val source = edge.source().maybeProjectCoordinates(buildPath).flatten() + val target = edge.target().maybeProjectCoordinates(buildPath).flatten() + + // In the un-flattened graphs, self-loops are sort of possible because nodes with different capabilities are + // different. + if (source != target) { + builder.putEdge(source, target) + } + } + + return builder.build() +} + +internal fun newGraphBuilder(): ImmutableGraph.Builder { + return GraphBuilder.directed() + .allowsSelfLoops(false) + .incidentEdgeOrder(ElementOrder.stable()) + .immutable() +} + +/** + * Might transform [this][Coordinates] into [ProjectCoordinates], if it is an [IncludedBuildCoordinates] that is from + * "this" build (with buildPath == [buildPath]). + */ +internal fun Coordinates.maybeProjectCoordinates(buildPath: String): Coordinates { + return if (this is IncludedBuildCoordinates && isForBuild(buildPath)) resolvedProject else this +} + +private fun Coordinates.flatten(): Coordinates { + return if (this is ProjectCoordinates) flatten() else this +} diff --git a/src/main/kotlin/com/autonomousapps/internal/utils/moshi.kt b/src/main/kotlin/com/autonomousapps/internal/utils/moshi.kt index 69e7e81be..adb5524f7 100644 --- a/src/main/kotlin/com/autonomousapps/internal/utils/moshi.kt +++ b/src/main/kotlin/com/autonomousapps/internal/utils/moshi.kt @@ -1,12 +1,13 @@ // Copyright (c) 2024. Tony Robalik. // SPDX-License-Identifier: Apache-2.0 @file:JvmName("MoshiUtils") +@file:Suppress("UnstableApiUsage") package com.autonomousapps.internal.utils import com.autonomousapps.model.Coordinates -import com.autonomousapps.model.internal.DependencyGraphView import com.autonomousapps.model.declaration.Variant +import com.autonomousapps.model.internal.DependencyGraphView import com.google.common.graph.Graph import com.squareup.moshi.* import com.squareup.moshi.Types.newParameterizedType @@ -22,7 +23,7 @@ const val prettyJsonIndent = " " val MOSHI: Moshi by lazy { Moshi.Builder() - .add(GraphViewAdapter()) + .add(GraphAdapter()) .add(MoshiSealedJsonAdapterFactory()) .add(TypeAdapters()) .addLast(KotlinJsonAdapterFactory()) @@ -181,15 +182,15 @@ inline fun File.bufferWriteJsonSet(set: Set, indent: String = noJ } /** - * Buffers writes of the set to disk, using the indent to make the output human-readable. + * Buffers writes of the object to disk, using the indent to make the output human-readable. * By default, the output is compacted. * - * @param set The set to write to file + * @param obj The object to write to file * @param indent The indent to control how the result is formatted */ -inline fun File.bufferWriteJson(set: T, indent: String = noJsonIndent) { +inline fun File.bufferWriteJson(obj: T, indent: String = noJsonIndent) { JsonWriter.of(sink().buffer()).use { writer -> - getJsonAdapter().indent(indent).toJson(writer, set) + getJsonAdapter().indent(indent).toJson(writer, obj) } } @@ -211,17 +212,19 @@ internal class TypeAdapters { @FromJson fun fileFromJson(absolutePath: String): File = File(absolutePath) } -@Suppress("unused", "UnstableApiUsage") -internal class GraphViewAdapter { +@Suppress("unused") +internal class GraphAdapter { @ToJson fun graphViewToJson(graphView: DependencyGraphView): GraphViewJson { return GraphViewJson( variant = graphView.variant, configurationName = graphView.configurationName, - nodes = graphView.graph.nodes(), - edges = graphView.graph.edges().asSequence().map { pair -> - pair.nodeU() to pair.nodeV() - }.toSet() + graphJson = GraphJson( + nodes = graphView.graph.nodes(), + edges = graphView.graph.edges().asSequence().map { pair -> + pair.nodeU() to pair.nodeV() + }.toSet(), + ), ) } @@ -229,11 +232,20 @@ internal class GraphViewAdapter { return DependencyGraphView( variant = json.variant, configurationName = json.configurationName, - graph = jsonToGraph(json) + graph = jsonToGraph(json), ) } - private fun jsonToGraph(json: GraphViewJson): Graph { + @ToJson fun graphToJson(graph: Graph): GraphJson { + return GraphJson( + nodes = graph.nodes(), + edges = graph.edges().asSequence().map { pair -> + pair.nodeU() to pair.nodeV() + }.toSet(), + ) + } + + @FromJson fun jsonToGraph(json: GraphJson): Graph { val graphBuilder = DependencyGraphView.newGraphBuilder() json.nodes.forEach { graphBuilder.addNode(it) } json.edges.forEach { (source, target) -> graphBuilder.putEdge(source, target) } @@ -241,16 +253,32 @@ internal class GraphViewAdapter { return graphBuilder.build() } + private fun jsonToGraph(json: GraphViewJson): Graph { + val graphBuilder = DependencyGraphView.newGraphBuilder() + json.graphJson.nodes.forEach { graphBuilder.addNode(it) } + json.graphJson.edges.forEach { (source, target) -> graphBuilder.putEdge(source, target) } + + return graphBuilder.build() + } + + private infix fun Coordinates.to(target: Coordinates) = EdgeJson(this, target) + + @JsonClass(generateAdapter = false) + internal data class GraphContainer(val graph: Graph) + @JsonClass(generateAdapter = false) internal data class GraphViewJson( val variant: Variant, val configurationName: String, + val graphJson: GraphJson, + ) + + @JsonClass(generateAdapter = false) + internal data class GraphJson( val nodes: Set, - val edges: Set + val edges: Set, ) @JsonClass(generateAdapter = false) internal data class EdgeJson(val source: Coordinates, val target: Coordinates) - - private infix fun Coordinates.to(target: Coordinates) = EdgeJson(this, target) } diff --git a/src/main/kotlin/com/autonomousapps/internal/utils/project/projects.kt b/src/main/kotlin/com/autonomousapps/internal/utils/project/projects.kt new file mode 100644 index 000000000..46c32c24e --- /dev/null +++ b/src/main/kotlin/com/autonomousapps/internal/utils/project/projects.kt @@ -0,0 +1,18 @@ +package com.autonomousapps.internal.utils.project + +import com.autonomousapps.internal.GradleVersions.isAtLeastGradle82 +import org.gradle.api.Project +import org.gradle.api.artifacts.component.ProjectComponentIdentifier +import org.gradle.api.provider.Provider +import org.gradle.kotlin.dsl.get + +/** Get the buildPath of the current build from the root component of the resolution result. */ +internal fun Project.buildPath(configuration: String): Provider { + return configurations[configuration].incoming.resolutionResult.let { + if (isAtLeastGradle82) { + it.rootComponent.map { root -> (root.id as ProjectComponentIdentifier).build.buildPath } + } else { + project.provider { @Suppress("DEPRECATION") (it.root.id as ProjectComponentIdentifier).build.name } + } + } +} diff --git a/src/main/kotlin/com/autonomousapps/model/Coordinates.kt b/src/main/kotlin/com/autonomousapps/model/Coordinates.kt index ce59bd436..9810bf6e6 100644 --- a/src/main/kotlin/com/autonomousapps/model/Coordinates.kt +++ b/src/main/kotlin/com/autonomousapps/model/Coordinates.kt @@ -170,6 +170,15 @@ data class ProjectCoordinates( check(identifier.startsWith(':')) { "Project coordinates must start with a ':'" } } + /** + * Returns a view of [this][ProjectCoordinates] with only the [identifier]. Used to flatten graphs (ignore variants). + */ + internal fun flatten(): ProjectCoordinates = ProjectCoordinates( + identifier = identifier, + gradleVariantIdentification = GradleVariantIdentification.EMPTY, + buildPath = null, + ) + override fun gav(): String = identifier } @@ -199,8 +208,11 @@ data class IncludedBuildCoordinates( val resolvedProject: ProjectCoordinates, override val gradleVariantIdentification: GradleVariantIdentification, ) : Coordinates(identifier, gradleVariantIdentification) { + override fun gav(): String = identifier + internal fun isForBuild(buildPath: String): Boolean = resolvedProject.buildPath == buildPath + companion object { fun of(requested: ModuleCoordinates, resolvedProject: ProjectCoordinates) = IncludedBuildCoordinates( identifier = requested.identifier, diff --git a/src/main/kotlin/com/autonomousapps/model/internal/DependencyGraphView.kt b/src/main/kotlin/com/autonomousapps/model/internal/DependencyGraphView.kt index 933befee1..0dfe1e818 100644 --- a/src/main/kotlin/com/autonomousapps/model/internal/DependencyGraphView.kt +++ b/src/main/kotlin/com/autonomousapps/model/internal/DependencyGraphView.kt @@ -8,9 +8,7 @@ package com.autonomousapps.model.internal import com.autonomousapps.internal.unsafeLazy import com.autonomousapps.model.Coordinates import com.autonomousapps.model.declaration.Variant -import com.google.common.graph.ElementOrder import com.google.common.graph.Graph -import com.google.common.graph.GraphBuilder import com.google.common.graph.ImmutableGraph /** @@ -23,7 +21,7 @@ internal class DependencyGraphView( /** E.g. `compileClasspath` or `debugRuntimeClasspath`. */ val configurationName: String, /** The dependency DAG. */ - internal val graph: Graph + internal val graph: Graph, ) { /** The variant (Android) or source set (JVM) name. */ @@ -33,10 +31,7 @@ internal class DependencyGraphView( companion object { internal fun newGraphBuilder(): ImmutableGraph.Builder { - return GraphBuilder.directed() - .allowsSelfLoops(false) - .incidentEdgeOrder(ElementOrder.stable()) - .immutable() + return com.autonomousapps.internal.graph.newGraphBuilder() } } diff --git a/src/main/kotlin/com/autonomousapps/subplugin/ProjectPlugin.kt b/src/main/kotlin/com/autonomousapps/subplugin/ProjectPlugin.kt index c35f53fac..8444786f3 100644 --- a/src/main/kotlin/com/autonomousapps/subplugin/ProjectPlugin.kt +++ b/src/main/kotlin/com/autonomousapps/subplugin/ProjectPlugin.kt @@ -14,7 +14,6 @@ import com.autonomousapps.Flags.androidIgnoredVariants import com.autonomousapps.Flags.projectPathRegex import com.autonomousapps.Flags.shouldAnalyzeTests import com.autonomousapps.internal.* -import com.autonomousapps.internal.GradleVersions.isAtLeastGradle82 import com.autonomousapps.internal.advice.DslKind import com.autonomousapps.internal.analyzer.* import com.autonomousapps.internal.android.AgpVersion @@ -22,6 +21,7 @@ import com.autonomousapps.internal.artifacts.DagpArtifacts import com.autonomousapps.internal.artifacts.Publisher.Companion.interProjectPublisher import com.autonomousapps.internal.utils.addAll import com.autonomousapps.internal.utils.log +import com.autonomousapps.internal.utils.project.buildPath import com.autonomousapps.internal.utils.toJson import com.autonomousapps.model.DuplicateClass import com.autonomousapps.model.declaration.SourceSetKind @@ -32,7 +32,6 @@ import com.autonomousapps.tasks.* import org.gradle.api.NamedDomainObjectSet import org.gradle.api.Project import org.gradle.api.UnknownTaskException -import org.gradle.api.artifacts.component.ProjectComponentIdentifier import org.gradle.api.file.RegularFile import org.gradle.api.provider.Provider import org.gradle.api.tasks.SourceSet @@ -84,22 +83,27 @@ internal class ProjectPlugin(private val project: Project) { /** We only want to register the aggregation tasks if the by-variants tasks are registered. */ private val aggregatorsRegistered = AtomicBoolean(false) - private lateinit var findDeclarationsTask: TaskProvider - private lateinit var redundantJvmPlugin: RedundantJvmPlugin private lateinit var computeAdviceTask: TaskProvider - private lateinit var reasonTask: TaskProvider private lateinit var computeResolvedDependenciesTask: TaskProvider + private lateinit var findDeclarationsTask: TaskProvider + private lateinit var mergeProjectGraphsTask: TaskProvider + private lateinit var reasonTask: TaskProvider + private lateinit var redundantJvmPlugin: RedundantJvmPlugin private val isDataBindingEnabled = project.objects.property().convention(false) private val isViewBindingEnabled = project.objects.property().convention(false) private val projectHealthPublisher = interProjectPublisher( project = project, - artifact = DagpArtifacts.Kind.PROJECT_HEALTH + artifact = DagpArtifacts.Kind.PROJECT_HEALTH, ) private val resolvedDependenciesPublisher = interProjectPublisher( project = project, - artifact = DagpArtifacts.Kind.RESOLVED_DEPS + artifact = DagpArtifacts.Kind.RESOLVED_DEPS, + ) + private val combinedGraphPublisher = interProjectPublisher( + project = project, + artifact = DagpArtifacts.Kind.COMBINED_GRAPH, ) private val dslService = GlobalDslService.of(project) @@ -626,6 +630,7 @@ internal class ProjectPlugin(private val project: Project) { private fun Project.analyzeDependencies(dependencyAnalyzer: DependencyAnalyzer) { configureAggregationTasks() + val theRootDir = rootDir val thisProjectPath = path val variantName = dependencyAnalyzer.variantName val taskNameSuffix = dependencyAnalyzer.taskNameSuffix @@ -705,6 +710,7 @@ internal class ProjectPlugin(private val project: Project) { val computeDominatorCompile = tasks.register("computeDominatorTreeCompile$taskNameSuffix") { + buildPath.set(buildPath(dependencyAnalyzer.compileConfigurationName)) projectPath.set(thisProjectPath) physicalArtifacts.set(artifactsReport.flatMap { it.output }) graphView.set(graphViewTask.flatMap { it.output }) @@ -716,6 +722,7 @@ internal class ProjectPlugin(private val project: Project) { val computeDominatorRuntime = tasks.register("computeDominatorTreeRuntime$taskNameSuffix") { + buildPath.set(buildPath(dependencyAnalyzer.runtimeConfigurationName)) projectPath.set(thisProjectPath) physicalArtifacts.set(artifactsReportRuntime.flatMap { it.output }) graphView.set(graphViewTask.flatMap { it.outputRuntime }) @@ -739,7 +746,9 @@ internal class ProjectPlugin(private val project: Project) { } // Generates graph view of local (project) dependencies - tasks.register("generateProjectGraph$taskNameSuffix") { + val generateProjectGraphTask = tasks.register("generateProjectGraph$taskNameSuffix") { + buildPath.set(buildPath(dependencyAnalyzer.compileConfigurationName)) + compileClasspath.set( configurations[dependencyAnalyzer.compileConfigurationName] .incoming @@ -755,6 +764,20 @@ internal class ProjectPlugin(private val project: Project) { output.set(outputPaths.projectGraphDir) } + // Prints some help text relating to generateProjectGraphTask. This is the "user-facing" task. + tasks.register("projectGraph$taskNameSuffix") { + rootDir.set(theRootDir) + projectPath.set(thisProjectPath) + graphsDir.set(generateProjectGraphTask.flatMap { it.output }) + } + + // Merges the graphs from generateProjectGraphTask into a single variant-agnostic output. + mergeProjectGraphsTask.configure { + projectGraphs.add(generateProjectGraphTask.flatMap { + it.output.file(GenerateProjectGraphTask.PROJECT_COMBINED_CLASSPATH_JSON) + }) + } + /* ****************************** * Producers. Find the capabilities of all the producers (dependencies). There are many capabilities, including: * 1. Android linters. @@ -1057,6 +1080,10 @@ internal class ProjectPlugin(private val project: Project) { output.set(paths.resolvedDepsPath) } + mergeProjectGraphsTask = tasks.register("generateMergedProjectGraph") { + output.set(paths.mergedProjectGraphPath) + } + /* * Finalizing work. */ @@ -1064,22 +1091,12 @@ internal class ProjectPlugin(private val project: Project) { // Store the main output in the extension for consumption by end-users storeAdviceOutput(filterAdviceTask.flatMap { it.output }) - // Publish our artifacts, and add project dependencies on root project to this project + // Publish our artifacts + combinedGraphPublisher.publish(mergeProjectGraphsTask.flatMap { it.output }) projectHealthPublisher.publish(filterAdviceTask.flatMap { it.output }) resolvedDependenciesPublisher.publish(computeResolvedDependenciesTask.flatMap { it.output }) } - /** Get the buildPath of the current build from the root component of the resolution result. */ - private fun Project.buildPath(configuration: String): Provider { - return configurations[configuration].incoming.resolutionResult.let { - if (isAtLeastGradle82) { - it.rootComponent.map { root -> (root.id as ProjectComponentIdentifier).build.buildPath } - } else { - project.provider { @Suppress("DEPRECATION") (it.root.id as ProjectComponentIdentifier).build.name } - } - } - } - private fun Project.isKaptApplied() = providers.provider { plugins.hasPlugin("org.jetbrains.kotlin.kapt") } /** diff --git a/src/main/kotlin/com/autonomousapps/subplugin/RootPlugin.kt b/src/main/kotlin/com/autonomousapps/subplugin/RootPlugin.kt index a7a7d02d9..4c4014dfc 100644 --- a/src/main/kotlin/com/autonomousapps/subplugin/RootPlugin.kt +++ b/src/main/kotlin/com/autonomousapps/subplugin/RootPlugin.kt @@ -14,11 +14,9 @@ import com.autonomousapps.internal.artifacts.Publisher.Companion.interProjectPub import com.autonomousapps.internal.artifacts.Resolver.Companion.interProjectResolver import com.autonomousapps.internal.artifactsFor import com.autonomousapps.internal.utils.log +import com.autonomousapps.internal.utils.project.buildPath import com.autonomousapps.services.GlobalDslService -import com.autonomousapps.tasks.BuildHealthTask -import com.autonomousapps.tasks.ComputeDuplicateDependenciesTask -import com.autonomousapps.tasks.GenerateBuildHealthTask -import com.autonomousapps.tasks.PrintDuplicateDependenciesTask +import com.autonomousapps.tasks.* import org.gradle.api.Project import org.gradle.kotlin.dsl.register @@ -48,11 +46,15 @@ internal class RootPlugin(private val project: Project) { private val adviceResolver = interProjectResolver( project = project, - artifact = DagpArtifacts.Kind.PROJECT_HEALTH + artifact = DagpArtifacts.Kind.PROJECT_HEALTH, + ) + private val combinedGraphResolver = interProjectResolver( + project = project, + artifact = DagpArtifacts.Kind.COMBINED_GRAPH, ) private val resolvedDepsResolver = interProjectResolver( project = project, - artifact = DagpArtifacts.Kind.RESOLVED_DEPS + artifact = DagpArtifacts.Kind.RESOLVED_DEPS, ) fun apply() = project.run { @@ -121,18 +123,29 @@ internal class RootPlugin(private val project: Project) { postscript.set(dagpExtension.reportingHandler.postscript) } + tasks.register("generateWorkPlan") { + buildPath.set(buildPath(combinedGraphResolver.internal.name)) + combinedProjectGraphs.setFrom(combinedGraphResolver.internal.map { it.artifactsFor("json").artifactFiles }) + outputDirectory.set(paths.workPlanDir) + } + // Add a dependency from the root project all projects (including itself). + val combinedGraphPublisher = interProjectPublisher( + project = project, + artifact = DagpArtifacts.Kind.COMBINED_GRAPH, + ) val projectHealthPublisher = interProjectPublisher( project = this, - artifact = DagpArtifacts.Kind.PROJECT_HEALTH + artifact = DagpArtifacts.Kind.PROJECT_HEALTH, ) val resolvedDependenciesPublisher = interProjectPublisher( project = this, - artifact = DagpArtifacts.Kind.RESOLVED_DEPS + artifact = DagpArtifacts.Kind.RESOLVED_DEPS, ) allprojects.forEach { p -> dependencies.run { + add(combinedGraphPublisher.declarableName, project(p.path)) add(projectHealthPublisher.declarableName, project(p.path)) add(resolvedDependenciesPublisher.declarableName, project(p.path)) } diff --git a/src/main/kotlin/com/autonomousapps/tasks/ComputeAdviceTask.kt b/src/main/kotlin/com/autonomousapps/tasks/ComputeAdviceTask.kt index fbf2e5249..e02049b28 100644 --- a/src/main/kotlin/com/autonomousapps/tasks/ComputeAdviceTask.kt +++ b/src/main/kotlin/com/autonomousapps/tasks/ComputeAdviceTask.kt @@ -31,8 +31,9 @@ import org.gradle.workers.WorkerExecutor import javax.inject.Inject /** - * Takes [usage][com.autonomousapps.model.intermediates.Usage] information from [ComputeUsagesTask] and emits the set of - * transforms a user should perform to have correct and simple dependency declarations. I.e., produces the advice. + * Takes [usage][com.autonomousapps.model.internal.intermediates.Usage] information from [ComputeUsagesTask] and emits + * the set of transforms a user should perform to have correct and simple dependency declarations. I.e., produces the + * advice. */ @CacheableTask abstract class ComputeAdviceTask @Inject constructor( diff --git a/src/main/kotlin/com/autonomousapps/tasks/ComputeDominatorTreeTask.kt b/src/main/kotlin/com/autonomousapps/tasks/ComputeDominatorTreeTask.kt index da70a06fa..b60832622 100644 --- a/src/main/kotlin/com/autonomousapps/tasks/ComputeDominatorTreeTask.kt +++ b/src/main/kotlin/com/autonomousapps/tasks/ComputeDominatorTreeTask.kt @@ -11,12 +11,11 @@ import com.autonomousapps.graph.DominanceTreeDataWriter import com.autonomousapps.graph.DominanceTreeWriter import com.autonomousapps.graph.Graphs.reachableNodes import com.autonomousapps.internal.graph.GraphWriter -import com.autonomousapps.internal.utils.FileUtils -import com.autonomousapps.internal.utils.bufferWriteParameterizedJson -import com.autonomousapps.internal.utils.fromJson -import com.autonomousapps.internal.utils.fromJsonSet -import com.autonomousapps.internal.utils.getAndDelete -import com.autonomousapps.model.* +import com.autonomousapps.internal.utils.* +import com.autonomousapps.model.Coordinates +import com.autonomousapps.model.GradleVariantIdentification +import com.autonomousapps.model.IncludedBuildCoordinates +import com.autonomousapps.model.ProjectCoordinates import com.autonomousapps.model.internal.DependencyGraphView import com.autonomousapps.model.internal.PhysicalArtifact import org.gradle.api.DefaultTask @@ -33,6 +32,9 @@ abstract class ComputeDominatorTreeTask : DefaultTask() { description = "Computes a dominator view of the dependency graph" } + @get:Input + abstract val buildPath: Property + @get:Input abstract val projectPath: Property @@ -55,12 +57,13 @@ abstract class ComputeDominatorTreeTask : DefaultTask() { @TaskAction fun action() { compute( + buildPath = buildPath, projectPath = projectPath, outputTxt = outputTxt, outputDot = outputDot, outputJson = outputJson, physicalArtifacts = physicalArtifacts, - graphView = graphView + graphView = graphView, ) } @@ -134,6 +137,7 @@ abstract class ComputeDominatorTreeTask : DefaultTask() { private companion object { @Suppress("NAME_SHADOWING") fun compute( + buildPath: Property, projectPath: Property, outputTxt: RegularFileProperty, outputDot: RegularFileProperty, @@ -158,7 +162,7 @@ abstract class ComputeDominatorTreeTask : DefaultTask() { tree = tree, root = project ) - val writer: DominanceTreeWriter = DominanceTreeWriter( + val dominanceTreeWriter: DominanceTreeWriter = DominanceTreeWriter( root = project, tree = tree, nodeWriter = nodeWriter, @@ -168,9 +172,10 @@ abstract class ComputeDominatorTreeTask : DefaultTask() { tree = tree, nodeWriter = nodeWriter, ) + val graphWriter = GraphWriter(buildPath.get()) - outputTxt.writeText(writer.string) - outputDot.writeText(GraphWriter.toDot(tree.dominanceGraph)) + outputTxt.writeText(dominanceTreeWriter.string) + outputDot.writeText(graphWriter.toDot(tree.dominanceGraph)) outputJson.bufferWriteParameterizedJson, String>( dataWriter.sizeTree.map { it.identifier } // we only really care about the identitfiers ) diff --git a/src/main/kotlin/com/autonomousapps/tasks/GenerateProjectGraphTask.kt b/src/main/kotlin/com/autonomousapps/tasks/GenerateProjectGraphTask.kt new file mode 100644 index 000000000..687a0f339 --- /dev/null +++ b/src/main/kotlin/com/autonomousapps/tasks/GenerateProjectGraphTask.kt @@ -0,0 +1,78 @@ +package com.autonomousapps.tasks + +import com.autonomousapps.internal.graph.GraphViewBuilder +import com.autonomousapps.internal.graph.GraphWriter +import com.autonomousapps.internal.graph.stripVariants +import com.autonomousapps.internal.graph.plus +import com.autonomousapps.internal.utils.GraphAdapter.GraphContainer +import com.autonomousapps.internal.utils.bufferWriteJson +import com.autonomousapps.internal.utils.getAndDelete +import org.gradle.api.DefaultTask +import org.gradle.api.artifacts.result.ResolvedComponentResult +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* + +@CacheableTask +abstract class GenerateProjectGraphTask : DefaultTask() { + + init { + description = "Generates several graph views of this project's local dependency graph" + } + + internal companion object { + const val PROJECT_COMBINED_CLASSPATH_JSON = "project-combined-classpath.json" + const val PROJECT_COMPILE_CLASSPATH_GV = "project-compile-classpath.gv" + const val PROJECT_RUNTIME_CLASSPATH_GV = "project-runtime-classpath.gv" + const val PROJECT_COMBINED_CLASSPATH_GV = "project-combined-classpath.gv" + } + + @get:Input + abstract val buildPath: Property + + @get:Input + abstract val compileClasspath: Property + + @get:Input + abstract val runtimeClasspath: Property + + @get:OutputDirectory + abstract val output: DirectoryProperty + + @TaskAction fun action() { + val compileOutput = output.file(PROJECT_COMPILE_CLASSPATH_GV).getAndDelete() + val runtimeOutput = output.file(PROJECT_RUNTIME_CLASSPATH_GV).getAndDelete() + val combinedOutput = output.file(PROJECT_COMBINED_CLASSPATH_GV).getAndDelete() + val combinedJsonOutput = output.file(PROJECT_COMBINED_CLASSPATH_JSON).getAndDelete() + val compileTopOutput = output.file("project-compile-classpath-topological.txt").getAndDelete() + val runtimeTopOutput = output.file("project-runtime-classpath-topological.txt").getAndDelete() + + val compileGraph = GraphViewBuilder( + root = compileClasspath.get(), + fileCoordinates = emptySet(), + localOnly = true, + ).graph + + val runtimeGraph = GraphViewBuilder( + root = runtimeClasspath.get(), + fileCoordinates = emptySet(), + localOnly = true, + ).graph + + val graphWriter = GraphWriter(buildPath.get()) + val buildPath = buildPath.get() + + // Write graphs + compileOutput.writeText(graphWriter.toDot(compileGraph)) + runtimeOutput.writeText(graphWriter.toDot(runtimeGraph)) + + // Write out combined compile + runtime graph + val combinedGraph = compileGraph.stripVariants(buildPath) + runtimeGraph.stripVariants(buildPath) + combinedOutput.writeText(graphWriter.toDot(combinedGraph)) + combinedJsonOutput.bufferWriteJson(GraphContainer(combinedGraph)) + + // Write topological sorts + compileTopOutput.writeText(graphWriter.topological(compileGraph)) + runtimeTopOutput.writeText(graphWriter.topological(runtimeGraph)) + } +} diff --git a/src/main/kotlin/com/autonomousapps/tasks/GenerateWorkPlan.kt b/src/main/kotlin/com/autonomousapps/tasks/GenerateWorkPlan.kt new file mode 100644 index 000000000..d9e3c5031 --- /dev/null +++ b/src/main/kotlin/com/autonomousapps/tasks/GenerateWorkPlan.kt @@ -0,0 +1,75 @@ +package com.autonomousapps.tasks + +import com.autonomousapps.graph.Graphs.roots +import com.autonomousapps.internal.graph.GraphWriter +import com.autonomousapps.internal.graph.newGraphBuilder +import com.autonomousapps.internal.graph.plus +import com.autonomousapps.internal.utils.GraphAdapter.GraphContainer +import com.autonomousapps.internal.utils.bufferWriteJson +import com.autonomousapps.internal.utils.fromJson +import com.autonomousapps.internal.utils.getAndDelete +import com.autonomousapps.model.Coordinates +import com.autonomousapps.model.GradleVariantIdentification +import com.autonomousapps.model.ProjectCoordinates +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* + +// TODO(tsr): fix or delete +abstract class GenerateWorkPlan : DefaultTask() { + + init { + description = "Generates work plan for fixing dependency issues with minimal conflict" + } + + @get:Input + abstract val buildPath: Property + + @get:PathSensitive(PathSensitivity.RELATIVE) + @get:InputFiles + abstract val combinedProjectGraphs: ConfigurableFileCollection + + @get:OutputDirectory + abstract val outputDirectory: DirectoryProperty + + @TaskAction fun action() { + val combinedGraphOut = outputDirectory.file("combined-graph.json").getAndDelete() + val combinedGraphDotOut = outputDirectory.file("combined-graph.gz").getAndDelete() + val workPlanJsonOut = outputDirectory.file("work-plan.json").getAndDelete() + val workPlanTextOut = outputDirectory.file("work-plan.txt").getAndDelete() + + // TODO(tsr): this is all very ugly + val combinedGraph = combinedProjectGraphs.files + .map { it.fromJson().graph } + .reduce { acc, graph -> acc + graph } + + val graphWriter = GraphWriter(buildPath.get()) + + // combinedGraphDotOut.writeText(graphWriter.toDotS(combinedGraph)) + // combinedGraphOut.bufferWriteJson(GraphStringContainer(combinedGraph)) + + // Need to add artificial edges from the root project to every root (generally, apps) in the build + val rootProject = ProjectCoordinates( + identifier = ":", + gradleVariantIdentification = GradleVariantIdentification.EMPTY, + buildPath = buildPath.get(), + ) + val graphBuilder = newGraphBuilder() + combinedGraph.roots() + .filterNot { it.identifier == ":" } + .forEach { root -> + graphBuilder.putEdge(ProjectCoordinates(":", GradleVariantIdentification.EMPTY), root) + } + val finalGraph = graphBuilder.build() + combinedGraph + + combinedGraphDotOut.writeText(graphWriter.toDot(finalGraph)) + combinedGraphOut.bufferWriteJson(GraphContainer(finalGraph)) + + // TODO(tsr): this task should only generate the plan, and another task will print it. + graphWriter.workPlan(finalGraph).also { + logger.quiet("[INCUBATING] Work plan:\n$it") + } + } +} diff --git a/src/main/kotlin/com/autonomousapps/tasks/GraphViewTask.kt b/src/main/kotlin/com/autonomousapps/tasks/GraphViewTask.kt index 6cb560805..dca351cf9 100644 --- a/src/main/kotlin/com/autonomousapps/tasks/GraphViewTask.kt +++ b/src/main/kotlin/com/autonomousapps/tasks/GraphViewTask.kt @@ -149,11 +149,12 @@ abstract class GraphViewTask : DefaultTask() { configurationName = runtimeClasspathName.get(), graph = runtimeGraph ) + val graphWriter = GraphWriter(buildPath.get()) output.bufferWriteJson(compileGraphView) - outputDot.writeText(GraphWriter.toDot(compileGraph)) + outputDot.writeText(graphWriter.toDot(compileGraph)) outputNodes.bufferWriteJson(CoordinatesContainer(compileGraphView.nodes)) outputRuntime.bufferWriteJson(runtimeGraphView) - outputRuntimeDot.writeText(GraphWriter.toDot(runtimeGraph)) + outputRuntimeDot.writeText(graphWriter.toDot(runtimeGraph)) } } diff --git a/src/main/kotlin/com/autonomousapps/tasks/MergeProjectGraphsTask.kt b/src/main/kotlin/com/autonomousapps/tasks/MergeProjectGraphsTask.kt new file mode 100644 index 000000000..cbf4b637e --- /dev/null +++ b/src/main/kotlin/com/autonomousapps/tasks/MergeProjectGraphsTask.kt @@ -0,0 +1,36 @@ +package com.autonomousapps.tasks + +import com.autonomousapps.internal.graph.plus +import com.autonomousapps.internal.utils.GraphAdapter.GraphContainer +import com.autonomousapps.internal.utils.bufferWriteJson +import com.autonomousapps.internal.utils.fromJson +import com.autonomousapps.internal.utils.getAndDelete +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFile +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.tasks.* + +abstract class MergeProjectGraphsTask : DefaultTask() { + + init { + description = "Merges the project graphs of all variants into a single graph" + } + + @get:PathSensitive(PathSensitivity.RELATIVE) + @get:InputFiles + abstract val projectGraphs: ListProperty + + @get:OutputFile + abstract val output: RegularFileProperty + + @TaskAction fun action() { + val output = output.getAndDelete() + + val graph = projectGraphs.get() + .map { it.fromJson().graph } + .reduce { acc, graph -> acc + graph } + + output.bufferWriteJson(GraphContainer(graph)) + } +} diff --git a/src/main/kotlin/com/autonomousapps/tasks/ProjectGraphTask.kt b/src/main/kotlin/com/autonomousapps/tasks/ProjectGraphTask.kt index 6bbe1e6f3..d230e2728 100644 --- a/src/main/kotlin/com/autonomousapps/tasks/ProjectGraphTask.kt +++ b/src/main/kotlin/com/autonomousapps/tasks/ProjectGraphTask.kt @@ -1,52 +1,58 @@ package com.autonomousapps.tasks import com.autonomousapps.TASK_GROUP_DEP -import com.autonomousapps.internal.graph.GraphViewBuilder -import com.autonomousapps.internal.graph.GraphWriter -import com.autonomousapps.internal.utils.getAndDelete import org.gradle.api.DefaultTask -import org.gradle.api.artifacts.result.ResolvedComponentResult import org.gradle.api.file.DirectoryProperty import org.gradle.api.provider.Property -import org.gradle.api.tasks.CacheableTask -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.OutputDirectory -import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.* -@CacheableTask +@UntrackedTask(because = "Prints text to console") abstract class ProjectGraphTask : DefaultTask() { init { group = TASK_GROUP_DEP - description = "Generates a graph view of this project's local dependency graph" + description = "Generates several graph views of this project's local dependency graph" } + /** Used for logging. */ @get:Input - abstract val compileClasspath: Property + abstract val projectPath: Property - @get:Input - abstract val runtimeClasspath: Property + /** + * Used for relativizing output paths for logging. Internal because we don't want Gradle to hash the entire project. + */ + @get:Internal + abstract val rootDir: DirectoryProperty - @get:OutputDirectory - abstract val output: DirectoryProperty + @get:InputDirectory + abstract val graphsDir: DirectoryProperty @TaskAction fun action() { - val compileOutput = output.file("project-compile-classpath.gv").getAndDelete() - val runtimeOutput = output.file("project-runtime-classpath.gv").getAndDelete() - - val compileGraph = GraphViewBuilder( - root = compileClasspath.get(), - fileCoordinates = emptySet(), - localOnly = true, - ).graph - - val runtimeGraph = GraphViewBuilder( - root = runtimeClasspath.get(), - fileCoordinates = emptySet(), - localOnly = true, - ).graph - - compileOutput.writeText(GraphWriter.toDot(compileGraph)) - runtimeOutput.writeText(GraphWriter.toDot(runtimeGraph)) + val compileOutput = graphsDir.file(GenerateProjectGraphTask.PROJECT_COMPILE_CLASSPATH_GV).get().asFile + val runtimeOutput = graphsDir.file(GenerateProjectGraphTask.PROJECT_RUNTIME_CLASSPATH_GV).get().asFile + val combinedOutput = graphsDir.file(GenerateProjectGraphTask.PROJECT_COMBINED_CLASSPATH_GV).get().asFile + + // Print a message so users know how to do something with the generated .gv files. + val msg = buildString { + // convert ":foo:bar" to "foo-bar.svg" + val svgName = projectPath.get().removePrefix(":").replace(':', '-') + ".svg" + + // Get relative paths to output for more readable logging + val rootPath = rootDir.get().asFile + val compilePath = compileOutput.relativeTo(rootPath) + val runtimePath = runtimeOutput.relativeTo(rootPath) + val combinedPath = combinedOutput.relativeTo(rootPath) + + appendLine("Graphs generated to:") + appendLine(" - $compilePath") + appendLine(" - $runtimePath") + appendLine(" - $combinedPath") + appendLine() + appendLine("To generate an SVG with graphviz, you could run the following. (You must have graphviz installed.)") + appendLine() + appendLine(" dot -Tsvg $runtimePath -o $svgName") + } + + logger.quiet(msg) } } diff --git a/src/test/kotlin/com/autonomousapps/model/internal/DependencyGraphViewTest.kt b/src/test/kotlin/com/autonomousapps/model/internal/DependencyGraphViewTest.kt index d42d6d598..38a81f16a 100644 --- a/src/test/kotlin/com/autonomousapps/model/internal/DependencyGraphViewTest.kt +++ b/src/test/kotlin/com/autonomousapps/model/internal/DependencyGraphViewTest.kt @@ -30,7 +30,7 @@ internal class DependencyGraphViewTest { val serialized = graphView.toJson() assertThat(serialized).isEqualTo( """ - {"variant":{"variant":"main","kind":"MAIN"},"configurationName":"compileClasspath","nodes":[{"type":"project","identifier":":secondary:root","gradleVariantIdentification":{"capabilities":[],"attributes":{}}},{"type":"project","identifier":":root","gradleVariantIdentification":{"capabilities":[],"attributes":{}}},{"type":"module","identifier":"foo:bar","resolvedVersion":"1","gradleVariantIdentification":{"capabilities":[],"attributes":{}}},{"type":"module","identifier":"bar:baz","resolvedVersion":"1","gradleVariantIdentification":{"capabilities":[],"attributes":{}}}],"edges":[{"source":{"type":"project","identifier":":root","gradleVariantIdentification":{"capabilities":[],"attributes":{}}},"target":{"type":"module","identifier":"foo:bar","resolvedVersion":"1","gradleVariantIdentification":{"capabilities":[],"attributes":{}}}},{"source":{"type":"module","identifier":"foo:bar","resolvedVersion":"1","gradleVariantIdentification":{"capabilities":[],"attributes":{}}},"target":{"type":"module","identifier":"bar:baz","resolvedVersion":"1","gradleVariantIdentification":{"capabilities":[],"attributes":{}}}}]} + {"variant":{"variant":"main","kind":"MAIN"},"configurationName":"compileClasspath","graphJson":{"nodes":[{"type":"project","identifier":":secondary:root","gradleVariantIdentification":{"capabilities":[],"attributes":{}}},{"type":"project","identifier":":root","gradleVariantIdentification":{"capabilities":[],"attributes":{}}},{"type":"module","identifier":"foo:bar","resolvedVersion":"1","gradleVariantIdentification":{"capabilities":[],"attributes":{}}},{"type":"module","identifier":"bar:baz","resolvedVersion":"1","gradleVariantIdentification":{"capabilities":[],"attributes":{}}}],"edges":[{"source":{"type":"project","identifier":":root","gradleVariantIdentification":{"capabilities":[],"attributes":{}}},"target":{"type":"module","identifier":"foo:bar","resolvedVersion":"1","gradleVariantIdentification":{"capabilities":[],"attributes":{}}}},{"source":{"type":"module","identifier":"foo:bar","resolvedVersion":"1","gradleVariantIdentification":{"capabilities":[],"attributes":{}}},"target":{"type":"module","identifier":"bar:baz","resolvedVersion":"1","gradleVariantIdentification":{"capabilities":[],"attributes":{}}}}]}} """.trimIndent() )