diff --git a/build.gradle.kts b/build.gradle.kts index 6d36db1466b..84f108616c7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -149,6 +149,7 @@ val lintPaths = listOf( "aws-runtime/**/*.kt", "examples/**/*.kt", "dokka-aws/**/*.kt", + "gradle/sdk-plugins/src/**/*.kt", "services/**/*.kt", "!services/*/generated-src/**/*.kt" ) diff --git a/codegen/protocol-tests/build.gradle.kts b/codegen/protocol-tests/build.gradle.kts index 94fcddd378f..ee930801c6d 100644 --- a/codegen/protocol-tests/build.gradle.kts +++ b/codegen/protocol-tests/build.gradle.kts @@ -2,28 +2,25 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0. */ +import aws.sdk.kotlin.gradle.codegen.dsl.smithyKotlinPlugin import software.amazon.smithy.gradle.tasks.SmithyBuild plugins { - id("software.amazon.smithy") + id("aws.sdk.kotlin.codegen") } description = "Smithy protocol test suite" -buildscript { - val smithyVersion: String by project - dependencies { - classpath("software.amazon.smithy:smithy-cli:$smithyVersion") - } -} - - val smithyVersion: String by project dependencies { implementation("software.amazon.smithy:smithy-aws-protocol-tests:$smithyVersion") - implementation(project(":codegen:smithy-aws-kotlin-codegen")) } +data class ProtocolTest(val projectionName: String, val serviceShapeId: String, val sdkId: String? = null) { + val packageName: String = projectionName.toLowerCase().filter { it.isLetterOrDigit() } +} + + // The following section exposes Smithy protocol test suites as gradle test targets // for the configured protocols in [enabledProtocols]. val enabledProtocols = listOf( @@ -41,157 +38,96 @@ val enabledProtocols = listOf( ProtocolTest("machinelearning", "com.amazonaws.machinelearning#AmazonML_20141212", sdkId = "Machine Learning"), ) -// This project doesn't produce a JAR. -tasks["jar"].enabled = false - -// Run the SmithyBuild task manually since this project needs the built JAR -// from smithy-aws-kotlin-codegen. -tasks["smithyBuildJar"].enabled = false - -task("generateSmithyBuild") { - group = "codegen" - description = "generate smithy-build.json" - val buildFile = projectDir.resolve("smithy-build.json") - doFirst { - buildFile.writeText(generateSmithyBuild(enabledProtocols)) +codegen { + enabledProtocols.forEach { test -> + projections.register(test.projectionName) { + transforms = listOf( + """ + { + "name": "includeServices", + "args": { + "services": ["${test.serviceShapeId}"] + } + } + """ + ) + + smithyKotlinPlugin { + serviceShapeId = test.serviceShapeId + packageName = "aws.sdk.kotlin.services.${test.packageName}" + packageVersion = "1.0" + sdkId = test.sdkId + buildSettings { + generateFullProject = true + optInAnnotations = listOf( + "aws.smithy.kotlin.runtime.util.InternalApi", + "aws.sdk.kotlin.runtime.InternalSdkApi" + ) + } + } + } } - outputs.file(buildFile) } -// Remove generated model file for clean -tasks["clean"].doFirst { - delete("smithy-build.json") -} +tasks.named("generateSmithyProjections") { + // NOTE: The protocol tests are published to maven as a jar, this ensures that + // the aws-protocol-tests dependency is found when generating code such that the `includeServices` transform + // actually works + addCompileClasspath = true -tasks.create("generateSdk") { - group = "codegen" // ensure the generated clients use the same version of the runtime as the aws aws-runtime val smithyKotlinVersion: String by project doFirst { System.setProperty("smithy.kotlin.codegen.clientRuntimeVersion", smithyKotlinVersion) } - addRuntimeClasspath = true - dependsOn(tasks["generateSmithyBuild"]) - inputs.file(projectDir.resolve("smithy-build.json")) - // ensure smithy-aws-kotlin-codegen is up to date - inputs.files(configurations.compileClasspath) -} - -// force rebuild every time while developing -tasks["generateSdk"].outputs.upToDateWhen { false } - -data class ProtocolTest(val projectionName: String, val serviceShapeId: String, val sdkId: String? = null) { - val packageName: String - get() = projectionName.toLowerCase().filter { it.isLetterOrDigit() } -} - - -// Generates a smithy-build.json file by creating a new projection. -// The generated smithy-build.json file is not committed to git since -// it's rebuilt each time codegen is performed. -fun generateSmithyBuild(tests: List): String { - val projections = tests.joinToString(",") { test -> - val sdkIdEntry = test.sdkId?.let { """"sdkId": "$it",""" } ?: "" - """ - "${test.projectionName}": { - "transforms": [ - { - "name": "includeServices", - "args": { - "services": [ - "${test.serviceShapeId}" - ] - } - } - ], - "plugins": { - "kotlin-codegen": { - "service": "${test.serviceShapeId}", - "package": { - "name": "aws.sdk.kotlin.services.${test.packageName}", - "version": "1.0" - }, - $sdkIdEntry - "build": { - "rootProject": true, - "optInAnnotations": [ - "aws.smithy.kotlin.runtime.util.InternalApi", - "aws.sdk.kotlin.runtime.InternalSdkApi" - ] - } - } - } - } - """ - } - return """ - { - "version": "1.0", - "projections": { - $projections - } - } - """.trimIndent() } open class ProtocolTestTask : DefaultTask() { /** - * The protocol name + * The projection */ @get:Input - var protocol: String = "" + var projection: aws.sdk.kotlin.gradle.codegen.dsl.SmithyProjection? = null - /** - * The plugin name to use - */ - @get:Input - var plugin: String = "" - - /** - * The build directory for the task - */ - val generatedBuildDir: File - @OutputDirectory - get() = project.buildDir.resolve("smithyprojections/${project.name}/$protocol/$plugin") @TaskAction fun runTests() { - require(protocol.isNotEmpty()) { "protocol name must be specified" } - require(plugin.isNotEmpty()) { "plugin name must be specified" } - - println("[$protocol] buildDir: $generatedBuildDir") - if (!generatedBuildDir.exists()) { - throw GradleException("$generatedBuildDir does not exist") + val projection = requireNotNull(projection) { "projection is required task input" } + println("[${projection.name}] buildDir: ${projection.projectionRootDir}") + if (!projection.projectionRootDir.exists()) { + throw GradleException("${projection.projectionRootDir} does not exist") } val wrapper = if (System.getProperty("os.name").toLowerCase().contains("windows")) "gradlew.bat" else "gradlew" val gradlew = project.rootProject.file(wrapper).absolutePath // NOTE - this still requires us to publish to maven local. project.exec { - workingDir = generatedBuildDir + workingDir = projection.projectionRootDir executable = gradlew args = listOf("test") } } } -enabledProtocols.forEach { - val protocolName = it.projectionName +val codegenTask = tasks.getByName("generateSmithyProjections") +codegen.projections.forEach { + val protocolName = it.name - val protocolTestTask = tasks.register("testProtocol-$protocolName") { - dependsOn(tasks["generateSdk"]) + tasks.register("testProtocol-$protocolName") { + dependsOn(codegenTask) group = "Verification" - protocol = protocolName - plugin = "kotlin-codegen" - }.get() + projection = it + } // FIXME This is a hack to work around how protocol tests aren't in the actual service model and thus codegen // separately from service customizations. - tasks.create("copyStaticFiles-$protocolName") { + val copyStaticFiles = tasks.register("copyStaticFiles-$protocolName") { + group = "codegen" from(rootProject.projectDir.resolve("services/$protocolName/common/src")) - into(protocolTestTask.generatedBuildDir.resolve("src/main/kotlin/")) - tasks["generateSdk"].finalizedBy(this) + into(it.projectionRootDir.resolve("src/main/kotlin/")) } + + codegenTask.finalizedBy(copyStaticFiles) } tasks.register("testAllProtocols") { diff --git a/codegen/sdk/build.gradle.kts b/codegen/sdk/build.gradle.kts index 6d1828550b1..31ba48e04af 100644 --- a/codegen/sdk/build.gradle.kts +++ b/codegen/sdk/build.gradle.kts @@ -6,7 +6,9 @@ // This build file has been adapted from the Go v2 SDK, here: // https://github.com/aws/aws-sdk-go-v2/blob/master/codegen/sdk-codegen/build.gradle.kts -import software.amazon.smithy.gradle.tasks.SmithyBuild +import aws.sdk.kotlin.gradle.codegen.dsl.SmithyProjection +import aws.sdk.kotlin.gradle.codegen.dsl.projectionRootDir +import aws.sdk.kotlin.gradle.codegen.dsl.smithyKotlinPlugin import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.ServiceShape import java.util.* @@ -16,27 +18,20 @@ import kotlin.streams.toList description = "AWS SDK codegen tasks" plugins { - id("software.amazon.smithy") + id("aws.sdk.kotlin.codegen") } buildscript { val smithyVersion: String by project dependencies { + classpath("software.amazon.smithy:smithy-model:$smithyVersion") classpath("software.amazon.smithy:smithy-aws-traits:$smithyVersion") - classpath("software.amazon.smithy:smithy-cli:$smithyVersion") } } -dependencies { - implementation(project(":codegen:smithy-aws-kotlin-codegen")) -} - // This project doesn't produce a JAR. tasks["jar"].enabled = false -// Run the SmithyBuild task manually since this project needs the built JAR -tasks["smithyBuildJar"].enabled = false - // get a project property by name if it exists (including from local.properties) fun getProperty(name: String): String? { if (project.hasProperty(name)) { @@ -108,56 +103,49 @@ val disabledServices = setOf( "timestreamquery" ) -// Generates a smithy-build.json file by creating a new projection. -// The generated smithy-build.json file is not committed to git since -// it's rebuilt each time codegen is performed. -fun generateSmithyBuild(services: List): String { - require(services.isNotEmpty()) { - "No services discovered. Verify aws.services and aws.protocols properties in local.build. Aborting." - } - - val projections = services.joinToString(",") { service -> - // escape windows paths for valid json - val absModelPath = service.modelFile.absolutePath.replace("\\", "\\\\") - val importPaths = mutableListOf(absModelPath) - if (file(service.modelExtrasDir).exists()) { - importPaths.add(service.modelExtrasDir.replace("\\", "\\\\")) - } - val imports = importPaths.joinToString { "\"$it\"" } - val transforms = transformsForService(service) - - """ - "${service.projectionName}": { - "imports": [$imports], - "plugins": { - "kotlin-codegen": { - "service": "${service.name}", - "package" : { - "name": "${service.packageName}", - "version": "${service.packageVersion}", - "description": "${service.description}" - }, - "sdkId": "${service.sdkId}", - "build": { - "generateDefaultBuildFiles": false - } +// Manually create the projections rather than using the extension to avoid unnecessary configuration evaluation. +// Otherwise we would be reading the models from disk on every gradle invocation for unrelated projects/tasks +fun awsServiceProjections(): Provider> { + val p = project.provider { + println("AWS service projection provider called") + discoveredServices + }.map { + it.map { service -> + SmithyProjection( + service.projectionName, + project.projectionRootDir(service.projectionName) + ).apply { + val importPaths = mutableListOf(service.modelFile.absolutePath) + if (file(service.modelExtrasDir).exists()) { + importPaths.add(service.modelExtrasDir) + } + imports = importPaths + transforms = transformsForService(service) ?: emptyList() + + smithyKotlinPlugin { + serviceShapeId = service.name + packageName = service.packageName + packageVersion = service.packageVersion + packageDescription = service.description + sdkId = service.sdkId + buildSettings { + generateFullProject = false + generateDefaultBuildFiles = false } - }, - "transforms": $transforms + } } - """ - } - - return """ - { - "version": "1.0", - "projections": { - $projections } } - """.trimIndent() + + // get around class cast issues, listProperty implements what we need to pass this to `NamedObjectContainer` + return project.objects.listProperty().value(p) } +// this will lazily evaluate the provider and only cause the models to be +// mapped if the tasks are actually needed +// NOTE: FYI evaluation still happens if you ask for the list of tasks or rebuild the gradle model in intellij +codegen.projections.addAllLater(awsServiceProjections()) + /** * This function retrieves Smithy transforms associated with the target service to be merged into generated * `smithy-build.json` files. The transform file MUST live in /transforms. @@ -168,11 +156,11 @@ fun generateSmithyBuild(services: List): String { * -.json * Example: renameShapes-MarketplaceCommerceAnalyticsException.json */ -fun transformsForService(service: AwsService): String { +fun transformsForService(service: AwsService): List? { val transformsDir = File(service.transformsDir) return transformsDir.listFiles()?.map { transformFile -> transformFile.readText() - }?.toString() ?: "[]" + } } val discoveredServices: List by lazy { discoverServices() } @@ -185,6 +173,7 @@ val sdkPackageNamePrefix = "aws.sdk.kotlin.services." * membership tests */ fun discoverServices(applyFilters: Boolean = true): List { + println("discover services called") val modelsDir: String by project val serviceMembership = parseMembership(getProperty("aws.services")) val protocolMembership = parseMembership(getProperty("aws.protocols")) @@ -294,43 +283,6 @@ fun java.util.Optional.orNull(): T? = this.orElse(null) fun String.kotlinNamespace(): String = split(".") .joinToString(separator = ".") { segment -> segment.filter { it.isLetterOrDigit() } } -// Generate smithy-build.json as first step in build task -task("generateSmithyBuild") { - group = "codegen" - description = "generate smithy-build.json" - doFirst { - projectDir - .resolve("smithy-build.json") - .writeText(generateSmithyBuild(discoveredServices)) - } -} - -tasks.create("generateSdk") { - group = "codegen" - // ensure the generated clients use the same version of the runtime as the aws aws-runtime - val smithyKotlinVersion: String by project - doFirst { - System.setProperty("smithy.kotlin.codegen.clientRuntimeVersion", smithyKotlinVersion) - } - - addRuntimeClasspath = true - dependsOn(tasks["generateSmithyBuild"]) - inputs.file(projectDir.resolve("smithy-build.json")) - // ensure smithy-aws-kotlin-codegen is up to date - inputs.files(configurations.compileClasspath) -} - -// Remove generated model file for clean -tasks["clean"].doFirst { - delete("smithy-build.json") -} - -/** - * The directory code is generated to - */ -val AwsService.projectionOutputDir: String - get() = project.file("${project.buildDir}/smithyprojections/${project.name}/${projectionName}/kotlin-codegen").absolutePath - /** * The project directory under `aws-sdk-kotlin/services` */ @@ -352,31 +304,33 @@ val AwsService.modelExtrasDir: String val AwsService.transformsDir: String get() = rootProject.file("${destinationDir}/transforms").absolutePath -task("stageSdks") { +val stageSdks = tasks.register("stageSdks") { group = "codegen" description = "relocate generated SDK(s) from build directory to services/ dir" - dependsOn("generateSdk") + dependsOn(tasks.named("generateSmithyProjections")) doLast { + println("discoveredServices = ${discoveredServices.joinToString { it.sdkId }}") discoveredServices.forEach { - logger.info("copying ${it.projectionOutputDir} to ${it.destinationDir}") + val projectionOutputDir = codegen.projections.getByName(it.projectionName).projectionRootDir + logger.info("copying $projectionOutputDir to ${it.destinationDir}") copy { - from("${it.projectionOutputDir}/src") + from("$projectionOutputDir/src") into("${it.destinationDir}/generated-src") } copy { - from("${it.projectionOutputDir}/build.gradle.kts") - into("${it.destinationDir}") + from("$projectionOutputDir/build.gradle.kts") + into(it.destinationDir) } } } } -tasks.create("bootstrap") { +tasks.register("bootstrap") { group = "codegen" description = "Generate AWS SDK's and register them with the build" - dependsOn(tasks["generateSdk"]) - finalizedBy(tasks["stageSdks"]) + dependsOn(tasks.named("generateSmithyProjections")) + finalizedBy(stageSdks) } diff --git a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/transforms/IncludeOperations.kt b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/transforms/IncludeOperations.kt new file mode 100644 index 00000000000..f77c32d0639 --- /dev/null +++ b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/transforms/IncludeOperations.kt @@ -0,0 +1,35 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +package aws.sdk.kotlin.codegen.transforms + +import software.amazon.smithy.build.TransformContext +import software.amazon.smithy.build.transforms.ConfigurableProjectionTransformer +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.shapes.OperationShape + +/** + * A smithy build [transform](https://awslabs.github.io/smithy/1.0/guides/building-models/build-config.html#transforms) + * that filters out operations not included in the `operations` list of shape IDs + */ +class IncludeOperations : ConfigurableProjectionTransformer() { + + class Config { + var operations: Set = emptySet() + } + + override fun getName(): String = "awsSdkKotlinIncludeOperations" + override fun getConfigType(): Class = Config::class.java + + override fun transformWithConfig(context: TransformContext, config: Config): Model { + check(config.operations.isNotEmpty()) { "no operations provided to IncludeOperations transform!" } + return context.transformer.filterShapes(context.model) { shape -> + when (shape) { + is OperationShape -> shape.id.toString() in config.operations + else -> true + } + } + } +} diff --git a/codegen/smithy-aws-kotlin-codegen/src/main/resources/META-INF/services/software.amazon.smithy.build.ProjectionTransformer b/codegen/smithy-aws-kotlin-codegen/src/main/resources/META-INF/services/software.amazon.smithy.build.ProjectionTransformer new file mode 100644 index 00000000000..8c906c3195b --- /dev/null +++ b/codegen/smithy-aws-kotlin-codegen/src/main/resources/META-INF/services/software.amazon.smithy.build.ProjectionTransformer @@ -0,0 +1 @@ +aws.sdk.kotlin.codegen.transforms.IncludeOperations \ No newline at end of file diff --git a/gradle/sdk-plugins/build.gradle.kts b/gradle/sdk-plugins/build.gradle.kts new file mode 100644 index 00000000000..cb58a1014ea --- /dev/null +++ b/gradle/sdk-plugins/build.gradle.kts @@ -0,0 +1,54 @@ + +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +import java.util.Properties +buildscript { + repositories { + mavenCentral() + } +} + +plugins { + `kotlin-dsl` + `java-gradle-plugin` +} + +repositories { + mavenCentral() + gradlePluginPortal() +} + +fun loadVersions() { + val gradleProperties = Properties() + val propertiesFile: File = file("../../gradle.properties") + if (propertiesFile.exists()) { + propertiesFile.inputStream().use { gradleProperties.load(it) } + } + gradleProperties.forEach { + project.ext.set(it.key.toString(), it.value) + } +} +// use the versions from aws-sdk-kotlin/gradle.properties +loadVersions() + +val smithyVersion: String by project +val smithyGradleVersion: String by project + +dependencies { + implementation("software.amazon.smithy:smithy-gradle-plugin:$smithyGradleVersion") + implementation("software.amazon.smithy:smithy-model:$smithyVersion") + implementation("software.amazon.smithy:smithy-aws-traits:$smithyVersion") + implementation(gradleApi()) +} + +gradlePlugin { + plugins { + val awsCodegenPlugin by creating { + id = "aws.sdk.kotlin.codegen" + implementationClass = "aws.sdk.kotlin.gradle.codegen.CodegenPlugin" + } + } +} + diff --git a/gradle/sdk-plugins/settings.gradle.kts b/gradle/sdk-plugins/settings.gradle.kts new file mode 100644 index 00000000000..642d47af4cc --- /dev/null +++ b/gradle/sdk-plugins/settings.gradle.kts @@ -0,0 +1,5 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +rootProject.name = "sdk-plugins" \ No newline at end of file diff --git a/gradle/sdk-plugins/src/main/kotlin/aws/sdk/kotlin/gradle/codegen/CodegenExtension.kt b/gradle/sdk-plugins/src/main/kotlin/aws/sdk/kotlin/gradle/codegen/CodegenExtension.kt new file mode 100644 index 00000000000..36065b8479c --- /dev/null +++ b/gradle/sdk-plugins/src/main/kotlin/aws/sdk/kotlin/gradle/codegen/CodegenExtension.kt @@ -0,0 +1,20 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +package aws.sdk.kotlin.gradle.codegen + +import aws.sdk.kotlin.gradle.codegen.dsl.SmithyProjection +import aws.sdk.kotlin.gradle.codegen.dsl.projectionRootDir +import org.gradle.api.Project + +/** + * Register and build Smithy projections + */ +open class CodegenExtension(private val project: Project) { + + val projections = project.objects.domainObjectContainer(SmithyProjection::class.java) { name -> + SmithyProjection(name, project.projectionRootDir(name)) + } +} diff --git a/gradle/sdk-plugins/src/main/kotlin/aws/sdk/kotlin/gradle/codegen/CodegenPlugin.kt b/gradle/sdk-plugins/src/main/kotlin/aws/sdk/kotlin/gradle/codegen/CodegenPlugin.kt new file mode 100644 index 00000000000..b5f77e5cdcf --- /dev/null +++ b/gradle/sdk-plugins/src/main/kotlin/aws/sdk/kotlin/gradle/codegen/CodegenPlugin.kt @@ -0,0 +1,38 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +package aws.sdk.kotlin.gradle.codegen + +import aws.sdk.kotlin.gradle.codegen.tasks.createSmithyCliConfiguration +import aws.sdk.kotlin.gradle.codegen.tasks.registerCodegenTasks +import org.gradle.api.Plugin +import org.gradle.api.Project + +const val CODEGEN_EXTENSION_NAME = "codegen" + +/** + * This plugin handles: + * - applying smithy-gradle-plugin to the project to generate code + * - providing a tasks to generate Kotlin sources from their respective smithy models. + */ +class CodegenPlugin : Plugin { + override fun apply(target: Project): Unit = target.run { + configurePlugins() + installExtension() + registerCodegenTasks() + } + + private fun Project.configurePlugins() { + createSmithyCliConfiguration() + // unfortunately all of the tasks provided by smithy rely on the plugin extension, so it also needs applied + // see https://github.com/awslabs/smithy-gradle-plugin/issues/45 + plugins.apply("software.amazon.smithy") + tasks.getByName("smithyBuildJar").enabled = false + } + + private fun Project.installExtension(): CodegenExtension { + return extensions.create(CODEGEN_EXTENSION_NAME, CodegenExtension::class.java, project) + } +} diff --git a/gradle/sdk-plugins/src/main/kotlin/aws/sdk/kotlin/gradle/codegen/dsl/SmithyBuildPlugin.kt b/gradle/sdk-plugins/src/main/kotlin/aws/sdk/kotlin/gradle/codegen/dsl/SmithyBuildPlugin.kt new file mode 100644 index 00000000000..9685eb7ea5e --- /dev/null +++ b/gradle/sdk-plugins/src/main/kotlin/aws/sdk/kotlin/gradle/codegen/dsl/SmithyBuildPlugin.kt @@ -0,0 +1,16 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +package aws.sdk.kotlin.gradle.codegen.dsl + +import software.amazon.smithy.model.node.ToNode + +interface SmithyBuildPlugin : ToNode { + /** + * The name of the build plugin (e.g. `kotlin-codegen`). This is used when generating + * the projection settings for the plugin + */ + val pluginName: String +} diff --git a/gradle/sdk-plugins/src/main/kotlin/aws/sdk/kotlin/gradle/codegen/dsl/SmithyKotlinPluginSettings.kt b/gradle/sdk-plugins/src/main/kotlin/aws/sdk/kotlin/gradle/codegen/dsl/SmithyKotlinPluginSettings.kt new file mode 100644 index 00000000000..2f320dbd5eb --- /dev/null +++ b/gradle/sdk-plugins/src/main/kotlin/aws/sdk/kotlin/gradle/codegen/dsl/SmithyKotlinPluginSettings.kt @@ -0,0 +1,90 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +package aws.sdk.kotlin.gradle.codegen.dsl + +import software.amazon.smithy.model.node.ArrayNode +import software.amazon.smithy.model.node.Node +import software.amazon.smithy.model.node.ObjectNode +import software.amazon.smithy.model.node.ToNode +import java.util.* + +class SmithyKotlinBuildSettings : ToNode { + var generateFullProject: Boolean? = null + var generateDefaultBuildFiles: Boolean? = null + var optInAnnotations: List? = null + + override fun toNode(): Node { + val builder = ObjectNode.objectNodeBuilder() + + builder.withNullableMember("rootProject", generateFullProject) + builder.withNullableMember("generateDefaultBuildFiles", generateDefaultBuildFiles) + + val optInArrNode = optInAnnotations?.map { Node.from(it) }?.let { ArrayNode.fromNodes(it) } + builder.withOptionalMember("optInAnnotations", Optional.ofNullable(optInArrNode)) + return builder.build() + } +} + +class SmithyKotlinPluginSettings : SmithyBuildPlugin { + override val pluginName: String = "kotlin-codegen" + + var serviceShapeId: String? = null + var packageName: String? = null + var packageVersion: String? = null + var packageDescription: String? = null + var sdkId: String? = null + + internal var buildSettings: SmithyKotlinBuildSettings? = null + fun buildSettings(configure: SmithyKotlinBuildSettings.() -> Unit) { + if (buildSettings == null) buildSettings = SmithyKotlinBuildSettings() + buildSettings!!.apply(configure) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SmithyKotlinPluginSettings + + if (serviceShapeId != other.serviceShapeId) return false + if (packageName != other.packageName) return false + if (packageVersion != other.packageVersion) return false + if (packageDescription != other.packageDescription) return false + if (sdkId != other.sdkId) return false + if (buildSettings != other.buildSettings) return false + + return true + } + + override fun hashCode(): Int { + var result = serviceShapeId?.hashCode() ?: 0 + result = 31 * result + (packageName?.hashCode() ?: 0) + result = 31 * result + (packageVersion?.hashCode() ?: 0) + result = 31 * result + (packageDescription?.hashCode() ?: 0) + result = 31 * result + (sdkId?.hashCode() ?: 0) + result = 31 * result + (buildSettings?.hashCode() ?: 0) + return result + } + + override fun toNode(): Node { + val obj = ObjectNode.objectNodeBuilder() + .withMember("service", serviceShapeId!!) + .withObjectMember("package") { + withMember("name", packageName!!) + withNullableMember("version", packageVersion) + withNullableMember("description", packageDescription) + } + .withNullableMember("sdkId", sdkId) + .withNullableMember("build", buildSettings) + + return obj.build() + } +} + +fun SmithyProjection.smithyKotlinPlugin(configure: SmithyKotlinPluginSettings.() -> Unit) { + val p = plugins.computeIfAbsent("kotlin-codegen") { SmithyKotlinPluginSettings() } as SmithyKotlinPluginSettings + p.apply(configure) +} diff --git a/gradle/sdk-plugins/src/main/kotlin/aws/sdk/kotlin/gradle/codegen/dsl/SmithyProjection.kt b/gradle/sdk-plugins/src/main/kotlin/aws/sdk/kotlin/gradle/codegen/dsl/SmithyProjection.kt new file mode 100644 index 00000000000..9df648823d4 --- /dev/null +++ b/gradle/sdk-plugins/src/main/kotlin/aws/sdk/kotlin/gradle/codegen/dsl/SmithyProjection.kt @@ -0,0 +1,90 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +package aws.sdk.kotlin.gradle.codegen.dsl + +import software.amazon.smithy.model.node.ArrayNode +import software.amazon.smithy.model.node.Node +import software.amazon.smithy.model.node.ObjectNode + +/** + * A container for settings related to a single Smithy projection. + * + * See https://awslabs.github.io/smithy/1.0/guides/building-models/build-config.html#projections + */ +class SmithyProjection( + /** + * The name of the projection + */ + val name: String, + + // FIXME - technically this is based on plugin. Should the projection root dir be based on plugin as well rather than a single field? + /** + * Root directory for this projection + */ + val projectionRootDir: java.io.File, +) { + + /** + * List of files/directories to import when building the projection + */ + var imports: List = emptyList() + + /** + * A list of transforms to apply + * + * See https://awslabs.github.io/smithy/1.0/guides/building-models/build-config.html#transforms + */ + var transforms: List = emptyList() + + /** + * Plugin name to plugin settings. Plugins should provide an extension function to configure their own plugin settings + */ + val plugins: MutableMap = mutableMapOf() + + internal fun toNode(): Node { + // escape windows paths for valid json + val formattedImports = imports + .map { it.replace("\\", "\\\\") } + + val transformNodes = transforms.map { Node.parse(it) } + val obj = ObjectNode.objectNodeBuilder() + .withArrayMember("imports", formattedImports) + .withMember("transforms", ArrayNode.fromNodes(transformNodes)) + + if (plugins.isNotEmpty()) { + obj.withObjectMember("plugins") { + plugins.forEach { (pluginName, pluginSettings) -> + withMember(pluginName, pluginSettings.toNode()) + } + } + } + return obj.build() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SmithyProjection + + if (name != other.name) return false + if (projectionRootDir != other.projectionRootDir) return false + if (imports != other.imports) return false + if (transforms != other.transforms) return false + if (plugins != other.plugins) return false + + return true + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + projectionRootDir.hashCode() + result = 31 * result + imports.hashCode() + result = 31 * result + transforms.hashCode() + result = 31 * result + plugins.hashCode() + return result + } +} diff --git a/gradle/sdk-plugins/src/main/kotlin/aws/sdk/kotlin/gradle/codegen/dsl/Utils.kt b/gradle/sdk-plugins/src/main/kotlin/aws/sdk/kotlin/gradle/codegen/dsl/Utils.kt new file mode 100644 index 00000000000..95f12d8a86e --- /dev/null +++ b/gradle/sdk-plugins/src/main/kotlin/aws/sdk/kotlin/gradle/codegen/dsl/Utils.kt @@ -0,0 +1,52 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +package aws.sdk.kotlin.gradle.codegen.dsl + +import aws.sdk.kotlin.gradle.codegen.CODEGEN_EXTENSION_NAME +import aws.sdk.kotlin.gradle.codegen.CodegenExtension +import org.gradle.api.Project +import org.gradle.api.plugins.ExtensionAware +import org.gradle.kotlin.dsl.get +import software.amazon.smithy.model.node.ArrayNode +import software.amazon.smithy.model.node.Node +import software.amazon.smithy.model.node.ObjectNode +import software.amazon.smithy.model.node.ToNode +import java.util.* + +/** + * Get the root directory of the generated kotlin code for a projection + */ +fun Project.projectionRootDir(projectionName: String): java.io.File = + file("${project.buildDir}/smithyprojections/${project.name}/$projectionName/kotlin-codegen") + +/** + * Get the [CodegenExtension] instance configured for the project + */ +internal val Project.codegenExtension: CodegenExtension + get() = ((this as ExtensionAware).extensions[CODEGEN_EXTENSION_NAME] as? CodegenExtension) ?: error("CodegenPlugin has not been applied") + +internal fun ObjectNode.Builder.withObjectMember(key: String, block: ObjectNode.Builder.() -> Unit): ObjectNode.Builder { + val builder = ObjectNode.objectNodeBuilder() + builder.apply(block) + return withMember(key, builder.build()) +} +internal fun ObjectNode.Builder.withNullableMember(key: String, member: String?): ObjectNode.Builder = apply { + if (member == null) return this + return withMember(key, member) +} + +internal fun ObjectNode.Builder.withNullableMember(key: String, member: Boolean?): ObjectNode.Builder = apply { + if (member == null) return this + return withMember(key, member) +} + +internal fun ObjectNode.Builder.withNullableMember(key: String, member: T?): ObjectNode.Builder = + withOptionalMember(key, Optional.ofNullable(member)) + +internal fun ObjectNode.Builder.withArrayMember(key: String, member: List): ObjectNode.Builder = apply { + val arrNode = member.map { Node.from(it) }.let { ArrayNode.fromNodes(it) } + return withMember(key, arrNode) +} diff --git a/gradle/sdk-plugins/src/main/kotlin/aws/sdk/kotlin/gradle/codegen/tasks/SmithyTasks.kt b/gradle/sdk-plugins/src/main/kotlin/aws/sdk/kotlin/gradle/codegen/tasks/SmithyTasks.kt new file mode 100644 index 00000000000..774884f15e5 --- /dev/null +++ b/gradle/sdk-plugins/src/main/kotlin/aws/sdk/kotlin/gradle/codegen/tasks/SmithyTasks.kt @@ -0,0 +1,121 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +package aws.sdk.kotlin.gradle.codegen.tasks + +import aws.sdk.kotlin.gradle.codegen.dsl.SmithyProjection +import aws.sdk.kotlin.gradle.codegen.dsl.codegenExtension +import aws.sdk.kotlin.gradle.codegen.dsl.withObjectMember +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.provideDelegate +import org.gradle.kotlin.dsl.register +import software.amazon.smithy.gradle.tasks.SmithyBuild +import software.amazon.smithy.model.node.Node + +private const val GENERATE_SMITHY_BUILD_CONFIG_TASK_NAME = "generateSmithyBuildConfig" +private const val GENERATE_SMITHY_PROJECTIONS_TASK_NAME = "generateSmithyProjections" + +internal fun Project.registerCodegenTasks() { + // generate the projection file for smithy to consume + val smithyBuildConfig = buildDir.resolve("smithy-build.json") + val generateSmithyBuild = tasks.register(GENERATE_SMITHY_BUILD_CONFIG_TASK_NAME) { + description = "generate smithy-build.json" + group = "codegen" + + // set an input property based on a hash of all the projections to get this task's + // up-to-date checks to work correctly (model files are configured as an input to the actual build task) + val projectionHash = project.objects.property(Int::class.java) + projectionHash.set(0) + project.codegenExtension.projections.all { + projectionHash.set(projectionHash.get() + hashCode()) + } + inputs.property("projectionHash", projectionHash) + outputs.file(smithyBuildConfig) + doFirst { + if (smithyBuildConfig.exists()) { + smithyBuildConfig.delete() + } + } + doLast { + buildDir.mkdir() + val extension = project.codegenExtension + val projections = extension.projections.asMap + smithyBuildConfig.writeText(generateSmithyBuild(projections.values)) + } + } + + val codegenConfig = createCodegenConfiguration() + project.tasks.register(GENERATE_SMITHY_PROJECTIONS_TASK_NAME) { + dependsOn(generateSmithyBuild) + description = "generate projections (code) using Smithy" + group = "codegen" + classpath = codegenConfig + smithyBuildConfigs = files(smithyBuildConfig) + + inputs.file(smithyBuildConfig) + + val extension = project.codegenExtension + + // every time a projection is added wire up the imports and outputs appropriately for this task + extension.projections.all { + imports.forEach { importPath -> + val f = project.file(importPath) + if (f.exists()) { + if (f.isDirectory) inputs.dir(f) else inputs.file(f) + } + } + outputs.dir(projectionRootDir) + } + + // ensure smithy-aws-kotlin-codegen is up to date + inputs.files(codegenConfig) + } +} + +/** + * Generate the "smithy-build.json" defining the projection + */ +private fun generateSmithyBuild(projections: Collection): String { + val buildConfig = Node.objectNodeBuilder() + .withMember("version", "1.0") + .withObjectMember("projections") { + projections.forEach { projection -> + withMember(projection.name, projection.toNode()) + } + } + .build() + + return Node.prettyPrintJson(buildConfig) +} + +// create a configuration (classpath) needed by the SmithyBuild task +private fun Project.createCodegenConfiguration(): Configuration { + val codegenConfig = configurations.maybeCreate("codegenTaskConfiguration") + codegenConfig.extendsFrom(createSmithyCliConfiguration()) + + dependencies { + // depend on aws-kotlin code generation + codegenConfig(project(":codegen:smithy-aws-kotlin-codegen")) + } + + return codegenConfig +} + +internal fun Project.createSmithyCliConfiguration(): Configuration { + // see: https://github.com/awslabs/smithy-gradle-plugin/blob/main/src/main/java/software/amazon/smithy/gradle/SmithyPlugin.java#L119 + val smithyCliConfig = configurations.maybeCreate("smithyCli") + dependencies { + // smithy plugin requires smithy-cli to be on the classpath, for whatever reason configuring the plugin + // from this plugin doesn't work correctly so we explicitly set it + val smithyVersion: String by project + smithyCliConfig("software.amazon.smithy:smithy-cli:$smithyVersion") + + // add aws traits to the compile classpath so that the smithy build task can discover them + smithyCliConfig("software.amazon.smithy:smithy-aws-traits:$smithyVersion") + } + return smithyCliConfig +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 99c1ab3f9f3..4fb9ab34839 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -23,6 +23,8 @@ pluginManagement { rootProject.name = "aws-sdk-kotlin" +includeBuild("./gradle/sdk-plugins") + include(":dokka-aws") include(":codegen:sdk") include(":codegen:smithy-aws-kotlin-codegen")