Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support a primary entry point for bundles. #666

Merged
merged 1 commit into from
May 10, 2022
Merged
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
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')
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what the new API looks like in practice. Note that I haven't added an override to the bundle() function, primarily due to limitations in Gradle that make it annoying to create domain objects with custom constructors.

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>>) {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved from elsewhere for easier testing.


// 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) ->
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some ugly duplication here, but it works ™️

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