Skip to content

Commit

Permalink
Support version catalog references in BundleHandler and DependenciesH…
Browse files Browse the repository at this point in the history
…andler. (#1006)
  • Loading branch information
autonomousapps authored Oct 27, 2023
1 parent 7e710df commit 017445e
Show file tree
Hide file tree
Showing 14 changed files with 276 additions and 95 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.autonomousapps.jvm

import com.autonomousapps.jvm.projects.VersionCatalogProject
import com.autonomousapps.utils.Colors

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

final class VersionCatalogSpec extends AbstractJvmSpec {

def "version catalogs work (#gradleVersion)"() {
given:
def project = new VersionCatalogProject()
gradleProject = project.gradleProject
when:
build(gradleVersion, gradleProject.rootDir, 'buildHealth', '-Pdependency.analysis.print.build.health=true')
then:
assertThat(project.actualProjectAdvice()).containsExactlyElementsIn(project.expectedProjectAdvice)
when: 'We ask about the reason using the version catalog alias'
def result = build(gradleVersion, gradleProject.rootDir, 'lib:reason', '--id', 'libs.commonCollections')
then: 'It works'
assertThat(Colors.decolorize(result.output)).contains(
'''\
You asked about the dependency 'org.apache.commons:commons-collections4:4.4 (libs.commonCollections)'.
You have been advised to remove this dependency from 'implementation'.'''.stripIndent()
)
where:
gradleVersion << gradleVersions()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.autonomousapps.jvm.projects

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

import static com.autonomousapps.AdviceHelper.*
import static com.autonomousapps.kit.gradle.Dependency.versionCatalog

final class VersionCatalogProject extends AbstractProject {

final GradleProject gradleProject

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

private GradleProject build() {
def builder = newGradleProjectBuilder()
builder.withRootProject { root ->
root.withFile('gradle/libs.versions.toml', '''\
[versions]
commonCollections = "4.4"
[libraries]
commonCollections = { module = "org.apache.commons:commons-collections4", version.ref = "commonCollections"}
'''.stripIndent())
}

builder.withSubproject('lib') { c ->
c.sources = sources
c.withBuildScript { bs ->
bs.plugins = [Plugin.javaLibraryPlugin]
bs.dependencies = [
versionCatalog('implementation', 'libs.commonCollections')
]
}
}

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

private sources = [
new Source(
SourceType.JAVA, 'Library', 'com/example/library',
"""\
package com.example.library;
public class Library {
}
""".stripIndent()
)
]

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

private Set<Advice> libAdvice = [
Advice.ofRemove(
moduleCoordinates('org.apache.commons:commons-collections4', '4.4'),
'implementation'
)
]

final Set<ProjectAdvice> expectedProjectAdvice = [
projectAdviceForDependencies(':lib', libAdvice),
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import javax.inject.Inject
*/
@Suppress("MemberVisibilityCanBePrivate")
open class DependencyAnalysisExtension @Inject constructor(
project: Project,
objects: ObjectFactory,
) : AbstractExtension(objects) {

Expand All @@ -43,7 +44,7 @@ open class DependencyAnalysisExtension @Inject constructor(
override val issueHandler: IssueHandler = objects.newInstance()
override val abiHandler: AbiHandler = objects.newInstance()
internal val usagesHandler: UsagesHandler = objects.newInstance()
internal val dependenciesHandler: DependenciesHandler = objects.newInstance()
internal val dependenciesHandler: DependenciesHandler = objects.newInstance(project)

/**
* Customize how dependencies are treated. See [DependenciesHandler] for more information.
Expand Down Expand Up @@ -83,7 +84,7 @@ open class DependencyAnalysisExtension @Inject constructor(

internal fun create(project: Project): DependencyAnalysisExtension = project
.extensions
.create(NAME)
.create(NAME, project)
}
}

Expand Down
85 changes: 85 additions & 0 deletions src/main/kotlin/com/autonomousapps/extension/BundleHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.autonomousapps.extension

import org.gradle.api.Named
import org.gradle.api.artifacts.MinimalExternalModuleDependency
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.provider.SetProperty
import org.gradle.kotlin.dsl.setProperty
import org.intellij.lang.annotations.Language
import javax.inject.Inject

/**
* ```
* 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")
*
* // 2: include all supplied dependencies as a single logical dependency
* includeDependency("org.jetbrains.kotlin:kotlin-stdlib")
* includeDependency("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
*
* // 3: include all dependencies that match the regex as a single logical dependency
* include(".*kotlin-stdlib.*")
* }
* ```
*/
abstract class BundleHandler @Inject constructor(
private val name: String,
objects: ObjectFactory,
) : Named {

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 primary(module: Provider<MinimalExternalModuleDependency>) {
primary(module.identifier())
}

fun includeGroup(group: String) {
include("^$group:.*")
}

fun includeGroup(module: Provider<MinimalExternalModuleDependency>) {
includeGroup(module.group())
}

fun includeDependency(identifier: String) {
include("^$identifier\$")
}

fun includeDependency(module: Provider<MinimalExternalModuleDependency>) {
includeDependency(module.identifier())
}

fun include(@Language("RegExp") regex: String) {
include(regex.toRegex())
}

fun include(regex: Regex) {
includes.add(regex)
}

private fun Provider<MinimalExternalModuleDependency>.identifier(): String {
return map { "${it.group}:${it.name}" }.get()
}

private fun Provider<MinimalExternalModuleDependency>.group(): String {
return map {
// group is in fact @Nullable
@Suppress("USELESS_ELVIS")
it.group ?: error("No group for $it")
}.get()
}
}
88 changes: 26 additions & 62 deletions src/main/kotlin/com/autonomousapps/extension/DependenciesHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,9 @@ package com.autonomousapps.extension
import com.autonomousapps.internal.coordinatesOrPathMatch
import com.autonomousapps.model.Coordinates
import org.gradle.api.*
import org.gradle.api.artifacts.VersionCatalogsExtension
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
import org.intellij.lang.annotations.Language
import java.io.Serializable
import javax.inject.Inject

Expand Down Expand Up @@ -42,7 +39,10 @@ import javax.inject.Inject
* }
* ```
*/
abstract class DependenciesHandler @Inject constructor(objects: ObjectFactory) {
abstract class DependenciesHandler @Inject constructor(
private val project: Project,
objects: ObjectFactory,
) {

val map = objects.mapProperty(String::class.java, String::class.java).convention(mutableMapOf())
val bundles = objects.domainObjectContainer(BundleHandler::class.java)
Expand All @@ -59,12 +59,14 @@ abstract class DependenciesHandler @Inject constructor(objects: ObjectFactory) {
includeGroup("com.google.firebase")
includeGroup("com.google.android.gms")
}

withVersionCatalogs()
}

companion object {
internal companion object {
/** Transform [map] into lambda function. Returns requested key as value if key isn't present. */
internal fun Map<String, String>.toLambda(): (String) -> String = { s ->
getOrDefault(s, s)
fun Map<String, String>.toLambda(): (String) -> String? = { s ->
get(s)
}
}

Expand All @@ -78,6 +80,22 @@ abstract class DependenciesHandler @Inject constructor(objects: ObjectFactory) {
}
}

private fun withVersionCatalogs() {
val catalogs = project.extensions.findByType(VersionCatalogsExtension::class.java) ?: return

catalogs.catalogNames.forEach { catalogName ->
val catalog = catalogs.named(catalogName)
val identifierMap = catalog.libraryAliases.associateBy { alias ->
catalog.findLibrary(alias).get().get().module.toString()
}
map.putAll(identifierMap.mapValues { (_, identifier) -> "${catalog.name}.$identifier" })
}
}

private fun wrapException(e: GradleException) = if (e is InvalidUserDataException)
GradleException("You must configure this project either at the root or the project level, not both", e)
else e

internal fun serializableBundles(): SerializableBundles = SerializableBundles.of(bundles)

class SerializableBundles(
Expand Down Expand Up @@ -115,58 +133,4 @@ abstract class DependenciesHandler @Inject constructor(objects: ObjectFactory) {
}
}
}

private fun wrapException(e: GradleException) = if (e is InvalidUserDataException)
GradleException("You must configure this project either at the root or the project level, not both", e)
else e
}

/**
* ```
* 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")
*
* // 2: include all supplied dependencies as a single logical dependency
* includeDependency("org.jetbrains.kotlin:kotlin-stdlib")
* includeDependency("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
*
* // 3: include all dependencies that match the regex as a single logical dependency
* include(".*kotlin-stdlib.*")
* }
* ```
*/
open class BundleHandler @Inject constructor(
private val name: String,
objects: ObjectFactory,
) : Named {

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:.*")
}

fun includeDependency(identifier: String) {
include("^$identifier\$")
}

fun include(@Language("RegExp") regex: String) {
include(regex.toRegex())
}

fun include(regex: Regex) {
includes.add(regex)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import com.autonomousapps.model.ProjectCoordinates
internal class AdvicePrinter(
private val dslKind: DslKind,
/** Customize how dependencies are printed. */
private val dependencyMap: (String) -> String = { it },
private val dependencyMap: ((String) -> String?)? = null,
) {

fun line(configuration: String, printableIdentifier: String, was: String = ""): String =
Expand Down Expand Up @@ -37,7 +37,11 @@ internal class AdvicePrinter(
DslKind.KOTLIN -> "\""
DslKind.GROOVY -> "'"
}
"($id) { capabilities {\n${coordinates.gradleVariantIdentification.capabilities.filter { !it.endsWith(":test-fixtures") }.joinToString("") { it.requireCapability(quote) }} }}"
"($id) { capabilities {\n${
coordinates.gradleVariantIdentification.capabilities
.filter { !it.endsWith(":test-fixtures") }
.joinToString("") { it.requireCapability(quote) }
} }}"
}
}
}
Expand All @@ -46,17 +50,18 @@ internal class AdvicePrinter(

private fun Coordinates.mapped(): String {
val gav = gav()
val mapped = dependencyMap(gav)
// if the map contains full GAV
val mapped = dependencyMap?.invoke(gav) ?: dependencyMap?.invoke(identifier)

return if (gav == mapped) {
return if (!mapped.isNullOrBlank()) {
// If the user is mapping, it's bring-your-own-quotes
mapped
} else {
// If there's no map, include quotes
when (dslKind) {
DslKind.KOTLIN -> "\"$mapped\""
DslKind.GROOVY -> "'$mapped'"
DslKind.KOTLIN -> "\"$gav\""
DslKind.GROOVY -> "'$gav'"
}
} else {
// If the user is mapping, it's bring-your-own-quotes
mapped
}
}
}
Loading

0 comments on commit 017445e

Please sign in to comment.