Skip to content

chore: Generate type-safe accessors for convention plugins #186

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

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
116 changes: 116 additions & 0 deletions build-logic/plugins/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,119 @@ gradlePlugin {
}
}
}

// This task generates a type-safe wrapper for a Gradle VersionCatalog in the build-logic project. This allows our
// convention plugins to apply other plugins (or specify dependencies) in a type-safe way, similar to how Gradle
// generates a type-safe wrapper for the version catalog in the main project.
// This is directly inspired by this commit in the nowinandroid app, which we've augmented by adding generated accessors
// for our own plugins as well: https://github.com/android/nowinandroid/commit/ec525b77f42d72e5549094db610317584fe6bae7
abstract class GenerateTypeSafeCatalogTask : DefaultTask() {

@get:Input
abstract val catalogName: Property<String>

@get:OutputDirectory
abstract val outputDir: DirectoryProperty

@TaskAction
fun generate() {
val catalogs = project.extensions.getByType<VersionCatalogsExtension>()
val catalog = catalogs.named(catalogName.get())

val pluginExtension = project.extensions.getByType<GradlePluginDevelopmentExtension>()

val generatedCode = buildString {
appendLine("package com.example.catalog")
appendLine()
appendLine("import org.gradle.api.artifacts.MinimalExternalModuleDependency")
appendLine("import org.gradle.api.provider.Provider")
appendLine("import org.gradle.plugin.use.PluginDependency")
appendLine("import org.gradle.api.artifacts.VersionCatalog")
appendLine("import org.gradle.api.artifacts.ExternalModuleDependencyBundle") // Ensure correct import
appendLine("import java.util.Optional")
appendLine()

// Define the extension function
appendLine("fun <T> Optional<T>.orElseThrowIllegalArgs(alias: String, type: String): T {")
appendLine(" return this.orElseThrow { IllegalArgumentException(\"\$type alias '\$alias' not found\") }")
appendLine("}")
appendLine()

// Generate the main wrapper class
appendLine("data class GeneratedCatalog(val catalog: VersionCatalog) {")
appendLine(" val versions = Versions(catalog)")
appendLine(" val libraries = Libraries(catalog)")
appendLine(" val bundles = Bundles(catalog)")
appendLine(" val plugins = Plugins(catalog)")
appendLine("}")
appendLine()

// Generate the Versions data class
appendLine("data class Versions(val catalog: VersionCatalog) {")
catalog.versionAliases.forEach { alias ->
appendLine(" val ${alias.toCamelCase()}: String")
appendLine(" get() = catalog.findVersion(\"$alias\").orElseThrowIllegalArgs(\"$alias\", \"Version\").requiredVersion")
}
appendLine("}")
appendLine()

// Generate the Libraries data class
appendLine("data class Libraries(val catalog: VersionCatalog) {")
catalog.libraryAliases.forEach { alias ->
appendLine(" val ${alias.toCamelCase()}: Provider<MinimalExternalModuleDependency>")
appendLine(" get() = catalog.findLibrary(\"$alias\").orElseThrowIllegalArgs(\"$alias\", \"Library\")")
}
appendLine("}")
appendLine()

// Generate the Bundles data class
appendLine("data class Bundles(val catalog: VersionCatalog) {")
catalog.bundleAliases.forEach { alias ->
appendLine(" val ${alias.toCamelCase()}: Provider<ExternalModuleDependencyBundle>")
appendLine(" get() = catalog.findBundle(\"$alias\").orElseThrowIllegalArgs(\"$alias\", \"Bundle\")")
}
appendLine("}")
appendLine()

// Generate the Plugins data class
appendLine("data class Plugins(val catalog: VersionCatalog) {")
catalog.pluginAliases.forEach { alias ->
appendLine(" val ${alias.toCamelCase()}: Provider<PluginDependency>")
appendLine(" get() = catalog.findPlugin(\"$alias\").orElseThrowIllegalArgs(\"$alias\", \"Plugin\")")
}
appendLine(" val conventions = ConventionPlugins()")
appendLine("}")
appendLine()

appendLine("class ConventionPlugins {")
pluginExtension.plugins.forEach { pluginDeclaration ->
appendLine(" val ${pluginDeclaration.name}: String = \"${pluginDeclaration.id}\"")
}
appendLine("}")
appendLine()
}

val outputFile = outputDir.get().file("GeneratedCatalog.kt").asFile
outputFile.parentFile.mkdirs()
outputFile.writeText(generatedCode)
}

private fun String.toCamelCase(): String = split("-", "_", ".")
.joinToString("") { it.capitalize() }
.decapitalize()
}

tasks.register<GenerateTypeSafeCatalogTask>("generateTypeSafeCatalog") {
outputDir.set(layout.buildDirectory.dir("generated/sources/versionCatalog"))
catalogName.set("libs")
}

sourceSets {
main {
kotlin.srcDir("build/generated/sources/versionCatalog")
}
}

tasks.named("compileKotlin") {
dependsOn("generateTypeSafeCatalog")
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,16 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
@Suppress("LocalVariableName")
class AndroidLibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target.pluginManager) {
apply("com.android.library")
apply("org.jetbrains.kotlin.android")
apply("amplify.android.ktlint")
}

val POM_GROUP: String by target

with(target) {
with(pluginManager) {
val libs = libs()
apply(libs.plugins.androidLibrary)
apply(libs.plugins.kotlinAndroid)
apply(libs.plugins.conventions.ktlint)
}

group = POM_GROUP
extensions.configure<LibraryExtension> {
configureAndroid(this)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ import org.gradle.kotlin.dsl.configure
class ApiValidatorConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
pluginManager.apply("org.jetbrains.kotlinx.binary-compatibility-validator")
val libs = libs()
pluginManager.apply(libs.plugins.binaryCompatibility)

extensions.configure<ApiValidationExtension> {
// Ignore anything marked with an internal API marker
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ val amplifyInternalMarkers = listOf(
class ComponentConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
pluginManager.apply("amplify.android.library")
pluginManager.apply("amplify.android.publishing")
pluginManager.apply("amplify.android.kover")
pluginManager.apply("amplify.android.api.validator")
pluginManager.apply("amplify.android.licenses")
val libs = libs()
pluginManager.apply(libs.plugins.conventions.androidLibrary)
pluginManager.apply(libs.plugins.conventions.publishing)
pluginManager.apply(libs.plugins.conventions.kover)
pluginManager.apply(libs.plugins.conventions.apiValidator)
pluginManager.apply(libs.plugins.conventions.licenses)

tasks.withType<KotlinCompile>().configureEach {
kotlinOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import org.gradle.kotlin.dsl.configure
class KoverConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
pluginManager.apply("org.jetbrains.kotlinx.kover")
val libs = libs()
pluginManager.apply(libs.plugins.kover)

extensions.configure<KoverReportExtension> {
defaults {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@ import org.jlleitschuh.gradle.ktlint.KtlintExtension
*/
class KtLintConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
target.pluginManager.apply("org.jlleitschuh.gradle.ktlint")
target.extensions.configure<KtlintExtension> {
android.set(true)
with(target) {
val libs = libs()
pluginManager.apply(libs.plugins.ktlint)
extensions.configure<KtlintExtension> {
android.set(true)
}
}
}
}
35 changes: 35 additions & 0 deletions build-logic/plugins/src/main/kotlin/Libs.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

import com.example.catalog.GeneratedCatalog
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.api.plugins.AppliedPlugin
import org.gradle.api.plugins.PluginManager
import org.gradle.api.provider.Provider
import org.gradle.kotlin.dsl.getByType
import org.gradle.plugin.use.PluginDependency

fun Project.libs(): GeneratedCatalog {
val catalog = extensions.getByType<VersionCatalogsExtension>().named("libs")
return GeneratedCatalog(catalog)
}

fun PluginManager.apply(provider: Provider<PluginDependency>) = apply(provider.get().pluginId)

fun PluginManager.withPlugin(
provider: Provider<PluginDependency>,
action: (AppliedPlugin) -> Unit,
) = withPlugin(provider.get().pluginId, action)
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import org.gradle.kotlin.dsl.configure
class LicensesConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
pluginManager.apply("app.cash.licensee")
val libs = libs()
pluginManager.apply(libs.plugins.licensee)

extensions.configure<app.cash.licensee.LicenseeExtension> {
allow("Apache-2.0")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ class PublishingConventionPlugin : Plugin<Project> {

// Configure the publishing block in the android extension
private fun Project.configureAndroidPublishing() {
pluginManager.withPlugin("com.android.library") {
val libs = libs()
pluginManager.withPlugin(libs.plugins.androidLibrary) {
extensions.configure<LibraryExtension> {
publishing {
singleVariant("release") {
Expand Down