Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ plugins {
id 'com.vanniktech.maven.publish' apply false
id 'com.android.lint' apply false
id 'org.jetbrains.intellij.platform' apply false
id 'com.github.johnrengelman.shadow' apply false
}

group = 'com.fueledbycaffeine'
Expand Down
12 changes: 11 additions & 1 deletion buildscript-utils/api/buildscript-utils.api
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,21 @@ public final class com/fueledbycaffeine/spotlight/buildscript/BuildFile {
}

public final class com/fueledbycaffeine/spotlight/buildscript/BuildGraph : com/fueledbycaffeine/spotlight/buildscript/graph/Graph {
public synthetic fun <init> (Ljava/util/Set;Ljava/util/Set;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public static final field Companion Lcom/fueledbycaffeine/spotlight/buildscript/BuildGraph$Companion;
public fun <init> (Ljava/util/Set;Ljava/util/Set;)V
public synthetic fun <init> (Ljava/util/Set;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public static final fun create (Ljava/util/Set;Ljava/util/Set;)Lcom/fueledbycaffeine/spotlight/buildscript/BuildGraph;
public static final fun createFromNode (Lcom/fueledbycaffeine/spotlight/buildscript/GradlePath;Ljava/util/Set;)Lcom/fueledbycaffeine/spotlight/buildscript/BuildGraph;
public fun getDependencyMap ()Ljava/util/Map;
}

public final class com/fueledbycaffeine/spotlight/buildscript/BuildGraph$Companion {
public final fun create (Ljava/util/Set;Ljava/util/Set;)Lcom/fueledbycaffeine/spotlight/buildscript/BuildGraph;
public static synthetic fun create$default (Lcom/fueledbycaffeine/spotlight/buildscript/BuildGraph$Companion;Ljava/util/Set;Ljava/util/Set;ILjava/lang/Object;)Lcom/fueledbycaffeine/spotlight/buildscript/BuildGraph;
public final fun createFromNode (Lcom/fueledbycaffeine/spotlight/buildscript/GradlePath;Ljava/util/Set;)Lcom/fueledbycaffeine/spotlight/buildscript/BuildGraph;
public static synthetic fun createFromNode$default (Lcom/fueledbycaffeine/spotlight/buildscript/BuildGraph$Companion;Lcom/fueledbycaffeine/spotlight/buildscript/GradlePath;Ljava/util/Set;ILjava/lang/Object;)Lcom/fueledbycaffeine/spotlight/buildscript/BuildGraph;
}

public final class com/fueledbycaffeine/spotlight/buildscript/BuildGraphKt {
public static final fun buildGraph (Lcom/fueledbycaffeine/spotlight/buildscript/GradlePath;Ljava/util/Set;)Lcom/fueledbycaffeine/spotlight/buildscript/BuildGraph;
public static synthetic fun buildGraph$default (Lcom/fueledbycaffeine/spotlight/buildscript/GradlePath;Ljava/util/Set;ILjava/lang/Object;)Lcom/fueledbycaffeine/spotlight/buildscript/BuildGraph;
Expand Down Expand Up @@ -96,6 +105,7 @@ public abstract class com/fueledbycaffeine/spotlight/buildscript/graph/Graph {
public fun <init> ()V
public final fun accessorsOf (Lcom/fueledbycaffeine/spotlight/buildscript/graph/GraphNode;)Ljava/util/Set;
public final fun edges ()Ljava/util/Set;
public final fun findShortestPath (Lcom/fueledbycaffeine/spotlight/buildscript/graph/GraphNode;Lcom/fueledbycaffeine/spotlight/buildscript/graph/GraphNode;)Ljava/util/List;
public abstract fun getDependencyMap ()Ljava/util/Map;
public final fun successorsOf (Lcom/fueledbycaffeine/spotlight/buildscript/graph/GraphNode;)Ljava/util/Set;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,32 @@ package com.fueledbycaffeine.spotlight.buildscript
import com.fueledbycaffeine.spotlight.buildscript.graph.BreadthFirstSearch
import com.fueledbycaffeine.spotlight.buildscript.graph.Graph
import com.fueledbycaffeine.spotlight.buildscript.graph.ImplicitDependencyRule
import java.util.LinkedList

public class BuildGraph private constructor(
private val allProjects: Set<GradlePath>,
private val rules: Set<ImplicitDependencyRule> = emptySet(),
public class BuildGraph(
allProjects: Set<GradlePath>,
rules: Set<ImplicitDependencyRule> = emptySet(),
): Graph<GradlePath>() {
internal companion object {
public companion object {
@JvmStatic
fun create(
public fun create(
allProjects: Set<GradlePath>,
rules: Set<ImplicitDependencyRule> = emptySet(),
): BuildGraph {
return BuildGraph(allProjects, rules)
}

@JvmStatic
fun createFromNode(
public fun createFromNode(
node: GradlePath,
rules: Set<ImplicitDependencyRule> = emptySet(),
): BuildGraph {
return create(setOf(node), rules)
}
}

override val dependencyMap: Map<GradlePath, Set<GradlePath>> by lazy { BreadthFirstSearch.run(allProjects, rules) }
override val dependencyMap: Map<GradlePath, Set<GradlePath>> = BreadthFirstSearch.run(allProjects, rules)
}

public fun GradlePath.buildGraph(rules: Set<ImplicitDependencyRule> = emptySet()): BuildGraph =
BuildGraph.createFromNode(this, rules)
BuildGraph.createFromNode(this, rules)
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.fueledbycaffeine.spotlight.buildscript.graph

import com.fueledbycaffeine.spotlight.buildscript.GradlePath
import java.util.LinkedList

public data class Edge<T>(
public val accessor: T,
public val successor: T,
Expand All @@ -21,4 +24,43 @@ public abstract class Graph<T: GraphNode<T>>() {

public fun accessorsOf(node: T): Set<T> =
edges().filter { it.successor == node }.map { it.accessor }.toSet()

/**
* Finds the shortest path between source and target nodes using BFS.
*
* @param source The starting node
* @param target The target node
* @return List representing the shortest path from source to target, or null if no path exists
*/
public fun findShortestPath(
source: T,
target: T
): List<T>? {
val queue = LinkedList<T>()
val visited = LinkedHashSet<T>(dependencyMap.size)
val parentMap = LinkedHashMap<T, T?>(dependencyMap.size)

queue.offer(source)
visited.add(source)
parentMap[source] = null

while (queue.isNotEmpty()) {
val current = queue.poll()

if (current == target) {
return generateSequence(target) { parentMap[it] }.toList().reversed()
}

// Process dependencies
dependencyMap[current]?.forEach { dep ->
if (dep !in visited) {
visited.add(dep)
parentMap[dep] = current
queue.offer(dep)
}
}
}

return null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package com.fueledbycaffeine.spotlight.buildscript.graph

import assertk.assertThat
import assertk.assertions.contains
import assertk.assertions.containsExactly
import assertk.assertions.containsExactlyInAnyOrder
import assertk.assertions.hasSize
import assertk.assertions.isEmpty
import assertk.assertions.isNotNull
import assertk.assertions.isNull
import com.fueledbycaffeine.spotlight.buildscript.BuildGraph
import com.fueledbycaffeine.spotlight.buildscript.GradlePath
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import java.nio.file.Path
import kotlin.io.path.createDirectories
import kotlin.io.path.createFile
import kotlin.io.path.writeText

class BuildGraphTest {

@TempDir
lateinit var tempDir: Path

private fun createNode(path: String): GradlePath {
// Create the project directory and build file
val gradlePath = GradlePath(tempDir, path)
gradlePath.projectDir.createDirectories()
gradlePath.projectDir.resolve("build.gradle").apply {
createFile()
writeText("// Test build file")
}

return GradlePath(tempDir.toFile(), path)
}

private fun setupDependencies(vararg dependencies: Pair<GradlePath, Set<GradlePath>>) {
dependencies.forEach { (project, deps) ->
val depsText = deps.joinToString("\n") { dep ->
" implementation project('${dep.path}')"
}
project.buildFilePath.writeText(
"""
dependencies {
$depsText
}
""".trimIndent()
)
}
}

@Test
fun `findShortestPath returns shortest path between nodes`() {
val nodeA = createNode(":a")
val nodeB = createNode(":b")
val nodeC = createNode(":c")

// A -> B -> C
setupDependencies(
nodeA to setOf(nodeB),
nodeB to setOf(nodeC),
nodeC to emptySet()
)

val graph = BuildGraph(setOf(nodeA, nodeB, nodeC))
val path = graph.findShortestPath(nodeA, nodeC)

assertThat(path).isNotNull()
assertThat(path!!).containsExactly(nodeA, nodeB, nodeC)
}

@Test
fun `findShortestPath returns null when no path exists`() {
val nodeA = createNode(":a")
val nodeB = createNode(":b")
val nodeC = createNode(":c")

// A -> B, C (isolated)
setupDependencies(
nodeA to setOf(nodeB),
nodeB to emptySet(),
nodeC to emptySet()
)

val graph = BuildGraph(setOf(nodeA, nodeB, nodeC))
val path = graph.findShortestPath(nodeA, nodeC)

assertThat(path).isNull()
}

@Test
fun `accessorsOf returns all direct consumers`() {
val nodeA = createNode(":a")
val nodeB = createNode(":b")
val nodeC = createNode(":c")
val nodeD = createNode(":d")

// B -> A, C -> A, D -> C (so D transitively consumes A)
setupDependencies(
nodeA to emptySet(),
nodeB to setOf(nodeA),
nodeC to setOf(nodeA),
nodeD to setOf(nodeC)
)

val graph = BuildGraph(setOf(nodeA, nodeB, nodeC, nodeD))
val consumers = graph.accessorsOf(nodeA)

assertThat(consumers).containsExactlyInAnyOrder(nodeB, nodeC)
}

@Test
fun `accessorsOf returns consumers when they exist`() {
val nodeA = createNode(":a")
val nodeB = createNode(":b")

// B -> A (B depends on A, so B is a consumer of A)
setupDependencies(
nodeA to emptySet(),
nodeB to setOf(nodeA)
)

val graph = BuildGraph(setOf(nodeA, nodeB))
val consumers = graph.accessorsOf(nodeA)

assertThat(consumers).containsExactlyInAnyOrder(nodeB)
}

@Test
fun `accessorsOf returns empty set when no consumers exist`() {
val nodeA = createNode(":a")

// A has no dependencies and no other nodes depend on A
setupDependencies(
nodeA to emptySet()
)

val graph = BuildGraph(setOf(nodeA))
val consumers = graph.accessorsOf(nodeA)

assertThat(consumers).isEmpty()
}
}
6 changes: 6 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ truth = "1.1.3"
testkit = "0.18"
lint-gradle = "1.0.0-alpha04"
moshi = "1.15.2"
picocli = "4.7.6"
microutilsLogging = "3.0.5"
slf4j = "2.0.13"

[libraries]
junit-platform = { module = "org.junit:junit-bom", version.ref = "junit" }
Expand All @@ -16,3 +19,6 @@ autonomousapps-testkit = { module = "com.autonomousapps:gradle-testkit", version
autonomousapps-testkit-support = { module = "com.autonomousapps:gradle-testkit-support", version.ref = "testkit" }
lint-gradle = { module = "androidx.lint:lint-gradle", version.ref = "lint-gradle" }
moshi = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" }
picocli = { module = "info.picocli:picocli", version.ref = "picocli"}
microutils-logging = { module = "io.github.microutils:kotlin-logging-jvm", version.ref = "microutilsLogging" }
slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" }
4 changes: 3 additions & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pluginManagement {
id 'com.vanniktech.maven.publish' version '0.31.0'
id 'com.android.lint' version '8.9.1'
id 'org.jetbrains.intellij.platform' version '2.6.0'
id 'com.github.johnrengelman.shadow' version '8.1.1'
}
repositories {
gradlePluginPortal()
Expand Down Expand Up @@ -51,4 +52,5 @@ rootProject.name = 'spotlight'

include ':spotlight-gradle-plugin'
include ':spotlight-idea-plugin'
include ':buildscript-utils'
include ':spotlight-cli'
include ':buildscript-utils'
Loading