From 4348224bb488d67a1deb15ccf4048bc89a62dc33 Mon Sep 17 00:00:00 2001 From: Matt Creaser Date: Thu, 5 Sep 2024 11:06:01 -0300 Subject: [PATCH 1/3] Generate type-safe accessors for version catalog in convention plugins --- build-logic/plugins/build.gradle.kts | 100 ++++++++++++++++++ .../kotlin/AndroidLibraryConventionPlugin.kt | 13 +-- .../kotlin/ApiValidatorConventionPlugin.kt | 2 +- .../src/main/kotlin/KoverConventionPlugin.kt | 2 +- .../src/main/kotlin/KtLintConventionPlugin.kt | 8 +- build-logic/plugins/src/main/kotlin/Libs.kt | 35 ++++++ .../main/kotlin/LicensesConventionPlugin.kt | 2 +- .../main/kotlin/PublishingConventionPlugin.kt | 2 +- 8 files changed, 151 insertions(+), 13 deletions(-) create mode 100644 build-logic/plugins/src/main/kotlin/Libs.kt diff --git a/build-logic/plugins/build.gradle.kts b/build-logic/plugins/build.gradle.kts index 0930cc31..068ddc21 100644 --- a/build-logic/plugins/build.gradle.kts +++ b/build-logic/plugins/build.gradle.kts @@ -68,3 +68,103 @@ gradlePlugin { } } } + +abstract class GenerateTypeSafeCatalogTask : DefaultTask() { + + @get:Input + abstract val catalogName: Property + + @get:OutputDirectory + abstract val outputDir: DirectoryProperty + + @TaskAction + fun generate() { + val catalogs = project.extensions.getByType() + val catalog = catalogs.named(catalogName.get()) + + 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 Optional.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") + 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") + 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") + appendLine(" get() = catalog.findPlugin(\"$alias\").orElseThrowIllegalArgs(\"$alias\", \"Plugin\")") + } + 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("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") +} diff --git a/build-logic/plugins/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/plugins/src/main/kotlin/AndroidLibraryConventionPlugin.kt index c97afd55..2801ed4a 100644 --- a/build-logic/plugins/src/main/kotlin/AndroidLibraryConventionPlugin.kt +++ b/build-logic/plugins/src/main/kotlin/AndroidLibraryConventionPlugin.kt @@ -31,15 +31,16 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile @Suppress("LocalVariableName") class AndroidLibraryConventionPlugin : Plugin { 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("amplify.android.ktlint") + } + group = POM_GROUP extensions.configure { configureAndroid(this) diff --git a/build-logic/plugins/src/main/kotlin/ApiValidatorConventionPlugin.kt b/build-logic/plugins/src/main/kotlin/ApiValidatorConventionPlugin.kt index 2015a389..4744bd15 100644 --- a/build-logic/plugins/src/main/kotlin/ApiValidatorConventionPlugin.kt +++ b/build-logic/plugins/src/main/kotlin/ApiValidatorConventionPlugin.kt @@ -25,7 +25,7 @@ import org.gradle.kotlin.dsl.configure class ApiValidatorConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { - pluginManager.apply("org.jetbrains.kotlinx.binary-compatibility-validator") + pluginManager.apply(libs().plugins.binaryCompatibility) extensions.configure { // Ignore anything marked with an internal API marker diff --git a/build-logic/plugins/src/main/kotlin/KoverConventionPlugin.kt b/build-logic/plugins/src/main/kotlin/KoverConventionPlugin.kt index f04ab0e6..15128869 100644 --- a/build-logic/plugins/src/main/kotlin/KoverConventionPlugin.kt +++ b/build-logic/plugins/src/main/kotlin/KoverConventionPlugin.kt @@ -24,7 +24,7 @@ import org.gradle.kotlin.dsl.configure class KoverConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { - pluginManager.apply("org.jetbrains.kotlinx.kover") + pluginManager.apply(libs().plugins.kover) extensions.configure { defaults { diff --git a/build-logic/plugins/src/main/kotlin/KtLintConventionPlugin.kt b/build-logic/plugins/src/main/kotlin/KtLintConventionPlugin.kt index a2f6ff06..94a83a7a 100644 --- a/build-logic/plugins/src/main/kotlin/KtLintConventionPlugin.kt +++ b/build-logic/plugins/src/main/kotlin/KtLintConventionPlugin.kt @@ -24,9 +24,11 @@ import org.jlleitschuh.gradle.ktlint.KtlintExtension */ class KtLintConventionPlugin : Plugin { override fun apply(target: Project) { - target.pluginManager.apply("org.jlleitschuh.gradle.ktlint") - target.extensions.configure { - android.set(true) + with(target) { + pluginManager.apply(libs().plugins.ktlint) + extensions.configure { + android.set(true) + } } } } diff --git a/build-logic/plugins/src/main/kotlin/Libs.kt b/build-logic/plugins/src/main/kotlin/Libs.kt new file mode 100644 index 00000000..e10f71c7 --- /dev/null +++ b/build-logic/plugins/src/main/kotlin/Libs.kt @@ -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().named("libs") + return GeneratedCatalog(catalog) +} + +fun PluginManager.apply(provider: Provider) = apply(provider.get().pluginId) + +fun PluginManager.withPlugin( + provider: Provider, + action: (AppliedPlugin) -> Unit, +) = withPlugin(provider.get().pluginId, action) diff --git a/build-logic/plugins/src/main/kotlin/LicensesConventionPlugin.kt b/build-logic/plugins/src/main/kotlin/LicensesConventionPlugin.kt index 1d33abfe..3650e3aa 100644 --- a/build-logic/plugins/src/main/kotlin/LicensesConventionPlugin.kt +++ b/build-logic/plugins/src/main/kotlin/LicensesConventionPlugin.kt @@ -24,7 +24,7 @@ import org.gradle.kotlin.dsl.configure class LicensesConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { - pluginManager.apply("app.cash.licensee") + pluginManager.apply(libs().plugins.licensee) extensions.configure { allow("Apache-2.0") diff --git a/build-logic/plugins/src/main/kotlin/PublishingConventionPlugin.kt b/build-logic/plugins/src/main/kotlin/PublishingConventionPlugin.kt index eae0aa9f..796245eb 100644 --- a/build-logic/plugins/src/main/kotlin/PublishingConventionPlugin.kt +++ b/build-logic/plugins/src/main/kotlin/PublishingConventionPlugin.kt @@ -45,7 +45,7 @@ class PublishingConventionPlugin : Plugin { // Configure the publishing block in the android extension private fun Project.configureAndroidPublishing() { - pluginManager.withPlugin("com.android.library") { + pluginManager.withPlugin(libs().plugins.androidLibrary) { extensions.configure { publishing { singleVariant("release") { From a2c96d6f17e391b7f67b8b403ee835e773f72705 Mon Sep 17 00:00:00 2001 From: Matt Creaser Date: Thu, 12 Sep 2024 11:50:42 -0300 Subject: [PATCH 2/3] Generate type-safe accessors for Amplify Convention plugins as well --- build-logic/plugins/build.gradle.kts | 11 +++++++++++ .../src/main/kotlin/AndroidLibraryConventionPlugin.kt | 2 +- .../src/main/kotlin/ApiValidatorConventionPlugin.kt | 3 ++- .../src/main/kotlin/ComponentConventionPlugin.kt | 11 ++++++----- .../plugins/src/main/kotlin/KoverConventionPlugin.kt | 3 ++- .../plugins/src/main/kotlin/KtLintConventionPlugin.kt | 3 ++- .../src/main/kotlin/LicensesConventionPlugin.kt | 3 ++- .../src/main/kotlin/PublishingConventionPlugin.kt | 3 ++- 8 files changed, 28 insertions(+), 11 deletions(-) diff --git a/build-logic/plugins/build.gradle.kts b/build-logic/plugins/build.gradle.kts index 068ddc21..20b5df1b 100644 --- a/build-logic/plugins/build.gradle.kts +++ b/build-logic/plugins/build.gradle.kts @@ -82,6 +82,8 @@ abstract class GenerateTypeSafeCatalogTask : DefaultTask() { val catalogs = project.extensions.getByType() val catalog = catalogs.named(catalogName.get()) + val pluginExtension = project.extensions.getByType() + val generatedCode = buildString { appendLine("package com.example.catalog") appendLine() @@ -141,7 +143,16 @@ abstract class GenerateTypeSafeCatalogTask : DefaultTask() { appendLine(" val ${alias.toCamelCase()}: Provider") 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 diff --git a/build-logic/plugins/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/plugins/src/main/kotlin/AndroidLibraryConventionPlugin.kt index 2801ed4a..b50842ff 100644 --- a/build-logic/plugins/src/main/kotlin/AndroidLibraryConventionPlugin.kt +++ b/build-logic/plugins/src/main/kotlin/AndroidLibraryConventionPlugin.kt @@ -38,7 +38,7 @@ class AndroidLibraryConventionPlugin : Plugin { val libs = libs() apply(libs.plugins.androidLibrary) apply(libs.plugins.kotlinAndroid) - apply("amplify.android.ktlint") + apply(libs.plugins.conventions.ktlint) } group = POM_GROUP diff --git a/build-logic/plugins/src/main/kotlin/ApiValidatorConventionPlugin.kt b/build-logic/plugins/src/main/kotlin/ApiValidatorConventionPlugin.kt index 4744bd15..077403f4 100644 --- a/build-logic/plugins/src/main/kotlin/ApiValidatorConventionPlugin.kt +++ b/build-logic/plugins/src/main/kotlin/ApiValidatorConventionPlugin.kt @@ -25,7 +25,8 @@ import org.gradle.kotlin.dsl.configure class ApiValidatorConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { - pluginManager.apply(libs().plugins.binaryCompatibility) + val libs = libs() + pluginManager.apply(libs.plugins.binaryCompatibility) extensions.configure { // Ignore anything marked with an internal API marker diff --git a/build-logic/plugins/src/main/kotlin/ComponentConventionPlugin.kt b/build-logic/plugins/src/main/kotlin/ComponentConventionPlugin.kt index 69218eae..5b45fb97 100644 --- a/build-logic/plugins/src/main/kotlin/ComponentConventionPlugin.kt +++ b/build-logic/plugins/src/main/kotlin/ComponentConventionPlugin.kt @@ -29,11 +29,12 @@ val amplifyInternalMarkers = listOf( class ComponentConventionPlugin : Plugin { 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().configureEach { kotlinOptions { diff --git a/build-logic/plugins/src/main/kotlin/KoverConventionPlugin.kt b/build-logic/plugins/src/main/kotlin/KoverConventionPlugin.kt index 15128869..5a79da33 100644 --- a/build-logic/plugins/src/main/kotlin/KoverConventionPlugin.kt +++ b/build-logic/plugins/src/main/kotlin/KoverConventionPlugin.kt @@ -24,7 +24,8 @@ import org.gradle.kotlin.dsl.configure class KoverConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { - pluginManager.apply(libs().plugins.kover) + val libs = libs() + pluginManager.apply(libs.plugins.kover) extensions.configure { defaults { diff --git a/build-logic/plugins/src/main/kotlin/KtLintConventionPlugin.kt b/build-logic/plugins/src/main/kotlin/KtLintConventionPlugin.kt index 94a83a7a..2dd068e2 100644 --- a/build-logic/plugins/src/main/kotlin/KtLintConventionPlugin.kt +++ b/build-logic/plugins/src/main/kotlin/KtLintConventionPlugin.kt @@ -25,7 +25,8 @@ import org.jlleitschuh.gradle.ktlint.KtlintExtension class KtLintConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { - pluginManager.apply(libs().plugins.ktlint) + val libs = libs() + pluginManager.apply(libs.plugins.ktlint) extensions.configure { android.set(true) } diff --git a/build-logic/plugins/src/main/kotlin/LicensesConventionPlugin.kt b/build-logic/plugins/src/main/kotlin/LicensesConventionPlugin.kt index 3650e3aa..84687277 100644 --- a/build-logic/plugins/src/main/kotlin/LicensesConventionPlugin.kt +++ b/build-logic/plugins/src/main/kotlin/LicensesConventionPlugin.kt @@ -24,7 +24,8 @@ import org.gradle.kotlin.dsl.configure class LicensesConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { - pluginManager.apply(libs().plugins.licensee) + val libs = libs() + pluginManager.apply(libs.plugins.licensee) extensions.configure { allow("Apache-2.0") diff --git a/build-logic/plugins/src/main/kotlin/PublishingConventionPlugin.kt b/build-logic/plugins/src/main/kotlin/PublishingConventionPlugin.kt index 796245eb..fc04743d 100644 --- a/build-logic/plugins/src/main/kotlin/PublishingConventionPlugin.kt +++ b/build-logic/plugins/src/main/kotlin/PublishingConventionPlugin.kt @@ -45,7 +45,8 @@ class PublishingConventionPlugin : Plugin { // Configure the publishing block in the android extension private fun Project.configureAndroidPublishing() { - pluginManager.withPlugin(libs().plugins.androidLibrary) { + val libs = libs() + pluginManager.withPlugin(libs.plugins.androidLibrary) { extensions.configure { publishing { singleVariant("release") { From ccf107aadede43be30851d4d0e6eee2297448100 Mon Sep 17 00:00:00 2001 From: Matt Creaser Date: Thu, 12 Sep 2024 12:05:00 -0300 Subject: [PATCH 3/3] Add attribution comment --- build-logic/plugins/build.gradle.kts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build-logic/plugins/build.gradle.kts b/build-logic/plugins/build.gradle.kts index 20b5df1b..25ac8a78 100644 --- a/build-logic/plugins/build.gradle.kts +++ b/build-logic/plugins/build.gradle.kts @@ -69,6 +69,11 @@ 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