Skip to content

Commit

Permalink
Support a primary entry point for bundles.
Browse files Browse the repository at this point in the history
  • Loading branch information
autonomousapps committed May 9, 2022
1 parent 5fcd780 commit d999ba4
Show file tree
Hide file tree
Showing 8 changed files with 474 additions and 117 deletions.
24 changes: 24 additions & 0 deletions src/functionalTest/groovy/com/autonomousapps/jvm/BundleSpec.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.autonomousapps.jvm

import com.autonomousapps.jvm.projects.BundleProject

import static com.autonomousapps.utils.Runner.build
import static com.google.common.truth.Truth.assertThat

final class BundleSpec extends AbstractJvmSpec {

def "can define entry point to bundle (#gradleVersion)"() {
given:
def project = new BundleProject()
gradleProject = project.gradleProject
when:
build(gradleVersion, gradleProject.rootDir, 'buildHealth')
then:
assertThat(project.actualProjectAdvice()).containsExactlyElementsIn(project.expectedProjectAdvice)
where:
gradleVersion << gradleVersions()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package com.autonomousapps.jvm.projects

import com.autonomousapps.AbstractProject
import com.autonomousapps.kit.GradleProject
import com.autonomousapps.kit.Plugin
import com.autonomousapps.kit.Source
import com.autonomousapps.kit.SourceType
import com.autonomousapps.model.Advice
import com.autonomousapps.model.ProjectAdvice

import static com.autonomousapps.AdviceHelper.*
import static com.autonomousapps.kit.Dependency.project

final class BundleProject extends AbstractProject {

final GradleProject gradleProject

BundleProject() {
this.gradleProject = build()
}

private GradleProject build() {
def builder = newGradleProjectBuilder()
builder.withRootProject {
it.withBuildScript { bs ->
bs.additions = """
dependencyAnalysis {
dependencies {
bundle('facade') {
primary(':entry-point')
includeDependency(':entry-point')
includeDependency(':used')
}
}
}
""".stripIndent()
}
}
// consumer -> unused -> entry-point -> used
// consumer only uses :used.
// :used and :entry-point are in a bundle
// plugin should advise to add :entry-point and remove :unused.
// it should _not_ advise to add :used.
builder.withSubproject('consumer') { c ->
c.sources = sourcesConsumer
c.withBuildScript { bs ->
bs.plugins = [Plugin.javaLibraryPlugin]
bs.dependencies = [
project('implementation', ':unused')
]
}

}
builder.withSubproject('unused') { s ->
s.sources = sourcesUnused
s.withBuildScript { bs ->
bs.plugins = [Plugin.javaLibraryPlugin]
bs.dependencies = [
project('api', ':entry-point')
]
}
}
builder.withSubproject('entry-point') { s ->
s.sources = sourcesEntryPoint
s.withBuildScript { bs ->
bs.plugins = [Plugin.javaLibraryPlugin]
bs.dependencies = [
project('api', ':used')
]
}
}
builder.withSubproject('used') { s ->
s.sources = sourcesUsed
s.withBuildScript { bs ->
bs.plugins = [Plugin.javaLibraryPlugin]
}
}

def project = builder.build()
project.writer().write()
return project
}

private sourcesConsumer = [
new Source(
SourceType.JAVA, "Consumer", "com/example/consumer",
"""\
package com.example.consumer;
import com.example.used.Used;
public class Consumer {
private Used used;
}
""".stripIndent()
)
]

private sourcesUnused = [
new Source(
SourceType.JAVA, "Unused", "com/example/unused",
"""\
package com.example.unused;
import com.example.entry.EntryPoint;
public abstract class Unused {
public abstract EntryPoint getEntryPoint();
}
""".stripIndent()
)
]

private sourcesEntryPoint = [
new Source(
SourceType.JAVA, "EntryPoint", "com/example/entry",
"""\
package com.example.entry;
import com.example.used.Used;
public abstract class EntryPoint {
public abstract Used getUsed();
}
""".stripIndent()
)
]

private sourcesUsed = [
new Source(
SourceType.JAVA, "Used", "com/example/used",
"""\
package com.example.used;
public class Used {}
""".stripIndent()
)
]

Set<ProjectAdvice> actualProjectAdvice() {
return actualProjectAdvice(gradleProject)
}

private final Set<Advice> consumerAdvice = [
Advice.ofAdd(projectCoordinates(':entry-point'), 'implementation'),
Advice.ofRemove(projectCoordinates(':unused'), 'implementation')
]

final Set<ProjectAdvice> expectedProjectAdvice = [
projectAdviceForDependencies(':consumer', consumerAdvice),
emptyProjectAdviceFor(':unused'),
emptyProjectAdviceFor(':entry-point'),
emptyProjectAdviceFor(':used')
]
}
48 changes: 33 additions & 15 deletions src/main/kotlin/com/autonomousapps/extension/DependenciesHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@
package com.autonomousapps.extension

import com.autonomousapps.model.Coordinates
import org.gradle.api.Action
import org.gradle.api.GradleException
import org.gradle.api.InvalidUserDataException
import org.gradle.api.Named
import org.gradle.api.*
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property
import org.gradle.api.provider.SetProperty
import org.gradle.api.tasks.Input
import org.gradle.kotlin.dsl.setProperty
Expand Down Expand Up @@ -62,30 +60,41 @@ open class DependenciesHandler @Inject constructor(objects: ObjectFactory) {
}
}

internal fun serializableBundles(): SerializableBundles {
return SerializableBundles.of(bundles.asMap.map { (name, groups) ->
name to groups.includes.get()
}.toMap())
}
internal fun serializableBundles(): SerializableBundles = SerializableBundles.of(bundles)

class SerializableBundles(
@get:Input
val bundles: Map<String, Set<Regex>>
@get:Input val rules: Map<String, Set<Regex>>,
@get:Input val primaries: Map<String, String>
) : Serializable {

/** Returns the collection of bundle rules that [coordinates] is a member of. (May be 0 or more.) */
fun matchingBundles(coordinates: Coordinates): Map<String, Set<Regex>> {
if (bundles.isEmpty()) return emptyMap()
internal fun matchingBundles(coordinates: Coordinates): Map<String, Set<Regex>> {
if (rules.isEmpty()) return emptyMap()

return bundles.filter { (_, regexes) ->
return rules.filter { (_, regexes) ->
regexes.any { regex ->
regex.matches(coordinates.identifier)
}
}
}

companion object {
internal fun of(map: Map<String, Set<Regex>>) = SerializableBundles(map)
internal fun of(
bundles: NamedDomainObjectContainer<BundleHandler>
): SerializableBundles {
val rules = mutableMapOf<String, Set<Regex>>()
val primaries = mutableMapOf<String, String>()
bundles.asMap.map { (name, groups) ->
rules[name] = groups.includes.get()

val primary = groups.primary.get()
if (primary.isNotEmpty()) {
primaries[name] = primary
}
}

return SerializableBundles(rules, primaries)
}
}
}

Expand All @@ -97,6 +106,9 @@ open class DependenciesHandler @Inject constructor(objects: ObjectFactory) {
/**
* ```
* bundle("kotlin-stdlib") {
* // 0 (Optional): Specify the primary entry point that the user is "supposed" to declare.
* primary("org.something:primary-entry-point")
*
* // 1: include all in group as a single logical dependency
* includeGroup("org.jetbrains.kotlin")
*
Expand All @@ -116,8 +128,14 @@ open class BundleHandler @Inject constructor(

override fun getName(): String = name

val primary: Property<String> = objects.property(String::class.java).convention("")
val includes: SetProperty<Regex> = objects.setProperty<Regex>().convention(emptySet())

fun primary(identifier: String) {
primary.set(identifier)
primary.disallowChanges()
}

fun includeGroup(group: String) {
include("^$group:.*")
}
Expand Down
120 changes: 120 additions & 0 deletions src/main/kotlin/com/autonomousapps/internal/Bundles.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package com.autonomousapps.internal

import com.autonomousapps.extension.DependenciesHandler.SerializableBundles
import com.autonomousapps.graph.Graphs.children
import com.autonomousapps.graph.Graphs.reachableNodes
import com.autonomousapps.model.Advice
import com.autonomousapps.model.Coordinates
import com.autonomousapps.model.DependencyGraphView
import com.autonomousapps.model.ProjectCoordinates
import com.autonomousapps.model.declaration.Bucket
import com.autonomousapps.model.intermediates.Usage

/**
* :proj
* |
* B -> unused, not declared, but top of graph (added by plugin)
* |
* C -> used as API, part of bundle with B. Should not be declared!
*/
internal class Bundles(private val dependencyUsages: Map<Coordinates, Set<Usage>>) {

// a sort of adjacency-list structure
private val parentKeyedBundle = mutableMapOf<Coordinates, MutableSet<Coordinates>>()

// link child/transitive node to parent node (which is directly adjacent to root project node)
private val parentPointers = mutableMapOf<Coordinates, Coordinates>()

// if a set of rules has a primary identifier, use this to map advice to it
private val primaryPointers = mutableMapOf<Coordinates, Coordinates>()

operator fun set(parentNode: Coordinates, childNode: Coordinates) {
// nb: parents point to themselves as well. This is what lets DoubleDeclarationsSpec pass.
parentKeyedBundle.merge(parentNode, mutableSetOf(parentNode, childNode)) { acc, inc ->
acc.apply { addAll(inc) }
}
parentPointers.putIfAbsent(parentNode, parentNode)
parentPointers.putIfAbsent(childNode, parentNode)
}

fun setPrimary(primary: Coordinates, subordinate: Coordinates) {
primaryPointers.putIfAbsent(subordinate, primary)
}

fun hasParentInBundle(coordinates: Coordinates): Boolean = parentPointers[coordinates] != null

fun primary(advice: Advice): Advice {
check(advice.isAdd()) { "Must be add-advice" }

return primaryPointers[advice.coordinates]?.let { primary ->
advice.copy(coordinates = primary)
} ?: advice
}

fun hasUsedChild(coordinates: Coordinates): Boolean {
val children = parentKeyedBundle[coordinates] ?: return false
return children.any { child ->
dependencyUsages[child].orEmpty().any { it.bucket != Bucket.NONE }
}
}

companion object {
fun of(
projectNode: ProjectCoordinates,
dependencyGraph: Map<String, DependencyGraphView>,
bundleRules: SerializableBundles,
dependencyUsages: Map<Coordinates, Set<Usage>>,
ignoreKtx: Boolean
): Bundles {
val bundles = Bundles(dependencyUsages)

// Handle bundles with primary entry points
bundleRules.primaries.forEach { (name, primaryId) ->
val regexes = bundleRules.rules[name]!!
dependencyGraph.forEach { (_, view) ->
view.graph.reachableNodes(projectNode)
.find { it.identifier == primaryId }
?.let { primaryNode ->
val reachableNodes = view.graph.reachableNodes(primaryNode)
reachableNodes.filter { subordinateNode ->
regexes.any { it.matches(subordinateNode.identifier) }
}.forEach { subordinateNode ->
bundles.setPrimary(primaryNode, subordinateNode)
}
}
}
}

// Handle bundles that don't have a primary entry point
dependencyGraph.forEach { (_, view) ->
view.graph.children(projectNode).forEach { parentNode ->
val rules = bundleRules.matchingBundles(parentNode)

// handle user-supplied bundles
if (rules.isNotEmpty()) {
val reachableNodes = view.graph.reachableNodes(parentNode)
rules.forEach { (_, regexes) ->
reachableNodes.filter { childNode ->
regexes.any { it.matches(childNode.identifier) }
}.forEach { childNode ->
bundles[parentNode] = childNode
}
}
}

// handle dynamic ktx bundles
if (ignoreKtx) {
if (parentNode.identifier.endsWith("-ktx")) {
val baseId = parentNode.identifier.substringBeforeLast("-ktx")
view.graph.children(parentNode).find { child ->
child.identifier == baseId
}?.let { bundles[parentNode] = it }
}
}
}
}

return bundles
}
}
}
Loading

0 comments on commit d999ba4

Please sign in to comment.