diff --git a/compiler-plugin/settings.gradle.kts b/compiler-plugin/settings.gradle.kts index dc064c5e2..b7ad5e63e 100644 --- a/compiler-plugin/settings.gradle.kts +++ b/compiler-plugin/settings.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ rootProject.name = "compiler-plugin" @@ -15,6 +15,7 @@ plugins { id("conventions-repositories") id("conventions-version-resolution") id("conventions-develocity") + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" } includeRootAsPublic() diff --git a/dokka-plugin/settings.gradle.kts b/dokka-plugin/settings.gradle.kts index 42d761a1f..cc02f5e7a 100644 --- a/dokka-plugin/settings.gradle.kts +++ b/dokka-plugin/settings.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ rootProject.name = "dokka-rpc-plugin" @@ -15,4 +15,5 @@ plugins { id("conventions-repositories") id("conventions-version-resolution") id("conventions-develocity") + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" } diff --git a/gradle-conventions/src/main/kotlin/util/csm/template.kt b/gradle-conventions/src/main/kotlin/util/csm/template.kt index cbab27b58..ee10f5287 100644 --- a/gradle-conventions/src/main/kotlin/util/csm/template.kt +++ b/gradle-conventions/src/main/kotlin/util/csm/template.kt @@ -145,7 +145,7 @@ object CsmTemplateProcessor { throw GradleException("Wildcard is not allowed in 'from' part of kotlin version range: $from, $pattern") } - if (to.contains("-") || to.contains("-")) { + if (from.contains("-") || to.contains("-")) { throw GradleException("Non stable versions are not allowed in kotlin version range: $pattern") } diff --git a/gradle-plugin/api/gradle-plugin.api b/gradle-plugin/api/gradle-plugin.api new file mode 100644 index 000000000..1c4dcb973 --- /dev/null +++ b/gradle-plugin/api/gradle-plugin.api @@ -0,0 +1,241 @@ +public abstract interface annotation class kotlinx/rpc/RpcDangerousApi : java/lang/annotation/Annotation { +} + +public class kotlinx/rpc/RpcExtension { + public fun (Lorg/gradle/api/model/ObjectFactory;Lorg/gradle/api/Project;)V + public final fun getAnnotationTypeSafetyEnabled ()Lorg/gradle/api/provider/Provider; + public final fun getGrpc ()Lkotlinx/rpc/grpc/GrpcExtension; + public final fun getStrict ()Lkotlinx/rpc/RpcStrictModeExtension; + public final fun grpc (Lorg/gradle/api/Action;)V + public static synthetic fun grpc$default (Lkotlinx/rpc/RpcExtension;Lorg/gradle/api/Action;ILjava/lang/Object;)V + public final fun strict (Lorg/gradle/api/Action;)V +} + +public final class kotlinx/rpc/RpcGradlePlugin : org/gradle/api/Plugin { + public fun ()V + public synthetic fun apply (Ljava/lang/Object;)V + public fun apply (Lorg/gradle/api/Project;)V +} + +public final class kotlinx/rpc/RpcStrictMode : java/lang/Enum { + public static final field ERROR Lkotlinx/rpc/RpcStrictMode; + public static final field NONE Lkotlinx/rpc/RpcStrictMode; + public static final field WARNING Lkotlinx/rpc/RpcStrictMode; + public static fun valueOf (Ljava/lang/String;)Lkotlinx/rpc/RpcStrictMode; + public static fun values ()[Lkotlinx/rpc/RpcStrictMode; +} + +public class kotlinx/rpc/RpcStrictModeExtension { + public fun (Lorg/gradle/api/model/ObjectFactory;)V + public final fun getFields ()Lorg/gradle/api/provider/Property; + public final fun getNestedFlow ()Lorg/gradle/api/provider/Property; + public final fun getNotTopLevelServerFlow ()Lorg/gradle/api/provider/Property; + public final fun getSharedFlow ()Lorg/gradle/api/provider/Property; + public final fun getStateFlow ()Lorg/gradle/api/provider/Property; + public final fun getStreamScopedFunctions ()Lorg/gradle/api/provider/Property; + public final fun getSuspendingServerStreaming ()Lorg/gradle/api/provider/Property; +} + +public final class kotlinx/rpc/VersionsKt { + public static final field BUF_TOOL_VERSION Ljava/lang/String; + public static final field GRPC_KOTLIN_VERSION Ljava/lang/String; + public static final field GRPC_VERSION Ljava/lang/String; + public static final field LIBRARY_VERSION Ljava/lang/String; + public static final field PLUGIN_VERSION Ljava/lang/String; + public static final field PROTOBUF_VERSION Ljava/lang/String; +} + +public class kotlinx/rpc/buf/BufExtension { + public fun (Lorg/gradle/api/model/ObjectFactory;)V + public final fun generate (Lorg/gradle/api/Action;)V + public final fun getConfigFile ()Lorg/gradle/api/provider/Property; + public final fun getGenerate ()Lkotlinx/rpc/buf/BufGenerateExtension; + public final fun getLogFormat ()Lorg/gradle/api/provider/Property; + public final fun getTasks ()Lkotlinx/rpc/buf/BufTasksExtension; + public final fun getTimeout ()Lorg/gradle/api/provider/Property; + public final fun tasks (Lorg/gradle/api/Action;)V +} + +public final class kotlinx/rpc/buf/BufExtension$LogFormat : java/lang/Enum { + public static final field Color Lkotlinx/rpc/buf/BufExtension$LogFormat; + public static final field Default Lkotlinx/rpc/buf/BufExtension$LogFormat; + public static final field Json Lkotlinx/rpc/buf/BufExtension$LogFormat; + public static final field Text Lkotlinx/rpc/buf/BufExtension$LogFormat; + public static fun valueOf (Ljava/lang/String;)Lkotlinx/rpc/buf/BufExtension$LogFormat; + public static fun values ()[Lkotlinx/rpc/buf/BufExtension$LogFormat; +} + +public class kotlinx/rpc/buf/BufGenerateExtension { + public fun (Lorg/gradle/api/Project;)V + public final fun getErrorFormat ()Lorg/gradle/api/provider/Property; + public final fun getIncludeImports ()Lorg/gradle/api/provider/Property; + public final fun getIncludeWkt ()Lorg/gradle/api/provider/Property; +} + +public final class kotlinx/rpc/buf/BufGenerateExtension$ErrorFormat : java/lang/Enum { + public static final field Default Lkotlinx/rpc/buf/BufGenerateExtension$ErrorFormat; + public static final field GithubActions Lkotlinx/rpc/buf/BufGenerateExtension$ErrorFormat; + public static final field Json Lkotlinx/rpc/buf/BufGenerateExtension$ErrorFormat; + public static final field Junit Lkotlinx/rpc/buf/BufGenerateExtension$ErrorFormat; + public static final field Msvs Lkotlinx/rpc/buf/BufGenerateExtension$ErrorFormat; + public static final field Text Lkotlinx/rpc/buf/BufGenerateExtension$ErrorFormat; + public static fun valueOf (Ljava/lang/String;)Lkotlinx/rpc/buf/BufGenerateExtension$ErrorFormat; + public static fun values ()[Lkotlinx/rpc/buf/BufGenerateExtension$ErrorFormat; +} + +public class kotlinx/rpc/buf/BufTasksExtension { + public fun (Lorg/gradle/api/Project;)V + public final fun registerWorkspaceTask (Lkotlin/reflect/KClass;Ljava/lang/String;Lorg/gradle/api/Action;)Lorg/gradle/api/provider/Provider; +} + +public final class kotlinx/rpc/buf/ConstsKt { + public static final field BUF_EXECUTABLE_CONFIGURATION Ljava/lang/String; + public static final field BUF_GEN_YAML Ljava/lang/String; + public static final field BUF_YAML Ljava/lang/String; +} + +public abstract class kotlinx/rpc/buf/tasks/BufExecTask : org/gradle/api/DefaultTask { + public fun ()V + public abstract fun getArgs ()Lorg/gradle/api/provider/ListProperty; + public abstract fun getBufTimeoutInWholeSeconds ()Lorg/gradle/api/provider/Property; + public abstract fun getCommand ()Lorg/gradle/api/provider/Property; + public abstract fun getConfigFile ()Lorg/gradle/api/provider/Property; + public abstract fun getLogFormat ()Lorg/gradle/api/provider/Property; + public abstract fun getWorkingDir ()Lorg/gradle/api/provider/Property; +} + +public final class kotlinx/rpc/buf/tasks/BufExecTaskKt { + public static final fun registerBufExecTask (Lorg/gradle/api/Project;Lkotlin/reflect/KClass;Ljava/lang/String;Lorg/gradle/api/provider/Provider;Lkotlin/jvm/functions/Function1;)Lorg/gradle/api/tasks/TaskProvider; + public static synthetic fun registerBufExecTask$default (Lorg/gradle/api/Project;Lkotlin/reflect/KClass;Ljava/lang/String;Lorg/gradle/api/provider/Provider;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/gradle/api/tasks/TaskProvider; +} + +public abstract class kotlinx/rpc/buf/tasks/BufGenerateTask : kotlinx/rpc/buf/tasks/BufExecTask { + public static final field NAME_PREFIX Ljava/lang/String; + public fun ()V + public abstract fun getAdditionalArgs ()Lorg/gradle/api/provider/ListProperty; + public abstract fun getErrorFormat ()Lorg/gradle/api/provider/Property; + public abstract fun getIncludeImports ()Lorg/gradle/api/provider/Property; + public abstract fun getIncludeWkt ()Lorg/gradle/api/provider/Property; + public abstract fun getOutputDirectory ()Lorg/gradle/api/provider/Property; +} + +public abstract class kotlinx/rpc/buf/tasks/GenerateBufGenYaml : org/gradle/api/DefaultTask { + public static final field NAME_PREFIX Ljava/lang/String; + public fun ()V + public abstract fun getBufGenFile ()Lorg/gradle/api/provider/Property; +} + +public final class kotlinx/rpc/buf/tasks/GenerateBufGenYamlKt$inlined$sam$i$org_gradle_api_Action$0 : org/gradle/api/Action { + public fun (Lkotlin/jvm/functions/Function1;)V + public final synthetic fun execute (Ljava/lang/Object;)V +} + +public abstract class kotlinx/rpc/buf/tasks/GenerateBufYaml : org/gradle/api/DefaultTask { + public static final field NAME_PREFIX Ljava/lang/String; + public fun ()V + public abstract fun getBufFile ()Lorg/gradle/api/provider/Property; +} + +public final class kotlinx/rpc/buf/tasks/GenerateBufYamlKt$inlined$sam$i$org_gradle_api_Action$0 : org/gradle/api/Action { + public fun (Lkotlin/jvm/functions/Function1;)V + public final synthetic fun execute (Ljava/lang/Object;)V +} + +public abstract interface class kotlinx/rpc/grpc/GrpcExtension { + public abstract fun buf (Lorg/gradle/api/Action;)V + public abstract fun getBuf ()Lkotlinx/rpc/buf/BufExtension; + public abstract fun getProtocPlugins ()Lorg/gradle/api/NamedDomainObjectContainer; + public abstract fun protocPlugins (Lorg/gradle/api/Action;)V +} + +public final class kotlinx/rpc/proto/ConstsKt { + public static final field KXRPC_PLUGIN_JAR_CONFIGURATION Ljava/lang/String; + public static final field PROTO_BUILD_DIR Ljava/lang/String; + public static final field PROTO_BUILD_GENERATED Ljava/lang/String; + public static final field PROTO_BUILD_SOURCE_SETS Ljava/lang/String; + public static final field PROTO_FILES_DIR Ljava/lang/String; + public static final field PROTO_FILES_IMPORT_DIR Ljava/lang/String; + public static final field PROTO_GROUP Ljava/lang/String; + public static final field PROTO_SOURCE_DIRECTORY_NAME Ljava/lang/String; + public static final field PROTO_SOURCE_SETS Ljava/lang/String; +} + +public final class kotlinx/rpc/proto/KxrpcPluginJarKt { + public static final fun getKxrpcProtocPluginJarPath (Lorg/gradle/api/Project;)Lorg/gradle/api/provider/Provider; +} + +public abstract class kotlinx/rpc/proto/ProcessProtoFiles : org/gradle/api/tasks/Copy { + public fun ()V +} + +public final class kotlinx/rpc/proto/ProcessProtoFilesKt$inlined$sam$i$org_gradle_api_Action$0 : org/gradle/api/Action { + public fun (Lkotlin/jvm/functions/Function1;)V + public final synthetic fun execute (Ljava/lang/Object;)V +} + +public abstract interface class kotlinx/rpc/proto/ProtoSourceSet { + public abstract fun getName ()Ljava/lang/String; + public abstract fun getProto ()Lorg/gradle/api/file/SourceDirectorySet; + public fun proto (Lorg/gradle/api/Action;)V + public abstract fun protocPlugin (Lkotlinx/rpc/proto/ProtocPlugin;)V + public abstract fun protocPlugin (Lorg/gradle/api/NamedDomainObjectProvider;)V +} + +public class kotlinx/rpc/proto/ProtocPlugin { + public static final field Companion Lkotlinx/rpc/proto/ProtocPlugin$Companion; + public static final field GRPC_JAVA Ljava/lang/String; + public static final field GRPC_KOTLIN Ljava/lang/String; + public static final field KXRPC Ljava/lang/String; + public static final field PROTOBUF_JAVA Ljava/lang/String; + public fun (Ljava/lang/String;Lorg/gradle/api/Project;)V + public final fun getArtifact ()Lorg/gradle/api/provider/Property; + public final fun getExcludeTypes ()Lorg/gradle/api/provider/ListProperty; + public final fun getIncludeImports ()Lorg/gradle/api/provider/Property; + public final fun getIncludeWkt ()Lorg/gradle/api/provider/Property; + public final fun getName ()Ljava/lang/String; + public final fun getOptions ()Lorg/gradle/api/provider/MapProperty; + public final fun getStrategy ()Lorg/gradle/api/provider/Property; + public final fun getTypes ()Lorg/gradle/api/provider/ListProperty; + public final fun isJava ()Lorg/gradle/api/provider/Property; + public final fun local (Lorg/gradle/api/Action;)V + public final fun remote (Lorg/gradle/api/Action;)V +} + +public abstract class kotlinx/rpc/proto/ProtocPlugin$Artifact { +} + +public final class kotlinx/rpc/proto/ProtocPlugin$Artifact$Local : kotlinx/rpc/proto/ProtocPlugin$Artifact { + public fun (Lorg/gradle/api/Project;)V + public final fun executor (Lorg/gradle/api/provider/Provider;)V + public final fun getExecutor ()Lorg/gradle/api/provider/ListProperty; + public final fun javaJar (Ljava/lang/String;)V + public final fun javaJar (Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;)V + public static synthetic fun javaJar$default (Lkotlinx/rpc/proto/ProtocPlugin$Artifact$Local;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;ILjava/lang/Object;)V +} + +public final class kotlinx/rpc/proto/ProtocPlugin$Artifact$Remote : kotlinx/rpc/proto/ProtocPlugin$Artifact { + public fun (Lorg/gradle/api/Project;)V + public final fun getLocator ()Lorg/gradle/api/provider/Property; +} + +public final class kotlinx/rpc/proto/ProtocPlugin$Companion { +} + +public final class kotlinx/rpc/proto/ProtocPlugin$Strategy : java/lang/Enum { + public static final field All Lkotlinx/rpc/proto/ProtocPlugin$Strategy; + public static final field Directory Lkotlinx/rpc/proto/ProtocPlugin$Strategy; + public static fun valueOf (Ljava/lang/String;)Lkotlinx/rpc/proto/ProtocPlugin$Strategy; + public static fun values ()[Lkotlinx/rpc/proto/ProtocPlugin$Strategy; +} + +public final class kotlinx/rpc/proto/ProtocPluginKt { + public static final fun getGrpcJava (Lorg/gradle/api/NamedDomainObjectContainer;)Lorg/gradle/api/NamedDomainObjectProvider; + public static final fun getGrpcKotlin (Lorg/gradle/api/NamedDomainObjectContainer;)Lorg/gradle/api/NamedDomainObjectProvider; + public static final fun getKxrpc (Lorg/gradle/api/NamedDomainObjectContainer;)Lorg/gradle/api/NamedDomainObjectProvider; + public static final fun getProtobufJava (Lorg/gradle/api/NamedDomainObjectContainer;)Lorg/gradle/api/NamedDomainObjectProvider; + public static final fun grpcJava (Lorg/gradle/api/NamedDomainObjectContainer;Lorg/gradle/api/Action;)V + public static final fun grpcKotlin (Lorg/gradle/api/NamedDomainObjectContainer;Lorg/gradle/api/Action;)V + public static final fun kxrpc (Lorg/gradle/api/NamedDomainObjectContainer;Lorg/gradle/api/Action;)V + public static final fun protobufJava (Lorg/gradle/api/NamedDomainObjectContainer;Lorg/gradle/api/Action;)V +} + diff --git a/gradle-plugin/build.gradle.kts b/gradle-plugin/build.gradle.kts index fddce380c..4ccce943b 100644 --- a/gradle-plugin/build.gradle.kts +++ b/gradle-plugin/build.gradle.kts @@ -16,10 +16,11 @@ version = rootProject.libs.versions.kotlinx.rpc.get() kotlin { explicitApi() + + jvmToolchain(11) } dependencies { - implementation(libs.protobuf.gradle.plugin) compileOnly(libs.kotlin.gradle.plugin) } @@ -50,6 +51,7 @@ abstract class GeneratePluginVersionTask @Inject constructor( @get:Input val protobufVersion: String, @get:Input val grpcVersion: String, @get:Input val grpcKotlinVersion: String, + @get:Input val bufToolVersion: String, @get:OutputDirectory val sourcesDir: File ) : DefaultTask() { @TaskAction @@ -58,30 +60,56 @@ abstract class GeneratePluginVersionTask @Inject constructor( sourceFile.writeText( """ - package kotlinx.rpc +// This file is generated by a $NAME gradle task. Do not modify manually. + +package kotlinx.rpc - public const val LIBRARY_VERSION: String = "$libraryVersion" +/** + * The version of the kotlinx.rpc library. + */ +public const val LIBRARY_VERSION: String = "$libraryVersion" - @Deprecated("Use kotlinx.rpc.LIBRARY_VERSION instead", ReplaceWith("kotlinx.rpc.LIBRARY_VERSION")) - public const val PLUGIN_VERSION: String = LIBRARY_VERSION +@Deprecated("Use kotlinx.rpc.LIBRARY_VERSION instead", ReplaceWith("kotlinx.rpc.LIBRARY_VERSION")) +public const val PLUGIN_VERSION: String = LIBRARY_VERSION - public const val PROTOBUF_VERSION: String = "$protobufVersion" - public const val GRPC_VERSION: String = "$grpcVersion" - public const val GRPC_KOTLIN_VERSION: String = "$grpcKotlinVersion" - - """.trimIndent() +/** + * The version of the protobuf library. + */ +public const val PROTOBUF_VERSION: String = "$protobufVersion" + +/** + * The version of the grpc java library. + */ +public const val GRPC_VERSION: String = "$grpcVersion" + +/** + * The version of the grpc kotlin library. + */ +public const val GRPC_KOTLIN_VERSION: String = "$grpcKotlinVersion" + +/** + * The version of the buf tool used to generate protobuf. + */ +public const val BUF_TOOL_VERSION: String = "$bufToolVersion" + +""".trimIndent() ) } + + companion object { + const val NAME = "generatePluginVersion" + } } val sourcesDir = File(project.layout.buildDirectory.asFile.get(), "generated-sources/pluginVersion") val generatePluginVersionTask = tasks.register( - "generatePluginVersion", + GeneratePluginVersionTask.NAME, version.toString(), - libs.versions.protobuf.asProvider().get().toString(), - libs.versions.grpc.asProvider().get().toString(), - libs.versions.grpc.kotlin.get().toString(), + libs.versions.protobuf.asProvider().get(), + libs.versions.grpc.asProvider().get(), + libs.versions.grpc.kotlin.get(), + libs.versions.buf.tool.get(), sourcesDir, ) diff --git a/gradle-plugin/settings.gradle.kts b/gradle-plugin/settings.gradle.kts index df60f9e3d..264b38dd0 100644 --- a/gradle-plugin/settings.gradle.kts +++ b/gradle-plugin/settings.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ rootProject.name = "gradle-plugin" @@ -14,6 +14,7 @@ pluginManagement { plugins { id("conventions-repositories") id("conventions-version-resolution") + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" } includeRootAsPublic() diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/Extensions.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/Extensions.kt index eeb1d81cd..d02d8d458 100644 --- a/gradle-plugin/src/main/kotlin/kotlinx/rpc/Extensions.kt +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/Extensions.kt @@ -6,6 +6,8 @@ package kotlinx.rpc +import kotlinx.rpc.grpc.DefaultGrpcExtension +import kotlinx.rpc.grpc.GrpcExtension import org.gradle.api.Action import org.gradle.api.Project import org.gradle.api.model.ObjectFactory @@ -14,11 +16,12 @@ import org.gradle.kotlin.dsl.findByType import org.gradle.api.provider.Provider import org.gradle.kotlin.dsl.newInstance import org.gradle.kotlin.dsl.property +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject -public fun Project.rpcExtension(): RpcExtension = extensions.findByType() ?: RpcExtension(objects) +internal fun Project.rpcExtension(): RpcExtension = extensions.findByType() ?: RpcExtension(objects, this) -public open class RpcExtension @Inject constructor(objects: ObjectFactory) { +public open class RpcExtension @Inject constructor(objects: ObjectFactory, private val project: Project) { /** * Controls `@Rpc` [annotation type-safety](https://github.com/Kotlin/kotlinx-rpc/pull/240) compile-time checkers. * @@ -42,15 +45,20 @@ public open class RpcExtension @Inject constructor(objects: ObjectFactory) { configure.execute(strict) } + internal val grpcApplied = AtomicBoolean(false) + /** * Grpc settings. */ - public val grpc: GrpcExtension = objects.newInstance() + public val grpc: GrpcExtension by lazy { + grpcApplied.set(true) + objects.newInstance() + } /** * Grpc settings. */ - public fun grpc(configure: Action) { + public fun grpc(configure: Action = Action {}) { configure.execute(grpc) } } diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/GrpcExtension.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/GrpcExtension.kt deleted file mode 100644 index 6a9c1674e..000000000 --- a/gradle-plugin/src/main/kotlin/kotlinx/rpc/GrpcExtension.kt +++ /dev/null @@ -1,262 +0,0 @@ -/* - * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.rpc - -import com.google.protobuf.gradle.ExecutableLocator -import com.google.protobuf.gradle.GenerateProtoTask -import com.google.protobuf.gradle.ProtobufExtension -import org.gradle.api.Action -import org.gradle.api.GradleException -import org.gradle.api.Project -import org.gradle.api.model.ObjectFactory -import org.gradle.api.provider.Property -import org.gradle.api.specs.Spec -import org.gradle.api.tasks.TaskCollection -import org.gradle.kotlin.dsl.findByType -import org.gradle.kotlin.dsl.property -import org.gradle.kotlin.dsl.the -import javax.inject.Inject - -public open class GrpcExtension @Inject constructor(objects: ObjectFactory, private val project: Project) { - /** - * Determines whether the gRPC support is enabled in the project. - * - * Allows for additional configuration checks. - */ - public val enabled: Property = objects.property().convention(false) - - /** - * Access for [GrpcPlugin] for [LOCATOR_NAME] name. - */ - public fun plugin(action: Action) { - pluginAccess(action, LOCATOR_NAME) - } - - /** - * Access for [GrpcPlugin] for [GRPC_JAVA_LOCATOR_NAME] name. - */ - public fun grpcJavaPlugin(action: Action) { - pluginAccess(action, GRPC_JAVA_LOCATOR_NAME) - } - - /** - * Access for [GrpcPlugin] for [GRPC_KOTLIN_LOCATOR_NAME] name. - */ - public fun grpcKotlinPlugin(action: Action) { - pluginAccess(action, GRPC_KOTLIN_LOCATOR_NAME) - } - - /** - * Shortcut for - * - * ```kotlin - * protobuf { - * generateProtoTasks.all().all { ... } - * } - * ``` - */ - public fun tasks(taskAction: Action) { - project.the().generateProtoTasks.all().all(taskAction) - } - - /** - * Shortcut for - * - * ```kotlin - * protobuf { - * generateProtoTasks.all().matching { ... } - * } - * ``` - */ - public fun tasksMatching(spec: Spec): TaskCollection { - return project.the().generateProtoTasks.all().matching(spec) - } - - private fun pluginAccess(action: Action, locatorName: String) { - val extension = project.the() - val plugin = object : GrpcPlugin { - override fun options(optionsAction: Action) { - extension.generateProtoTasks.all().all { - optionsAction.execute(plugins.maybeCreate(locatorName)) - } - } - - override fun locator(locatorAction: Action) { - extension.plugins { - locatorAction.execute(maybeCreate(locatorName)) - } - } - } - - action.execute(plugin) - } - - public companion object { - /** - * [com.google.protobuf.gradle.ExecutableLocator]'s name for the `kotlinx-rpc` plugin - * - * ```kotlin - * protobuf { - * plugins { - * named(LOCATOR_NAME) - * } - * } - * ``` - * - * Same name is used for [GenerateProtoTask] plugin in `generateProtoTasks.plugins` - */ - public const val LOCATOR_NAME: String = "kotlinx-rpc" - - /** - * [com.google.protobuf.gradle.ExecutableLocator]'s name for the `grpc-java` plugin - * - * ```kotlin - * protobuf { - * plugins { - * named(GRPC_JAVA_LOCATOR_NAME) - * } - * } - * ``` - * - * Same name is used for [GenerateProtoTask] plugin in `generateProtoTasks.plugins` - */ - public const val GRPC_JAVA_LOCATOR_NAME: String = "grpc" - - /** - * [com.google.protobuf.gradle.ExecutableLocator]'s name for the `grpc-kotlin` plugin - * - * ```kotlin - * protobuf { - * plugins { - * named(GRPC_KOTLIN_LOCATOR_NAME) - * } - * } - * ``` - * - * Same name is used for [GenerateProtoTask] plugin in `generateProtoTasks.plugins` - */ - public const val GRPC_KOTLIN_LOCATOR_NAME: String = "grpckt" - } -} - -/** - * Access to a specific protobuf plugin. - */ -public interface GrpcPlugin { - /** - * Access for [GenerateProtoTask.PluginOptions] - * - * ```kotlin - * rpc { - * grpc { - * plugin { - * options { - * option("option=value") - * } - * } - * } - * } - * ``` - */ - public fun options(optionsAction: Action) - - /** - * Access for [ExecutableLocator] - * - * ```kotlin - * rpc { - * grpc { - * plugin { - * locator { - * path = "$buildDirPath/libs/protobuf-plugin-$version-all.jar" - * } - * } - * } - * } - * ``` - */ - public fun locator(locatorAction: Action) -} - -internal fun Project.configureGrpc() { - val grpc = rpcExtension().grpc - var wasApplied = false - - pluginManager.withPlugin("com.google.protobuf") { - if (wasApplied) { - return@withPlugin - } - - wasApplied = true - - val protobuf = extensions.findByType() - ?: run { - logger.error("Protobuf plugin (com.google.protobuf) was not applied. Please report of this issue.") - return@withPlugin - } - - protobuf.configureProtobuf(project = project) - } - - afterEvaluate { - if (grpc.enabled.get() && !wasApplied) { - throw GradleException( - """ - gRPC Support is enabled, but 'com.google.protobuf' was not be applied during project evaluation. - The 'com.google.protobuf' plugin must be applied to the project first. - """.trimIndent() - ) - } - } -} - -private fun ProtobufExtension.configureProtobuf(project: Project) { - val buildDirPath: String = project.layout.buildDirectory.get().asFile.absolutePath - - protoc { - artifact = "com.google.protobuf:protoc:$PROTOBUF_VERSION" - } - - plugins { - val existed = findByName(GrpcExtension.LOCATOR_NAME) != null - maybeCreate(GrpcExtension.LOCATOR_NAME).apply { - if (!existed) { - artifact = "org.jetbrains.kotlinx:kotlinx-rpc-protobuf-plugin:$LIBRARY_VERSION:all@jar" - } - } - - val grpcJavaPluginExisted = findByName(GrpcExtension.GRPC_JAVA_LOCATOR_NAME) != null - maybeCreate(GrpcExtension.GRPC_JAVA_LOCATOR_NAME).apply { - if (!grpcJavaPluginExisted) { - artifact = "io.grpc:protoc-gen-grpc-java:$GRPC_VERSION" - } - } - - val grpcKotlinPluginExisted = findByName(GrpcExtension.GRPC_KOTLIN_LOCATOR_NAME) != null - maybeCreate(GrpcExtension.GRPC_KOTLIN_LOCATOR_NAME).apply { - if (!grpcKotlinPluginExisted) { - artifact = "io.grpc:protoc-gen-grpc-kotlin:$GRPC_KOTLIN_VERSION:jdk8@jar" - } - } - } - - generateProtoTasks { - all().all { - plugins { - val existed = findByName(GrpcExtension.LOCATOR_NAME) != null - maybeCreate(GrpcExtension.LOCATOR_NAME).apply { - if (!existed) { - option("debugOutput=$buildDirPath/protobuf-plugin.log") - option("messageMode=interface") - } - } - - maybeCreate(GrpcExtension.GRPC_JAVA_LOCATOR_NAME) - - maybeCreate(GrpcExtension.GRPC_KOTLIN_LOCATOR_NAME) - } - } - } -} diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/RpcGradlePlugin.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/RpcGradlePlugin.kt index ff83bc2fa..66442901c 100644 --- a/gradle-plugin/src/main/kotlin/kotlinx/rpc/RpcGradlePlugin.kt +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/RpcGradlePlugin.kt @@ -4,6 +4,8 @@ package kotlinx.rpc +import kotlinx.rpc.grpc.configurePluginProtections +import kotlinx.rpc.proto.createProtoExtensions import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.create @@ -15,7 +17,8 @@ public class RpcGradlePlugin : Plugin { applyCompilerPlugin(target) - target.configureGrpc() + target.createProtoExtensions() + target.configurePluginProtections() } private fun applyCompilerPlugin(target: Project) { diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/buf/BufExecutable.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/buf/BufExecutable.kt new file mode 100644 index 000000000..fd4a9acea --- /dev/null +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/buf/BufExecutable.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.buf + +import kotlinx.rpc.BUF_TOOL_VERSION +import kotlinx.rpc.buf.tasks.BufExecTask +import kotlinx.rpc.util.ProcessRunner +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.kotlin.dsl.dependencies + +// See: https://github.com/bufbuild/buf-gradle-plugin/blob/1bc48078880887797db3aa412d6a3fea60461276/src/main/kotlin/build/buf/gradle/BufSupport.kt#L28 +internal fun Project.configureBufExecutable() { + configurations.create(BUF_EXECUTABLE_CONFIGURATION) + + val os = System.getProperty("os.name").lowercase() + val osPart = + when { + os.startsWith("windows") -> "windows" + os.startsWith("linux") -> "linux" + os.startsWith("mac") -> "osx" + else -> error("unsupported os: $os") + } + + val archPart = + when (val arch = System.getProperty("os.arch").lowercase()) { + in setOf("x86_64", "amd64") -> "x86_64" + in setOf("arm64", "aarch64") -> "aarch_64" + else -> error("unsupported arch: $arch") + } + + dependencies { + add( + BUF_EXECUTABLE_CONFIGURATION, + mapOf( + "group" to "build.buf", + "name" to "buf", + "version" to BUF_TOOL_VERSION, + "classifier" to "$osPart-$archPart", + "ext" to "exe", + ), + ) + } +} + +internal fun BufExecTask.execBuf(args: Iterable) { + val executable = bufExecutable.get() + + if (!executable.canExecute()) { + executable.setExecutable(true) + } + + val baseArgs = buildList { + val configValue = configFile.orNull + if (configValue != null) { + add("--config") + add(configValue.absolutePath) + } + + if (debug.get()) { + add("--debug") + } + + val logFormatValue = logFormat.get() + if (logFormatValue != BufExtension.LogFormat.Default) { + add("--log-format") + add(logFormatValue.name.lowercase()) + } + + val timeoutValue = bufTimeoutInWholeSeconds.get() + if (timeoutValue != 0L) { + add("--timeout") + add("${timeoutValue}s") + } + } + + val processArgs = listOf(executable.absolutePath) + args + baseArgs + + val workingDirValue = workingDir.get() + + logger.debug("Running buf from {}: `buf {}`", workingDirValue, processArgs.joinToString(" ")) + + val result = ProcessRunner().use { it.shell("buf", workingDirValue, processArgs) } + + if (result.exitCode != 0) { + throw GradleException(result.formattedOutput()) + } else { + logger.debug(result.formattedOutput()) + } +} diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/buf/BufExtensions.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/buf/BufExtensions.kt new file mode 100644 index 000000000..7b66a5087 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/buf/BufExtensions.kt @@ -0,0 +1,229 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.buf + +import kotlinx.rpc.buf.tasks.BufExecTask +import org.gradle.api.Action +import org.gradle.api.Project +import org.gradle.api.provider.Property +import org.gradle.kotlin.dsl.property +import java.io.File +import javax.inject.Inject +import kotlin.time.Duration +import kotlinx.rpc.proto.ProtocPlugin +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Provider +import org.gradle.kotlin.dsl.listProperty +import kotlin.reflect.KClass + +/** + * Options for the Buf tasks. + * + * @see buf commands + */ +public open class BufExtension @Inject constructor(objects: ObjectFactory) { + /** + * `--config` argument value. + * + * @see buf commands + */ + public val configFile: Property = objects.property() + + /** + * `--log-format` option. + * + * @see buf --log-format + */ + public val logFormat: Property = objects.property().convention(LogFormat.Default) + + /** + * Possible values for `--log-format` [logFormat] option. + */ + public enum class LogFormat { + Text, + Color, + Json, + + /** + * Buf's default value. + */ + Default, + ; + } + + /** + * `--timeout` option. + * + * Value to Buf is passed in seconds using [Duration.inWholeSeconds]. + * + * @see buf --timeout + */ + public val timeout: Property = objects.property().convention(Duration.ZERO) + + /** + * `buf generate` options. + * + * @see "buf generate" command + * @see [BUF_GEN_YAML] + */ + public val generate: BufGenerateExtension = objects.newInstance(BufGenerateExtension::class.java) + + /** + * Configures the `buf generate` options. + * + * @see "buf generate" command + * @see [BUF_GEN_YAML] + */ + public fun generate(configure: Action) { + configure.execute(generate) + } + + /** + * Use this extension to register custom Buf tasks + * that will operate on the generated workspace. + */ + public val tasks: BufTasksExtension = objects.newInstance(BufTasksExtension::class.java) + + /** + * Use this extension to register custom Buf tasks + * that will operate on the generated workspace. + */ + public fun tasks(configure: Action) { + configure.execute(tasks) + } +} + +/** + * Allows registering custom Buf tasks that can operate on the generated workspace. + */ +public open class BufTasksExtension @Inject constructor(internal val project: Project) { + // TODO change to commonMain/commonTest in docs when it's supported KRPC-180 + /** + * Registers a custom Buf task that operates on the generated workspace. + * + * Name conventions: + * `lint` input for [name] will result in tasks + * named 'bufLintMain' and 'bufLintTest' for Kotlin/JVM projects + * and 'bufLintJvmMain' and 'bufLintJvmTest' for Kotlin/Multiplatform projects. + * + * Note the by default 'test' task doesn't depend on 'main' task. + */ + public fun registerWorkspaceTask( + kClass: KClass, + name: String, + configure: Action, + ): TaskProvider { + val mainProperty = project.objects.property(kClass) + val testProperty = project.objects.property(kClass) + + val provider = TaskProperty(mainProperty, testProperty) + + @Suppress("UNCHECKED_CAST") + customTasks.add(Definition(name, kClass, configure, provider as TaskProperty)) + + return provider + } + + // TODO change to commonMain/commonTest in docs when it's supported KRPC-180 + /** + * Registers a custom Buf task that operates on the generated workspace. + * + * Name conventions: + * `lint` input for [name] will result in tasks + * named 'bufLintMain' and 'bufLintTest' for Kotlin/JVM projects + * and 'bufLintJvmMain' and 'bufLintJvmTest' for Kotlin/Multiplatform projects. + * + * Note the by default 'test' task doesn't depend on 'main' task. + */ + public inline fun registerWorkspaceTask( + name: String, + configure: Action, + ): TaskProvider { + return registerWorkspaceTask(T::class, name, configure) + } + + internal val customTasks: ListProperty> = project.objects.listProperty() + + internal class Definition( + val name: String, + val kClass: KClass, + val configure: Action, + val property: TaskProperty, + ) + + /** + * Container for the main and test Buf tasks created by [BufTasksExtension.registerWorkspaceTask]. + */ + public sealed interface TaskProvider { + /** + * Task created via [BufTasksExtension.registerWorkspaceTask] and associated with the main source set. + */ + public val mainTask: Provider + + /** + * Task created via [BufTasksExtension.registerWorkspaceTask] and associated with the test source set. + */ + public val testTask: Provider + } + + internal class TaskProperty( + override val mainTask: Property, + override val testTask: Property, + ) : TaskProvider +} + +/** + * Options for the `buf generate` command. + * + * @see "buf generate" command + * @see [BUF_GEN_YAML] + */ +public open class BufGenerateExtension @Inject constructor(internal val project: Project) { + /** + * `--include-imports` option. + * + * @see + * buf generate --include-imports + * + * @see [ProtocPlugin.includeImports] + */ + public val includeImports: Property = project.objects.property().convention(false) + + /** + * `--include-wkt` option. + * + * @see buf generate --include-wkt + * @see [ProtocPlugin.includeWkt] + */ + public val includeWkt: Property = project.objects.property().convention(false) + + /** + * `--error-format` option. + * + * @see buf generate --error-format + */ + public val errorFormat: Property = project.objects.property() + .convention(ErrorFormat.Default) + + /** + * Possible values for `--error-format` option. + * + * @see buf generate --error-format + */ + public enum class ErrorFormat(internal val cliValue: String) { + Text("text"), + Json("json"), + Msvs("msvs"), + Junit("junit"), + GithubActions("github-actions"), + + /** + * Buf's default value. + */ + Default(""), + ; + } +} diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/buf/consts.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/buf/consts.kt new file mode 100644 index 000000000..02c00fb82 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/buf/consts.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.buf + +import org.gradle.api.artifacts.Configuration + +/** + * Name of the buf.gen.yaml file. + * + * @see buf.gen.yaml reference + */ +public const val BUF_GEN_YAML: String = "buf.gen.yaml" + +/** + * Name of the buf.yaml file. + * + * @see buf.yaml reference + */ +public const val BUF_YAML: String = "buf.yaml" + +/** + * [Configuration] name for buf executable. + * + * @see Buf + */ +public const val BUF_EXECUTABLE_CONFIGURATION: String = "bufExecutable" diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/buf/tasks/BufExecTask.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/buf/tasks/BufExecTask.kt new file mode 100644 index 000000000..a321684b9 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/buf/tasks/BufExecTask.kt @@ -0,0 +1,124 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.buf.tasks + +import kotlinx.rpc.buf.BUF_EXECUTABLE_CONFIGURATION +import kotlinx.rpc.buf.BufExtension +import kotlinx.rpc.buf.execBuf +import kotlinx.rpc.proto.PROTO_GROUP +import kotlinx.rpc.rpcExtension +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.TaskProvider +import org.gradle.kotlin.dsl.register +import java.io.File +import kotlin.reflect.KClass +import kotlinx.rpc.buf.BUF_GEN_YAML +import kotlinx.rpc.buf.BUF_YAML +import kotlinx.rpc.buf.BufTasksExtension +import org.gradle.api.logging.LogLevel + +/** + * Abstract base class for `buf` tasks. + */ +public abstract class BufExecTask : DefaultTask() { + init { + group = PROTO_GROUP + } + + @get:InputFile + internal abstract val bufExecutable: Property + + @get:Input + internal abstract val debug: Property + + /** + * The `buf` command to execute. + * + * Example: `build`, `generate`, `lint`, `mod`, `push`, `version`. + */ + @get:Input + public abstract val command: Property + + /** + * Arguments for the `buf` command. + */ + @get:Input + public abstract val args: ListProperty + + /** + * The working directory for the `buf` command. + */ + @get:InputDirectory + public abstract val workingDir: Property + + /** + * The `buf.yaml` file to use via `--config` option. + */ + @get:InputFile + @get:Optional + public abstract val configFile: Property + + /** + * @see [BufExtension.logFormat] + */ + @get:Input + public abstract val logFormat: Property + + /** + * @see [BufExtension.timeout] + */ + @get:Input + public abstract val bufTimeoutInWholeSeconds: Property + + @TaskAction + internal fun exec() { + execBuf(listOf(command.get()) + args.get()) + } +} + +/** + * Registers a [BufExecTask] of type [T]. + * + * Use it to create custom `buf` tasks. + * + * These tasks are NOT automatically configured + * to work with the generated [BUF_GEN_YAML] and [BUF_YAML] files and the corresponding workspace. + * + * For that use [BufTasksExtension.registerWorkspaceTask]. + */ +public inline fun Project.registerBufExecTask( + name: String, + workingDir: Provider, + noinline configuration: T.() -> Unit, +): TaskProvider = registerBufExecTask(T::class, name, workingDir, configuration) + +@PublishedApi +internal fun Project.registerBufExecTask( + clazz: KClass, + name: String, + workingDir: Provider, + configuration: T.() -> Unit = {}, +): TaskProvider = tasks.register(name, clazz) { + val executableConfiguration = configurations.getByName(BUF_EXECUTABLE_CONFIGURATION) + bufExecutable.set(executableConfiguration.singleFile) + this.workingDir.set(workingDir) + + val buf = project.rpcExtension().grpc.buf + configFile.set(buf.configFile) + logFormat.set(buf.logFormat) + bufTimeoutInWholeSeconds.set(buf.timeout.map { it.inWholeSeconds }) + debug.set(project.gradle.startParameter.logLevel == LogLevel.DEBUG) + + configuration() +} diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/buf/tasks/BufGenerateTask.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/buf/tasks/BufGenerateTask.kt new file mode 100644 index 000000000..88c2bf490 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/buf/tasks/BufGenerateTask.kt @@ -0,0 +1,129 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.buf.tasks + +import kotlinx.rpc.proto.PROTO_GROUP +import kotlinx.rpc.rpcExtension +import org.gradle.api.Project +import org.gradle.api.file.FileCollection +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskProvider +import java.io.File +import kotlinx.rpc.buf.BufGenerateExtension + +/** + * Buf `generate` command. + * + * @see buf generate + */ +public abstract class BufGenerateTask : BufExecTask() { + // unsued, but required for Gradle to properly recognize inputs + @get:InputFiles + internal abstract val protoFiles: ListProperty + + /** + * Whether to include imports. + * + * @see + * buf generate --include-imports + * + * @see [BufGenerateExtension.includeImports] + */ + @get:Input + public abstract val includeImports: Property + + /** + * Whether to include Well-Known Types. + * + * @see buf generate --include-wkt + * @see [BufGenerateExtension.includeWkt] + */ + @get:Input + public abstract val includeWkt: Property + + /** + * `--error-format` option. + * + * @see buf generate --error-format + * @see [BufGenerateExtension.errorFormat] + */ + @get:Input + public abstract val errorFormat: Property + + /** + * Additional arguments for `buf generate` command. + */ + @get:Input + @get:Optional + public abstract val additionalArgs: ListProperty + + /** + * The directory to output generated files. + */ + @get:OutputDirectory + public abstract val outputDirectory: Property + + init { + command.set("generate") + + val args = project.provider { + buildList { + add("--output"); add(outputDirectory.get().absolutePath) + + if (includeImports.get()) { + add("--include-imports") + } + + if (includeWkt.get()) { + add("--include-wkt") + } + + val errorFormatValue = errorFormat.get() + if (errorFormatValue != BufGenerateExtension.ErrorFormat.Default) { + add("--error-format"); add(errorFormatValue.cliValue) + } + } + additionalArgs.orNull.orEmpty() + } + + this.args.set(args) + } + + internal companion object { + const val NAME_PREFIX: String = "bufGenerate" + } +} + +internal fun Project.registerBufGenerateTask( + name: String, + workingDir: Provider, + outputDirectory: Provider, + protoFiles: Provider, + configure: BufGenerateTask.() -> Unit = {}, +): TaskProvider { + val capitalName = name.replaceFirstChar { it.uppercase() } + val bufGenerateTaskName = "${BufGenerateTask.NAME_PREFIX}$capitalName" + + return registerBufExecTask(bufGenerateTaskName, workingDir) { + group = PROTO_GROUP + description = "Generates code from .proto files using 'buf generate'" + + val generate = project.rpcExtension().grpc.buf.generate + + includeImports.set(generate.includeImports) + includeWkt.set(generate.includeWkt) + errorFormat.set(generate.errorFormat) + + this.outputDirectory.set(outputDirectory) + this.protoFiles.set(protoFiles) + + configure() + } +} diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/buf/tasks/GenerateBufGenYaml.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/buf/tasks/GenerateBufGenYaml.kt new file mode 100644 index 000000000..5f523b755 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/buf/tasks/GenerateBufGenYaml.kt @@ -0,0 +1,188 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.buf.tasks + +import kotlinx.rpc.buf.BUF_GEN_YAML +import kotlinx.rpc.proto.PROTO_FILES_DIR +import kotlinx.rpc.proto.PROTO_GROUP +import kotlinx.rpc.proto.ProtocPlugin +import kotlinx.rpc.proto.protoBuildDirSourceSets +import kotlinx.rpc.util.ensureRegularFileExists +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.TaskProvider +import org.gradle.kotlin.dsl.register +import java.io.File +import java.io.Serializable + +internal data class ResolvedGrpcPlugin( + val type: Type, + val locator: List, + val out: String, + val options: Map, + val strategy: String?, + val includeImports: Boolean?, + val includeWkt: Boolean?, + val types: List, + val excludeTypes: List, +) : Serializable { + @Suppress("EnumEntryName", "detekt.EnumNaming") + enum class Type { + local, remote, + ; + } + + companion object { + @Suppress("unused") + private const val serialVersionUID: Long = 1L + } +} + +/** + * Generates/updates Buf `buf.gen.yaml` file. + */ +public abstract class GenerateBufGenYaml : DefaultTask() { + @get:Input + internal abstract val plugins: ListProperty + + /** + * The `buf.gen.yaml` file to generate/update. + */ + @get:OutputFile + public abstract val bufGenFile: Property + + init { + group = PROTO_GROUP + } + + @TaskAction + @Suppress("detekt.CyclomaticComplexMethod", "detekt.NestedBlockDepth") + internal fun generate() { + val file = bufGenFile.get() + if (!file.exists()) { + file.parentFile.mkdirs() + file.createNewFile() + } + + file.bufferedWriter(Charsets.UTF_8).use { writer -> + writer.appendLine("version: v2") + writer.appendLine("clean: true") + writer.appendLine("plugins:") + plugins.get().forEach { plugin -> + val locatorLine = when (plugin.type) { + ResolvedGrpcPlugin.Type.local -> { + when (plugin.locator.size) { + 0 -> error("Local plugin without locators") + 1 -> plugin.locator.single() + else -> plugin.locator.joinToString(", ", prefix = "[", postfix = "]") + } + } + + ResolvedGrpcPlugin.Type.remote -> { + plugin.locator.single() + } + } + + writer.appendLine(" - ${plugin.type.name}: $locatorLine") + if (plugin.strategy != null) { + writer.appendLine(" strategy: ${plugin.strategy}") + } + if (plugin.includeImports != null) { + writer.appendLine(" include_imports: ${plugin.includeImports}") + } + if (plugin.includeWkt != null) { + writer.appendLine(" include_wkt: ${plugin.includeWkt}") + } + if (plugin.types.isNotEmpty()) { + writer.appendLine(" types:") + plugin.types.forEach { type -> + writer.appendLine(" - $type") + } + } + if (plugin.excludeTypes.isNotEmpty()) { + writer.appendLine(" exclude_types:") + plugin.excludeTypes.forEach { type -> + writer.appendLine(" - $type") + } + } + writer.appendLine(" out: ${plugin.out}") + if (plugin.options.isNotEmpty()) { + writer.appendLine(" opt:") + plugin.options.forEach { (key, value) -> + writer.appendLine(" - $key${if (value != null) "=$value" else ""}") + } + } + } + + writer.appendLine("inputs:") + writer.appendLine(" - directory: $PROTO_FILES_DIR") + + writer.flush() + } + } + + internal companion object { + const val NAME_PREFIX: String = "generateBufGenYaml" + } +} + +internal fun Project.registerGenerateBufGenYamlTask( + name: String, + dir: String, + protocPlugins: Iterable, + configure: GenerateBufGenYaml.() -> Unit = {}, +): TaskProvider { + val capitalizeName = name.replaceFirstChar { it.uppercase() } + return project.tasks.register("${GenerateBufGenYaml.NAME_PREFIX}$capitalizeName") { + val pluginsProvider = project.provider { + protocPlugins.map { plugin -> + if (!plugin.artifact.isPresent) { + throw GradleException( + "Artifact is not specified for protoc plugin ${plugin.name}. " + + "Use `local {}` or `remote {}` to specify it.") + } + + val artifact = plugin.artifact.get() + val locator = when (artifact) { + is ProtocPlugin.Artifact.Local -> artifact.executor.get() + is ProtocPlugin.Artifact.Remote -> listOf(artifact.locator.get()) + } + + ResolvedGrpcPlugin( + type = if (artifact is ProtocPlugin.Artifact.Local) { + ResolvedGrpcPlugin.Type.local + } else { + ResolvedGrpcPlugin.Type.remote + }, + locator = locator, + options = plugin.options.get(), + out = plugin.name, + strategy = plugin.strategy.orNull?.name?.lowercase(), + includeImports = plugin.includeImports.orNull, + includeWkt = plugin.includeWkt.orNull, + types = plugin.types.get(), + excludeTypes = plugin.excludeTypes.get(), + ) + } + } + + plugins.set(pluginsProvider) + + val bufGenYamlFile = project.protoBuildDirSourceSets + .resolve(dir) + .resolve(BUF_GEN_YAML) + .ensureRegularFileExists() + + bufGenFile.set(bufGenYamlFile) + + configure() + } +} diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/buf/tasks/GenerateBufYaml.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/buf/tasks/GenerateBufYaml.kt new file mode 100644 index 000000000..23ff47bed --- /dev/null +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/buf/tasks/GenerateBufYaml.kt @@ -0,0 +1,118 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.buf.tasks + +import kotlinx.rpc.buf.BUF_YAML +import kotlinx.rpc.proto.PROTO_FILES_IMPORT_DIR +import kotlinx.rpc.proto.PROTO_FILES_DIR +import kotlinx.rpc.proto.PROTO_GROUP +import kotlinx.rpc.proto.protoBuildDirSourceSets +import kotlinx.rpc.util.ensureDirectoryExists +import kotlinx.rpc.util.ensureRegularFileExists +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.provider.Property +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.TaskProvider +import org.gradle.kotlin.dsl.register +import java.io.File + +/** + * Generates/updates a Buf `buf.yaml` file. + */ +public abstract class GenerateBufYaml : DefaultTask() { + @get:InputDirectory + internal abstract val protoSourceDir: Property + + @get:Optional + @get:InputDirectory + internal abstract val importSourceDir: Property + + /** + * The `buf.yaml` file to generate/update. + */ + @get:OutputFile + public abstract val bufFile: Property + + init { + group = PROTO_GROUP + } + + @TaskAction + internal fun generate() { + val file = bufFile.get() + + if (!file.exists()) { + file.parentFile.mkdirs() + file.createNewFile() + } + + file.bufferedWriter(Charsets.UTF_8).use { writer -> + writer.appendLine("version: v2") + writer.appendLine("lint:") + writer.appendLine(" use:") + writer.appendLine(" - STANDARD") + writer.appendLine("breaking:") + writer.appendLine(" use:") + writer.appendLine(" - FILE") + + writer.appendLine("modules:") + + val protoDir = protoSourceDir.get() + if (protoDir.exists()) { + val modulePath = protoDir.relativeTo(file.parentFile) + writer.appendLine(" - path: $modulePath") + } + + val importDir = importSourceDir.orNull + if (importDir != null && importDir.exists()) { + val modulePath = importDir.relativeTo(file.parentFile) + writer.appendLine(" - path: $modulePath") + } + + writer.flush() + } + } + + internal companion object { + const val NAME_PREFIX: String = "generateBufYaml" + } +} + +internal fun Project.registerGenerateBufYamlTask( + name: String, + dir: String, + withImport: Boolean, + configure: GenerateBufYaml.() -> Unit = {}, +): TaskProvider { + val capitalizeName = name.replaceFirstChar { it.uppercase() } + return tasks.register("${GenerateBufYaml.NAME_PREFIX}$capitalizeName") { + val baseDir = project.protoBuildDirSourceSets.resolve(dir) + val protoDir = baseDir + .resolve(PROTO_FILES_DIR) + .ensureDirectoryExists() + + protoSourceDir.set(protoDir) + + if (withImport) { + val importDir = baseDir + .resolve(PROTO_FILES_IMPORT_DIR) + .ensureDirectoryExists() + + importSourceDir.set(importDir) + } + + val bufYamlFile = baseDir + .resolve(BUF_YAML) + .ensureRegularFileExists() + + bufFile.set(bufYamlFile) + + configure() + } +} diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/compilerPlugins.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/compilerPlugins.kt index fb74690ca..0e75571ac 100644 --- a/gradle-plugin/src/main/kotlin/kotlinx/rpc/compilerPlugins.kt +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/compilerPlugins.kt @@ -6,7 +6,6 @@ package kotlinx.rpc -import org.gradle.kotlin.dsl.findByType import org.jetbrains.kotlin.gradle.plugin.KotlinCompilerPluginSupportPlugin import org.jetbrains.kotlin.gradle.plugin.SubpluginOption @@ -26,8 +25,7 @@ internal class CompilerPluginCli : KotlinCompilerPluginSupportPlugin by compiler pluginSuffix = "-cli" applyToCompilation = { - val extension = it.target.project.extensions.findByType() - ?: RpcExtension(it.target.project.objects) + val extension = it.target.project.rpcExtension() it.target.project.provider { listOf( diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/grpc/DefaultGrpcExtension.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/grpc/DefaultGrpcExtension.kt new file mode 100644 index 000000000..5f180791a --- /dev/null +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/grpc/DefaultGrpcExtension.kt @@ -0,0 +1,329 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc + +import kotlinx.rpc.GRPC_KOTLIN_VERSION +import kotlinx.rpc.GRPC_VERSION +import kotlinx.rpc.PROTOBUF_VERSION +import kotlinx.rpc.buf.BufExtension +import kotlinx.rpc.buf.configureBufExecutable +import kotlinx.rpc.buf.tasks.registerBufExecTask +import kotlinx.rpc.buf.tasks.registerBufGenerateTask +import kotlinx.rpc.buf.tasks.registerGenerateBufGenYamlTask +import kotlinx.rpc.buf.tasks.registerGenerateBufYamlTask +import kotlinx.rpc.proto.* +import kotlinx.rpc.proto.ProtocPlugin.Companion.GRPC_JAVA +import kotlinx.rpc.proto.ProtocPlugin.Companion.GRPC_KOTLIN +import kotlinx.rpc.proto.ProtocPlugin.Companion.KXRPC +import kotlinx.rpc.proto.ProtocPlugin.Companion.PROTOBUF_JAVA +import org.gradle.api.Action +import org.gradle.api.GradleException +import org.gradle.api.NamedDomainObjectContainer +import org.gradle.api.Project +import org.gradle.api.model.ObjectFactory +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.api.tasks.compile.JavaCompile +import org.gradle.kotlin.dsl.findByType +import org.gradle.kotlin.dsl.newInstance +import org.gradle.kotlin.dsl.the +import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode +import org.jetbrains.kotlin.gradle.dsl.KotlinBaseExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import javax.inject.Inject + +internal open class DefaultGrpcExtension @Inject constructor( + objects: ObjectFactory, + private val project: Project, +) : GrpcExtension { + override val protocPlugins: NamedDomainObjectContainer = + objects.domainObjectContainer(ProtocPlugin::class.java) { name -> + ProtocPlugin(name, project) + } + + override fun protocPlugins(action: Action>) { + action.execute(protocPlugins) + } + + override val buf: BufExtension = project.objects.newInstance() + override fun buf(action: Action) { + action.execute(buf) + } + + init { + project.configureBufExecutable() + project.configureKxRpcPluginJarConfiguration() + + createDefaultProtocPlugins() + + project.configureProtoExtensions { _, protoSourceSet -> + protoSourceSet.protocPlugin(protocPlugins.protobufJava) + protoSourceSet.protocPlugin(protocPlugins.grpcJava) + protoSourceSet.protocPlugin(protocPlugins.grpcKotlin) + protoSourceSet.protocPlugin(protocPlugins.kxrpc) + } + + project.afterEvaluate { + project.protoSourceSets.forEach { sourceSet -> + if (sourceSet !is DefaultProtoSourceSet) { + return@forEach + } + + configureTasks(sourceSet) + } + } + } + + @Suppress("detekt.LongMethod", "detekt.CyclomaticComplexMethod") + private fun Project.configureTasks(protoSourceSet: DefaultProtoSourceSet) { + val baseName = protoSourceSet.name + val baseGenDir = project.protoBuildDirSourceSets.resolve(baseName) + + val pairSourceSet = protoSourceSet.correspondingMainSourceSetOrNull() + + val mainProtocPlugins = pairSourceSet?.protocPlugins?.get().orEmpty() + val protocPluginNames = (protoSourceSet.protocPlugins.get() + mainProtocPlugins).distinct() + + val includedProtocPlugins = protocPluginNames.map { + protocPlugins.findByName(it) + ?: throw GradleException("Protoc plugin $it not found") + } + + val protoFiles = protoSourceSet.proto + val hasFiles = !protoFiles.isEmpty + + val generateBufYamlTask = registerGenerateBufYamlTask( + name = baseName, + dir = baseName, + withImport = pairSourceSet != null, + ) + + val generateBufGenYamlTask = registerGenerateBufGenYamlTask( + name = baseName, + dir = baseName, + protocPlugins = includedProtocPlugins, + ) { + dependsOn(generateBufYamlTask) + } + + val processProtoTask = registerProcessProtoFilesTask( + name = baseName, + baseGenDir = provider { baseGenDir }, + protoFiles = protoFiles, + toDir = PROTO_FILES_DIR, + ) { + dependsOn(generateBufYamlTask) + dependsOn(generateBufGenYamlTask) + } + + val processImportProtoTask = if (pairSourceSet != null) { + val importProtoFiles = pairSourceSet.proto + + registerProcessProtoFilesTask( + name = "${baseName}Import", + baseGenDir = provider { baseGenDir }, + protoFiles = importProtoFiles, + toDir = PROTO_FILES_IMPORT_DIR, + ) { + dependsOn(generateBufYamlTask) + dependsOn(generateBufGenYamlTask) + dependsOn(processProtoTask) + } + } else { + null + } + + val out = protoBuildDirGenerated.resolve(baseName) + + val bufGenerateTask = registerBufGenerateTask( + name = baseName, + workingDir = provider { baseGenDir }, + outputDirectory = provider { out }, + protoFiles = provider { + protoFiles.asFileTree.let { + if (pairSourceSet != null) { + it + pairSourceSet.proto + } else { + it + } + } + }, + ) { + dependsOn(generateBufGenYamlTask) + dependsOn(generateBufYamlTask) + dependsOn(processProtoTask) + if (processImportProtoTask != null) { + dependsOn(processImportProtoTask) + } + + if (pairSourceSet != null) { + dependsOn(pairSourceSet.generateTask) + } + + onlyIf { hasFiles } + } + + protoSourceSet.generateTask.set(bufGenerateTask) + + tasks.withType().configureEach { + // compileKotlin - main + // compileTestKotlin - test + // compileKotlinJvm - jvmMain + // compileTestKotlinJvm - jvmTest + // compileKotlinIosArm64 - iosArm64Main + // compileTestKotlinIosArm64 - iosArm64Test + val taskNameAsSourceSet = name + .removePrefix("compile").let { + val suffix = it.substringBefore("Kotlin").takeIf { prefix -> + prefix.isNotEmpty() + } ?: "Main" + + (it.substringAfter("Kotlin") + .replaceFirstChar { ch -> ch.lowercase() } + suffix) + .takeIf { result -> result != suffix } + ?: suffix.lowercase() + } + + if (taskNameAsSourceSet == baseName) { + dependsOn(bufGenerateTask) + } + } + + tasks.withType().configureEach { + // compileJvmTestJava - test (java, kmp) + // compileJvmMainJava - main (java, kmp) + // compileJava - main (java) + // compileTestJava - test (java) + val taskNameAsSourceSet = when (name) { + "compileJvmTestJava" -> "test" + "compileJvmMainJava" -> "main" + "compileJava" -> "main" + "compileTestJava" -> "test" + + else -> throw GradleException("Unknown java compile task name: $name") + } + + if (taskNameAsSourceSet == baseName) { + dependsOn(bufGenerateTask) + } + } + + includedProtocPlugins.forEach { plugin -> + // locates correctly jvmMain, main jvmTest, test + val javaSourceSet = extensions.findByType() + ?.sourceSets?.findByName(baseName)?.java + + if (plugin.isJava.get() && javaSourceSet != null) { + javaSourceSet.srcDirs(out.resolve(plugin.name)) + } else { + protoSourceSet.languageSourceSets.get().find { it is KotlinSourceSet }?.let { + (it as KotlinSourceSet) + .kotlin.srcDirs(out.resolve(plugin.name)) + } ?: error( + "Unable to find fitting source directory set " + + "for plugin '${plugin.name}' in '$protoSourceSet' proto source set" + ) + } + } + + val baseCapital = baseName.replaceFirstChar { it.uppercase() } + buf.tasks.customTasks.get().forEach { definition -> + val capital = definition.name.replaceFirstChar { it.uppercase() } + val taskName = "buf$capital$baseCapital" + + val customTask = registerBufExecTask( + clazz = definition.kClass, + workingDir = provider { baseGenDir }, + name = taskName, + ) { + dependsOn(generateBufYamlTask) + dependsOn(generateBufGenYamlTask) + dependsOn(processProtoTask) + if (processImportProtoTask != null) { + dependsOn(processImportProtoTask) + } + + onlyIf { hasFiles } + } + + when { + baseName.lowercase().endsWith("main") -> { + definition.property.mainTask.set(customTask) + } + + baseName.lowercase().endsWith("test") -> { + definition.property.testTask.set(customTask) + } + + else -> { + throw GradleException("Unknown source set name: $baseName") + } + } + } + } + + private fun createDefaultProtocPlugins() { + protocPlugins.create(KXRPC) { + local { + javaJar(project.kxrpcProtocPluginJarPath) + } + + options.put("debugOutput", "protobuf-kxrpc-plugin.log") + options.put("messageMode", "interface") + options.put("explicitApiModeEnabled", project.provider { + project.the().explicitApi != ExplicitApiMode.Disabled + }) + } + + protocPlugins.create(GRPC_JAVA) { + isJava.set(true) + + remote { + locator.set("buf.build/grpc/java:v$GRPC_VERSION") + } + } + + protocPlugins.create(GRPC_KOTLIN) { + remote { + locator.set("buf.build/grpc/kotlin:v$GRPC_KOTLIN_VERSION") + } + } + + protocPlugins.create(PROTOBUF_JAVA) { + isJava.set(true) + + remote { + // for some reason they omit the first digit in this version: + // https://buf.build/protocolbuffers/java?version=v31.1 + locator.set("buf.build/protocolbuffers/java:v${PROTOBUF_VERSION.substringAfter(".")}") + } + } + } + + private fun DefaultProtoSourceSet.correspondingMainSourceSetOrNull(): DefaultProtoSourceSet? { + return when { + name.lowercase().endsWith("main") -> { + null + } + + name.lowercase().endsWith("test") -> { + project.protoSourceSets.getByName(correspondingMainName()) as DefaultProtoSourceSet + } + + else -> { + throw GradleException("Unknown source set name: $name") + } + } + } + + private fun ProtoSourceSet.correspondingMainName(): String { + return when { + name == "test" -> "main" + name.endsWith("Test") -> name.removeSuffix("Test") + "Main" + else -> throw GradleException("Unknown test source set name: $name") + } + } +} diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/grpc/GrpcExtension.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/grpc/GrpcExtension.kt new file mode 100644 index 000000000..265841fd5 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/grpc/GrpcExtension.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc + +import kotlinx.rpc.buf.BufExtension +import kotlinx.rpc.proto.ProtocPlugin +import org.gradle.api.Action +import org.gradle.api.NamedDomainObjectContainer + +/** + * Configuration for the gRPC capabilities. + * + * To enable the gRPC capabilities, add the following minimal code to your `build.gradle.kts`: + * ```kotlin + * rpc { + * grpc() + * } + * ``` + */ +public interface GrpcExtension { + /** + * List of protoc plugins to be applied to the project. + */ + public val protocPlugins: NamedDomainObjectContainer + + /** + * Configures the protoc plugins. + */ + public fun protocPlugins(action: Action>) + + /** + * Configuration for the Buf build tool. + */ + public val buf: BufExtension + + /** + * Configures the Buf build tool. + */ + public fun buf(action: Action) +} diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/grpc/protections.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/grpc/protections.kt new file mode 100644 index 000000000..cea7ccae7 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/grpc/protections.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc + +import kotlinx.rpc.rpcExtension +import org.gradle.api.GradleException +import org.gradle.api.Project + +private const val BUF_PLUGIN_ID = "build.buf" +private const val PROTOBUF_PLUGIN_ID = "com.google.protobuf" + +@Suppress("detekt.ThrowsCount") +internal fun Project.configurePluginProtections() { + var isBufPluginApplied = false + var isProtobufPluginApplied = false + + pluginManager.withPlugin(BUF_PLUGIN_ID) { isBufPluginApplied = true } + pluginManager.withPlugin(PROTOBUF_PLUGIN_ID) { isProtobufPluginApplied = true } + + afterEvaluate { + if (!rpcExtension().grpcApplied.get()) { + return@afterEvaluate + } + + if (isBufPluginApplied && !isProtobufPluginApplied) { + throw GradleException( + "Buf plugin ($BUF_PLUGIN_ID) can't be applied to the project, " + + "it is not compatible with the Rpc Gradle Plugin " + ) + } + + if (isProtobufPluginApplied && !isBufPluginApplied) { + throw GradleException( + "Protobuf plugin ($PROTOBUF_PLUGIN_ID) can't be applied to the project, " + + "it is not compatible with the Rpc Gradle Plugin " + ) + } + + if (isBufPluginApplied) { + throw GradleException( + "Both Buf ($BUF_PLUGIN_ID) and Protobuf ($PROTOBUF_PLUGIN_ID) " + + "plugins can't be applied to the project, they are not compatible with the Rpc Gradle Plugin " + ) + } + } +} diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/DefaultProtoSourceSet.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/DefaultProtoSourceSet.kt new file mode 100644 index 000000000..f8fd271f2 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/DefaultProtoSourceSet.kt @@ -0,0 +1,121 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.proto + +import kotlinx.rpc.buf.tasks.BufGenerateTask +import kotlinx.rpc.util.findOrCreate +import kotlinx.rpc.util.withKotlinJvmExtension +import kotlinx.rpc.util.withKotlinKmpExtension +import org.gradle.api.Action +import org.gradle.api.GradleException +import org.gradle.api.NamedDomainObjectFactory +import org.gradle.api.NamedDomainObjectProvider +import org.gradle.api.Project +import org.gradle.api.file.SourceDirectorySet +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.SourceSet +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.kotlin.dsl.add +import org.gradle.kotlin.dsl.listProperty +import org.gradle.kotlin.dsl.property +import javax.inject.Inject + +@Suppress("UNCHECKED_CAST") +internal val Project.protoSourceSets: ProtoSourceSets + get() = extensions.findByName(PROTO_SOURCE_SETS) as? ProtoSourceSets + ?: throw GradleException("Unable to find proto source sets in project $name") + +internal class ProtoSourceSetFactory(private val project: Project) : NamedDomainObjectFactory { + override fun create(name: String): ProtoSourceSet { + return project.objects.newInstance(DefaultProtoSourceSet::class.java, project, name) + } +} + +internal open class DefaultProtoSourceSet @Inject constructor( + internal val project: Project, + override val name: String, +) : ProtoSourceSet { + val languageSourceSets: ListProperty = project.objects.listProperty() + val protocPlugins: ListProperty = project.objects.listProperty().convention(emptyList()) + val generateTask: Property = project.objects.property() + + override fun protocPlugin(plugin: NamedDomainObjectProvider) { + protocPlugins.add(plugin.name) + } + + override fun protocPlugin(plugin: ProtocPlugin) { + protocPlugins.add(plugin.name) + } + + override val proto: SourceDirectorySet = project.objects.sourceDirectorySet( + PROTO_SOURCE_DIRECTORY_NAME, + "Proto sources", + ).apply { + srcDirs("src/$name/proto") + } + + override fun proto(action: Action) { + action.execute(proto) + } +} + +internal fun Project.configureProtoExtensions( + configure: Project.( + languageSourceSet: Any, + protoSourceSet: DefaultProtoSourceSet, + ) -> Unit +) { + fun findOrCreateAndConfigure(languageSourceSetName: String, languageSourceSet: Any) { + val container = project.findOrCreate(PROTO_SOURCE_SETS) { + val container = objects.domainObjectContainer( + ProtoSourceSet::class.java, + ProtoSourceSetFactory(project) + ) + + project.extensions.add(PROTO_SOURCE_SETS, container) + + container + } + + val protoSourceSet = container.maybeCreate(languageSourceSetName) as DefaultProtoSourceSet + + configure(languageSourceSet, protoSourceSet) + } + + project.withKotlinJvmExtension { + sourceSets.configureEach { + if (name == SourceSet.MAIN_SOURCE_SET_NAME || name == SourceSet.TEST_SOURCE_SET_NAME) { + findOrCreateAndConfigure(name, this) + } + } + + project.extensions.configure("sourceSets") { + configureEach { + if (name == SourceSet.MAIN_SOURCE_SET_NAME || name == SourceSet.TEST_SOURCE_SET_NAME) { + findOrCreateAndConfigure(name, this) + } + } + } + } + + project.withKotlinKmpExtension { + sourceSets.configureEach { + if (name == "jvmMain" || name == "jvmTest") { + findOrCreateAndConfigure(name, this) + } + } + } +} + +internal fun Project.createProtoExtensions() { + configureProtoExtensions { languageSourceSet, sourceSet -> + sourceSet.initExtension(languageSourceSet) + } +} + +private fun DefaultProtoSourceSet.initExtension(languageSourceSet: Any) { + this.languageSourceSets.add(languageSourceSet) +} diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/ProcessProtoFiles.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/ProcessProtoFiles.kt new file mode 100644 index 000000000..bf7d245e0 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/ProcessProtoFiles.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.proto + +import org.gradle.api.Project +import org.gradle.api.file.SourceDirectorySet +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Copy +import org.gradle.api.tasks.TaskProvider +import org.gradle.kotlin.dsl.register +import java.io.File + +/** + * Copy proto files to a temporary directory for Buf to process. + */ +public abstract class ProcessProtoFiles : Copy() { + init { + group = PROTO_GROUP + } +} + +internal fun Project.registerProcessProtoFilesTask( + name: String, + baseGenDir: Provider, + protoFiles: SourceDirectorySet, + toDir: String, + configure: ProcessProtoFiles.() -> Unit = {}, +): TaskProvider { + val capitalName = name.replaceFirstChar { it.uppercase() } + + return tasks.register("process${capitalName}ProtoFiles") { + val protoGenDir = baseGenDir.map { it.resolve(toDir) } + + val allFiles = protoFiles.files + + from(protoFiles.srcDirs) { + include { + it.file in allFiles + } + } + + into(protoGenDir) + + doFirst { + protoGenDir.get().deleteRecursively() + } + + configure() + } +} diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/ProtoSourceSet.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/ProtoSourceSet.kt new file mode 100644 index 000000000..9b14d33a7 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/ProtoSourceSet.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.proto + +import org.gradle.api.Action +import org.gradle.api.NamedDomainObjectContainer +import org.gradle.api.NamedDomainObjectProvider +import org.gradle.api.file.SourceDirectorySet + +public typealias ProtoSourceSets = NamedDomainObjectContainer + +/** + * Represents a source set for proto files. + */ +public interface ProtoSourceSet { + /** + * Name of the source set. + */ + public val name: String + + /** + * Adds a new protoc plugin. + * + * Example: + * ```kotlin + * protocPlugin(rpc.grpc.protocPlugins.myPlugin) + * ``` + */ + public fun protocPlugin(plugin: NamedDomainObjectProvider) + + /** + * Adds a new protoc plugin. + * + * Example: + * ```kotlin + * protocPlugin(rpc.grpc.protocPlugins.myPlugin.get()) + * ``` + */ + public fun protocPlugin(plugin: ProtocPlugin) + + /** + * Default [SourceDirectorySet] for proto files. + */ + public val proto: SourceDirectorySet + + /** + * Configures [proto] source directory set. + */ + public fun proto(action: Action) { + action.execute(proto) + } +} diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/ProtocPlugin.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/ProtocPlugin.kt new file mode 100644 index 000000000..30fcf3077 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/ProtocPlugin.kt @@ -0,0 +1,302 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.proto + +import kotlinx.rpc.proto.ProtocPlugin.Companion.GRPC_JAVA +import kotlinx.rpc.proto.ProtocPlugin.Companion.GRPC_KOTLIN +import kotlinx.rpc.proto.ProtocPlugin.Companion.KXRPC +import kotlinx.rpc.proto.ProtocPlugin.Companion.PROTOBUF_JAVA +import kotlinx.rpc.util.OS +import org.gradle.api.Action +import org.gradle.api.NamedDomainObjectContainer +import org.gradle.api.NamedDomainObjectProvider +import org.gradle.api.Project +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.kotlin.dsl.listProperty +import org.gradle.kotlin.dsl.mapProperty +import org.gradle.kotlin.dsl.property + +/** + * Access to the `kotlinx-rpc` protoc plugin. + */ +public val NamedDomainObjectContainer.kxrpc: NamedDomainObjectProvider + get() = named(KXRPC) + +/** + * Configures the `kotlinx-rpc` protoc plugin. + */ +public fun NamedDomainObjectContainer.kxrpc(action: Action) { + kxrpc.configure(action) +} + +/** + * Access to the `protobuf-java` protoc plugin. + */ +public val NamedDomainObjectContainer.protobufJava: NamedDomainObjectProvider + get() = named(PROTOBUF_JAVA) + +/** + * Configures the `protobuf-java` protoc plugin. + */ +public fun NamedDomainObjectContainer.protobufJava(action: Action) { + protobufJava.configure(action) +} + +/** + * Access to the `grpc-java` protoc plugin. + */ +public val NamedDomainObjectContainer.grpcJava: NamedDomainObjectProvider + get() = named(GRPC_JAVA) + +/** + * Configures the grpc-java protoc plugin. + */ +public fun NamedDomainObjectContainer.grpcJava(action: Action) { + grpcJava.configure(action) +} + +/** + * Access to the `grpc-kotlin` protoc plugin. + */ +public val NamedDomainObjectContainer.grpcKotlin: NamedDomainObjectProvider + get() = named(GRPC_KOTLIN) + +/** + * Configures the `grpc-kotlin` protoc plugin. + */ +public fun NamedDomainObjectContainer.grpcKotlin(action: Action) { + grpcKotlin.configure(action) +} + +/** + * Access to a specific protoc plugin. + */ +public open class ProtocPlugin( + public val name: String, + private val project: Project, +) { + /** + * Whether the plugin generates Java code. + * + * Plugins that have this property set to `true` will have their output directory + * added to the source set's `java` source directory set if present. + */ + public val isJava: Property = project.objects.property().convention(false) + + /** + * Protoc plugins options. + * + * @see Buf documentation - opt + */ + public val options: MapProperty = project.objects + .mapProperty() + .convention(emptyMap()) + + /** + * Local protoc plugin artifact. + * + * @see + * Buf documentation - Type of plugin + * + */ + public fun local(action: Action) { + artifact.set(Artifact.Local(project).apply(action::execute)) + } + + /** + * Remote protoc plugin artifact. + * + * @see + * Buf documentation - Type of plugin + * + */ + public fun remote(action: Action) { + artifact.set(Artifact.Remote(project).apply(action::execute)) + } + + /** + * Protoc plugin artifact. + * + * Can be either [Artifact.Local] or [Artifact.Remote]. + * + * @see + * Buf documentation - Type of plugin + * + */ + public val artifact: Property = project.objects.property() + + /** + * Strategy for this protoc plugin. + * + * Optional. + * Default is Buf's default. + * + * @see Buf documentation - strategy + */ + public val strategy: Property = project.objects.property().convention(null) + + /** + * Whether to include imports except for Well-Known Types. + * + * Optional. + * Default is Buf's default. + * + * @see + * Buf documentation - include_imports + * + */ + public val includeImports: Property = project.objects.property().convention(null) + + /** + * Whether to include Well-Known Types. + * + * Optional. + * Default is Buf's default. + * + * @see + * Buf documentation - include_wkt + * + */ + public val includeWkt: Property = project.objects.property().convention(null) + + /** + * Include only the specified types when generating with this plugin. + * + * Optional. + * + * @see + * Buf documentation - types + * + */ + public val types: ListProperty = project.objects.listProperty() + + /** + * Exclude the specified types when generating with this plugin. + * + * Optional. + * + * @see + * Buf documentation - exclude-types + * + */ + public val excludeTypes: ListProperty = project.objects.listProperty() + + public companion object { + /** + * The name of the kotlinx-rpc protoc plugin. + * + * @see [kxrpc] + */ + public const val KXRPC: String = "kotlinx-rpc" + + /** + * The name of the protobuf-java protoc plugin. + * + * @see [protobufJava] + */ + public const val PROTOBUF_JAVA: String = "java" + + /** + * The name of the grpc-java protoc plugin. + * + * @see [grpcJava] + */ + public const val GRPC_JAVA: String = "grpc-java" + + /** + * The name of the grpc-kotlin protoc plugin. + * + * @see [grpcKotlin] + */ + public const val GRPC_KOTLIN: String = "grpc-kotlin" + } + + /** + * Strategy for a protoc plugin. + * + * @see [strategy] + */ + public enum class Strategy { + Directory, All; + } + + /** + * Artifact for a protoc plugin. + */ + public sealed class Artifact { + /** + * Local protoc plugin artifact. + * + * Local artifact is defined by a list of command-line arguments that execute the plugin - [executor] + * + * @see + * Buf documentation - Type of plugin + * + */ + public class Local(private val project: Project) : Artifact() { + /** + * Command-line arguments that execute the plugin. + */ + public val executor: ListProperty = project.objects.listProperty() + + /** + * Command-line arguments that execute the plugin. + */ + public fun executor(elements: Provider>) { + executor.set(elements) + } + + /** + * Command-line arguments that execute the plugin. + */ + public fun executor(vararg elements: String) { + executor.set(elements.toList()) + } + + /** + * Protoc plugin jar file path. + * + * If [executablePath] is not specified, the jar will be executed with Java used for the Gradle build. + */ + public fun javaJar(jarPath: Provider, executablePath: Provider? = null) { + if (executablePath == null) { + executor(jarPath.map { listOf(OS.javaExePath, "-jar", it) }) + return + } + + val list = jarPath.zip(executablePath) { jar, exe -> + listOf(exe, "-jar", jar) + } + + executor(list) + } + + /** + * Protoc plugin jar file path. + * + * Uses default Java executable, the same as for the Gradle build. + */ + public fun javaJar(jarPath: String) { + javaJar(project.provider { jarPath }) + } + } + + /** + * Remote protoc plugin artifact. + * + * Locator is a BSR Url. + * + * @see + * Buf documentation - Type of plugin + * + */ + public class Remote(project: Project) : Artifact() { + public val locator: Property = project.objects.property() + } + } +} diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/consts.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/consts.kt new file mode 100644 index 000000000..dd800ad6f --- /dev/null +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/consts.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.proto + +import org.gradle.api.artifacts.Configuration + +/** + * `proto` group for related gradle tasks. + */ +public const val PROTO_GROUP: String = "proto" + +/** + * Container for proto source sets. + */ +public const val PROTO_SOURCE_SETS: String = "protoSourceSets" + +/** + * Name of the default source directory set for proto files in [PROTO_SOURCE_SETS]. + */ +public const val PROTO_SOURCE_DIRECTORY_NAME: String = "proto" + +/** + * Directory for proto build artifacts. + */ +public const val PROTO_BUILD_DIR: String = "protoBuild" + +/** + * Directory for proto build generated files. + */ +public const val PROTO_BUILD_GENERATED: String = "generated" + +/** + * Directory for proto build temporary files. + * Files there are constructed to form a valid Buf workspace. + */ +public const val PROTO_BUILD_SOURCE_SETS: String = "sourceSets" + +/** + * Source directory for proto files in [PROTO_BUILD_SOURCE_SETS]. + * + * For these files the `buf generate` task will be called. + */ +public const val PROTO_FILES_DIR: String = "proto" + +/** + * Source directory for proto imported files in [PROTO_BUILD_SOURCE_SETS]. + * + * These files are included as imports in the `buf generate` task, but don't get processed by it. + */ +public const val PROTO_FILES_IMPORT_DIR: String = "import" + +/** + * [Configuration] name for the `kotlinx-rpc` protoc plugin artifact. + * + * MUST be a single file. + */ +public const val KXRPC_PLUGIN_JAR_CONFIGURATION: String = "kxrpcPluginJar" diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/derictory.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/derictory.kt new file mode 100644 index 000000000..00cbe83f7 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/derictory.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.proto + +import org.gradle.api.Project +import java.io.File + +internal val Project.protoBuildDir + get() = + layout.buildDirectory + .dir(PROTO_BUILD_DIR) + .get() + .asFile + +internal val Project.protoBuildDirSourceSets: File + get() { + return protoBuildDir.resolve(PROTO_BUILD_SOURCE_SETS) + } + +internal val Project.protoBuildDirGenerated: File + get() { + return protoBuildDir.resolve(PROTO_BUILD_GENERATED) + } diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/kxrpcPluginJar.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/kxrpcPluginJar.kt new file mode 100644 index 000000000..e5477a15f --- /dev/null +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/kxrpcPluginJar.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.proto + +import kotlinx.rpc.LIBRARY_VERSION +import org.gradle.api.Project +import org.gradle.api.provider.Provider + +/** + * Absolute path to the `kotlinx-rpc-protobuf-plugin` jar. + * + * Can be used to customise the java executable path: + * ```kotlin + * rpc.grpc.protocPlugins.kxrpc { + * local { + * javaJar(kxrpcProtocPluginJarPath, provider { "my-path-to-java" }) + * } + * } + * ``` + */ +public val Project.kxrpcProtocPluginJarPath: Provider + get() = project.configurations.named(KXRPC_PLUGIN_JAR_CONFIGURATION).map { it.singleFile.absolutePath } + +internal fun Project.configureKxRpcPluginJarConfiguration() { + configurations.create(KXRPC_PLUGIN_JAR_CONFIGURATION) + + dependencies.add( + KXRPC_PLUGIN_JAR_CONFIGURATION, + mapOf( + "group" to "org.jetbrains.kotlinx", + "name" to "kotlinx-rpc-protobuf-plugin", + "version" to LIBRARY_VERSION, + "classifier" to "all", + "ext" to "jar", + ), + ) +} diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/util/OS.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/util/OS.kt new file mode 100644 index 000000000..e1aa39ded --- /dev/null +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/util/OS.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.util + +import org.gradle.api.GradleException +import java.io.File + +internal object OS { + internal val javaExePath: String by lazy { + val java = File(System.getProperty("java.home"), if (isWindows) "bin/java.exe" else "bin/java") + + if (!java.exists()) { + throw GradleException("Could not find java executable at " + java.path) + } + + java.path + } + + internal val isWindows: Boolean by lazy { + System.getProperty("os.name").lowercase().contains("win") + } +} diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/util/ProcessRunner.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/util/ProcessRunner.kt new file mode 100644 index 000000000..97c4cd663 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/util/ProcessRunner.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.util + +import java.io.ByteArrayOutputStream +import java.io.Closeable +import java.io.File +import java.io.InputStream +import java.nio.charset.StandardCharsets +import java.util.concurrent.Executors + +// See https://github.com/diffplug/spotless/blob/0fd20bb80c6c426d20e0a3157c3c2b89317032da/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java +internal class ProcessRunner : Closeable { + private val threadStdOut = Executors.newSingleThreadExecutor() + private val threadStdErr = Executors.newSingleThreadExecutor() + private val bufStdOut = ByteArrayOutputStream() + private val bufStdErr = ByteArrayOutputStream() + + fun shell( + name: String, + workingDir: File, + args: List, + ): Result { + val processBuilder = ProcessBuilder(args.map(Any::toString)) + processBuilder.directory(workingDir) + val process = processBuilder.start() + val out = threadStdOut.submit { drain(process.inputStream, bufStdOut) } + val err = threadStdErr.submit { drain(process.errorStream, bufStdErr) } + val exitCode = process.waitFor() + return Result(name, args, exitCode, out.get(), err.get()) + } + + private fun drain( + input: InputStream, + output: ByteArrayOutputStream, + ): ByteArray { + output.reset() + input.copyTo(output) + return output.toByteArray() + } + + override fun close() { + threadStdOut.shutdown() + threadStdErr.shutdown() + } + + class Result( + val name: String, + val args: List, + val exitCode: Int, + val stdOut: ByteArray, + val stdErr: ByteArray, + ) { + fun formattedOutput() = buildString { + appendLine("Process $name finished:") + appendLine(" - Arguments: $args") + appendLine(" - Exit code: $exitCode") + val perStream = { name: String, content: ByteArray -> + val string = content.toString(StandardCharsets.UTF_8) + if (string.isEmpty()) { + appendLine(" - $name: No output") + } else { + appendLine(" - $name:") + val lines = string.replace("\r", "").lines() + lines.forEach { appendLine(" $it") } + } + } + perStream("Stdout", stdOut) + perStream("Stderr", stdErr) + return toString() + } + } +} diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/util/files.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/util/files.kt new file mode 100644 index 000000000..bc1a6d684 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/util/files.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.util + +import java.io.File + +internal fun File.ensureDirectoryExists(): File { + if (!exists()) { + mkdirs() + } + + return this +} + +internal fun File.ensureRegularFileExists(): File { + if (!exists()) { + parentFile.ensureDirectoryExists() + createNewFile() + } + + return this +} diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/util/gradle.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/util/gradle.kt new file mode 100644 index 000000000..9f3628824 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/util/gradle.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.util + +import org.gradle.api.plugins.ExtensionAware + +internal inline fun Container.findOrCreate( + name: String, + noinline create: Container.() -> T, +): T = + findOrCreate(name, T::class.java, create) + +@Suppress("UNCHECKED_CAST") +private fun Container.findOrCreate( + name: String, + typeClass: Class, + create: Container.() -> T, +): T = + extensions.findByName(name)?.let { + it as? T + ?: error("Extension $name is already present, but of the wrong type: ${it::class} instead of $typeClass") + } ?: create() diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/util/kgp.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/util/kgp.kt new file mode 100644 index 000000000..dce7f0305 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/util/kgp.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.util + +import org.gradle.api.Action +import org.gradle.api.Project +import org.gradle.kotlin.dsl.the +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension + +private const val KOTLIN_MULTIPLATFORM_PLUGIN_ID = "org.jetbrains.kotlin.multiplatform" +private const val KOTLIN_JVM_PLUGIN_ID = "org.jetbrains.kotlin.jvm" + +internal fun Project.withKotlinJvmExtension(action: Action) { + plugins.withId(KOTLIN_JVM_PLUGIN_ID) { + the().apply { action.execute(this) } + } +} + +internal fun Project.withKotlinKmpExtension(action: Action) { + plugins.withId(KOTLIN_MULTIPLATFORM_PLUGIN_ID) { + the().apply { action.execute(this) } + } +} diff --git a/grpc/grpc-ktor-server-test/api/grpc-ktor-server-test.api b/grpc/grpc-ktor-server-test/api/grpc-ktor-server-test.api deleted file mode 100644 index e69de29bb..000000000 diff --git a/grpc/grpc-ktor-server-test/build.gradle.kts b/grpc/grpc-ktor-server-test/build.gradle.kts deleted file mode 100644 index c2d5de339..000000000 --- a/grpc/grpc-ktor-server-test/build.gradle.kts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. - */ - -plugins { - alias(libs.plugins.conventions.jvm) - alias(libs.plugins.kotlinx.rpc) - alias(libs.plugins.protobuf) -} - -dependencies { - // for the jar dependency - testImplementation(kotlin("test")) - testImplementation(projects.grpc.grpcCore) - testImplementation(projects.grpc.grpcKtorServer) - - testImplementation(libs.grpc.kotlin.stub) - testImplementation(libs.grpc.netty) - - testImplementation(libs.ktor.server.core) - testImplementation(libs.ktor.server.test.host) - testRuntimeOnly(libs.logback.classic) -} - -rpc { - grpc { - enabled = true - - val globalRootDir: String by extra - - plugin { - locator { - path = "$globalRootDir/protobuf-plugin/build/libs/protobuf-plugin-$version-all.jar" - } - } - - tasksMatching { it.isTest }.all { - dependsOn(project(":protobuf-plugin").tasks.jar) - } - } -} diff --git a/grpc/grpc-ktor-server-test/gradle.properties b/grpc/grpc-ktor-server-test/gradle.properties deleted file mode 100644 index b68c20f8d..000000000 --- a/grpc/grpc-ktor-server-test/gradle.properties +++ /dev/null @@ -1,5 +0,0 @@ -# -# Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. -# - -kotlinx.rpc.exclude.wasmWasi=true diff --git a/grpc/grpc-ktor-server/build.gradle.kts b/grpc/grpc-ktor-server/build.gradle.kts index e71d8343e..e1e82de8c 100644 --- a/grpc/grpc-ktor-server/build.gradle.kts +++ b/grpc/grpc-ktor-server/build.gradle.kts @@ -2,6 +2,11 @@ * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ +import kotlinx.rpc.buf.tasks.BufGenerateTask +import kotlinx.rpc.proto.kxrpc +import org.gradle.kotlin.dsl.kotlin +import org.gradle.kotlin.dsl.withType + plugins { alias(libs.plugins.conventions.kmp) alias(libs.plugins.kotlinx.rpc) @@ -15,5 +20,40 @@ kotlin { implementation(libs.ktor.server.core) } } + + jvmTest { + dependencies { + implementation(kotlin("test")) + + implementation(projects.grpc.grpcCore) + implementation(projects.grpc.grpcKtorServer) + + implementation(libs.grpc.kotlin.stub) + implementation(libs.grpc.netty) + + implementation(libs.ktor.server.core) + implementation(libs.ktor.server.test.host) + + runtimeOnly(libs.logback.classic) + } + } + } +} + +rpc { + grpc { + val globalRootDir: String by extra + + protocPlugins.kxrpc { + local { + javaJar("$globalRootDir/protobuf-plugin/build/libs/protobuf-plugin-$version-all.jar") + } + } + + project.tasks.withType().configureEach { + if (name.endsWith("Test")) { + dependsOn(":protobuf-plugin:jar") + } + } } } diff --git a/grpc/grpc-ktor-server-test/src/test/kotlin/kotlinx/rpc/grpc/ktor/server/test/TestServer.kt b/grpc/grpc-ktor-server/src/jvmTest/kotlin/kotlinx/rpc/grpc/ktor/server/test/TestServer.kt similarity index 100% rename from grpc/grpc-ktor-server-test/src/test/kotlin/kotlinx/rpc/grpc/ktor/server/test/TestServer.kt rename to grpc/grpc-ktor-server/src/jvmTest/kotlin/kotlinx/rpc/grpc/ktor/server/test/TestServer.kt diff --git a/grpc/grpc-ktor-server-test/src/test/proto/ktor-test-service.proto b/grpc/grpc-ktor-server/src/jvmTest/proto/ktor-test-service.proto similarity index 100% rename from grpc/grpc-ktor-server-test/src/test/proto/ktor-test-service.proto rename to grpc/grpc-ktor-server/src/jvmTest/proto/ktor-test-service.proto diff --git a/grpc/grpc-ktor-server-test/src/test/resources/logback.xml b/grpc/grpc-ktor-server/src/jvmTest/resources/logback.xml similarity index 100% rename from grpc/grpc-ktor-server-test/src/test/resources/logback.xml rename to grpc/grpc-ktor-server/src/jvmTest/resources/logback.xml diff --git a/protobuf-plugin/build.gradle.kts b/protobuf-plugin/build.gradle.kts index 4280e9a53..4ec6fcb6d 100644 --- a/protobuf-plugin/build.gradle.kts +++ b/protobuf-plugin/build.gradle.kts @@ -2,13 +2,15 @@ * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ +import kotlinx.rpc.buf.tasks.BufGenerateTask +import kotlinx.rpc.proto.kxrpc +import org.gradle.kotlin.dsl.withType import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode plugins { alias(libs.plugins.conventions.jvm) alias(libs.plugins.kotlinx.rpc) alias(libs.plugins.serialization) - alias(libs.plugins.protobuf) } dependencies { @@ -30,17 +32,10 @@ dependencies { testImplementation(libs.protobuf.kotlin) } -sourceSets { +protoSourceSets { test { proto { - exclude( - "**/enum_options.proto", - "**/empty_deprecated.proto", - "**/example.proto", - "**/multiple_files.proto", - "**/options.proto", - "**/with_comments.proto", - ) + exclude("exclude/**") } } } @@ -62,20 +57,18 @@ tasks.jar { ) } -val buildDirPath: String = project.layout.buildDirectory.get().asFile.absolutePath - rpc { grpc { - enabled = true - - plugin { - locator { - path = "$buildDirPath/libs/protobuf-plugin-$version-all.jar" + protocPlugins.kxrpc { + local { + javaJar(tasks.jar.map { it.archiveFile.get().asFile.absolutePath }) } } - tasksMatching { it.isTest }.all { - dependsOn(tasks.jar) + project.tasks.withType().configureEach { + if (name.endsWith("Test")) { + dependsOn(tasks.jar) + } } } } diff --git a/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/ProtoToModelInterpreter.kt b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/ProtoToModelInterpreter.kt index 4922018ab..bf5c9d843 100644 --- a/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/ProtoToModelInterpreter.kt +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/ProtoToModelInterpreter.kt @@ -294,6 +294,8 @@ class ProtoToModelInterpreter( val originalEntries = mutableMapOf() val aliases = mutableListOf() + val enumName = resolver.declarationFqName(name.fullProtoNameToKotlin(firstLetterUpper = true), parent) + valueList.forEach { enumEntry -> val original = originalEntries[enumEntry.number] if (original != null) { @@ -307,7 +309,7 @@ class ProtoToModelInterpreter( aliases.add( EnumDeclaration.Alias( - name = resolver.declarationFqName(enumEntry.name, parent), + name = resolver.declarationFqName(enumEntry.name, enumName), original = original, deprecated = enumEntry.options.deprecated, doc = null, @@ -315,7 +317,7 @@ class ProtoToModelInterpreter( ) } else { originalEntries[enumEntry.number] = EnumDeclaration.Entry( - name = resolver.declarationFqName(enumEntry.name, parent), + name = resolver.declarationFqName(enumEntry.name, enumName), deprecated = enumEntry.options.deprecated, doc = null, ) @@ -323,13 +325,13 @@ class ProtoToModelInterpreter( } originalEntries[-1] = EnumDeclaration.Entry( - name = resolver.declarationFqName(ENUM_UNRECOGNIZED, parent), + name = resolver.declarationFqName(ENUM_UNRECOGNIZED, enumName), deprecated = false, doc = null, ) return EnumDeclaration( - name = resolver.declarationFqName(name.fullProtoNameToKotlin(firstLetterUpper = true), parent), + name = enumName, outerClassName = outerClassName, originalEntries = originalEntries.values.toList(), aliases = aliases, diff --git a/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/RpcProtobufPlugin.kt b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/RpcProtobufPlugin.kt index 77ad16b86..2bd0b5c7e 100644 --- a/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/RpcProtobufPlugin.kt +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/RpcProtobufPlugin.kt @@ -77,7 +77,9 @@ class RpcProtobufPlugin { CodeGeneratorResponse.File.newBuilder() .apply { val dir = file.packagePath - ?.replace('.', File.separatorChar)?.plus(File.separatorChar) + ?.takeIf { it.isNotEmpty() } + ?.replace('.', File.separatorChar) + ?.plus(File.separatorChar) ?: "" // some filename already contain package (true for Google's default .proto files) diff --git a/protobuf-plugin/src/test/proto/empty_deprecated.proto b/protobuf-plugin/src/test/proto/exclude/empty_deprecated.proto similarity index 100% rename from protobuf-plugin/src/test/proto/empty_deprecated.proto rename to protobuf-plugin/src/test/proto/exclude/empty_deprecated.proto diff --git a/protobuf-plugin/src/test/proto/enum_options.proto b/protobuf-plugin/src/test/proto/exclude/enum_options.proto similarity index 50% rename from protobuf-plugin/src/test/proto/enum_options.proto rename to protobuf-plugin/src/test/proto/exclude/enum_options.proto index 0e8380ab8..1e85b4fcb 100644 --- a/protobuf-plugin/src/test/proto/enum_options.proto +++ b/protobuf-plugin/src/test/proto/exclude/enum_options.proto @@ -3,11 +3,11 @@ syntax = "proto3"; package kotlinx.rpc.protobuf.test; import "google/protobuf/descriptor.proto"; -import "options.proto"; +//import "options.proto"; -extend google.protobuf.EnumValueOptions { - optional Options options = 50000; -} +//extend google.protobuf.EnumValueOptions { +// optional Options options = 50000; +//} enum EnumOptions { option allow_alias = true; @@ -15,8 +15,8 @@ enum EnumOptions { ONE = 1; ONE_SECOND = 1; TWO = 2 [deprecated = true]; - THREE = 3 [ - (options).string = "three", - (options).inner.string = "inner three" - ]; + THREE = 3; // [ +// (options).string = "three", +// (options).inner.string = "inner three" + //]; } diff --git a/protobuf-plugin/src/test/proto/example.proto b/protobuf-plugin/src/test/proto/exclude/example.proto similarity index 100% rename from protobuf-plugin/src/test/proto/example.proto rename to protobuf-plugin/src/test/proto/exclude/example.proto diff --git a/protobuf-plugin/src/test/proto/multiple_files.proto b/protobuf-plugin/src/test/proto/exclude/multiple_files.proto similarity index 100% rename from protobuf-plugin/src/test/proto/multiple_files.proto rename to protobuf-plugin/src/test/proto/exclude/multiple_files.proto diff --git a/protobuf-plugin/src/test/proto/options.proto b/protobuf-plugin/src/test/proto/exclude/options.proto similarity index 100% rename from protobuf-plugin/src/test/proto/options.proto rename to protobuf-plugin/src/test/proto/exclude/options.proto diff --git a/protobuf-plugin/src/test/proto/with_comments.proto b/protobuf-plugin/src/test/proto/exclude/with_comments.proto similarity index 100% rename from protobuf-plugin/src/test/proto/with_comments.proto rename to protobuf-plugin/src/test/proto/exclude/with_comments.proto diff --git a/settings.gradle.kts b/settings.gradle.kts index 255cff875..8f60e4429 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -32,8 +32,6 @@ includePublic(":protobuf-plugin") include(":grpc") includePublic(":grpc:grpc-core") includePublic(":grpc:grpc-ktor-server") -// temporary module until KMP project structure support in Protobuf plugin -include(":grpc:grpc-ktor-server-test") includePublic(":bom") diff --git a/tests/compiler-plugin-tests/src/testData/diagnostics/rpcChecked.fir.txt b/tests/compiler-plugin-tests/src/testData/diagnostics/rpcChecked.fir.txt index 9c77a3020..9499a936f 100644 --- a/tests/compiler-plugin-tests/src/testData/diagnostics/rpcChecked.fir.txt +++ b/tests/compiler-plugin-tests/src/testData/diagnostics/rpcChecked.fir.txt @@ -61,6 +61,12 @@ FILE: rpcChecked.kt } @R|Grpc|() public abstract interface MyGrpcService : R|kotlin/Any| { + public final class $rpcServiceStub : R|kotlin/Any| { + public final companion object Companion : R|kotlin/Any| { + } + + } + } @R|Grpc|() public final class WrongGrpcTarget : R|kotlin/Any| { public constructor(): R|WrongGrpcTarget| { diff --git a/versions-root/libs.versions.toml b/versions-root/libs.versions.toml index c3332abaf..8c0ce15d2 100644 --- a/versions-root/libs.versions.toml +++ b/versions-root/libs.versions.toml @@ -32,6 +32,7 @@ grpc = "1.73.0" grpc-kotlin = "1.4.1" protobuf = "4.31.1" protobuf-gradle = "0.9.5" +buf-tool = "1.55.1" [libraries] # kotlinx.rpc – references to the included builds