diff --git a/build-logic/plugins/build.gradle.kts b/build-logic/plugins/build.gradle.kts index 0930cc31..25ac8a78 100644 --- a/build-logic/plugins/build.gradle.kts +++ b/build-logic/plugins/build.gradle.kts @@ -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 + + @get:OutputDirectory + abstract val outputDir: DirectoryProperty + + @TaskAction + fun generate() { + 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() + 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 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("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..b50842ff 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(libs.plugins.conventions.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..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("org.jetbrains.kotlinx.binary-compatibility-validator") + 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 f04ab0e6..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("org.jetbrains.kotlinx.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 a2f6ff06..2dd068e2 100644 --- a/build-logic/plugins/src/main/kotlin/KtLintConventionPlugin.kt +++ b/build-logic/plugins/src/main/kotlin/KtLintConventionPlugin.kt @@ -24,9 +24,12 @@ 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) { + val libs = libs() + 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..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("app.cash.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 eae0aa9f..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("com.android.library") { + val libs = libs() + pluginManager.withPlugin(libs.plugins.androidLibrary) { extensions.configure { publishing { singleVariant("release") {