概要
gRPCのQuick Start(Kotlin版)を動かしてみたのですが、私はこれまでGradleというものに触ったことが無く、Gradleのビルドスクリプトを読み解くのにとっても大変な思いをしました。
誰の役に立つのか分かりませんが、せっかく読み解けてきたのでメモしておきたいと思います。
見やすさなどは全く考えておりません。悪しからず。
読んでみたGradleスクリプト
gRPCのQuick Start(Kotlin版)のソースコードは、以下のリポジトリにあります。
https://github.com/grpc/grpc-kotlin
この中に「examples」ディレクトリがあるのですが、その中にあるGradleスクリプトを読んでみました。
GradleのビルドスクリプトにはGroovy版とKotlin版があるのですが、上記リポジトリにはKotlin版が入ってましたので、Kotlin版を読み解きました。(Groovy版と大差は無いはずです)
読み解きメモ
// この「plugins{}」というのは、実はメソッドではない。
// Projectのメソッドかと思ってしまうが、実は違う。
// 「plugins{}」は、他のスクリプトを実行するのに必要なクラスパスを構成するものであり、
// build.gradleを構成する要素の中では特殊な位置付けにある。
// gradleは、この「plugins{}」を見つけると、
// そこに定義されているクロージャをもとにクラスパスを構成する。
// see: https://discuss.gradle.org/t/method-for-plugins/37834
plugins {
// pluginsメソッドに渡すクロージャのdelegateは、
// PluginDependenciesSpecオブジェクト。
id("com.android.application") version "4.0.0" apply false
// PluginDependenciesSpecのidメソッドを呼び出し、
// その戻りオブジェクト(PluginDependencySpec)のversionメソッドを呼び出している。
// 次に、versionメソッドの戻りオブジェクト(PluginDependencySpec。つまりthis。)
// のapplyメソッドを呼び出している。
// applyメソッドにfalseを指定すると、
// プラグイン(Pluginインタフェースを実装したJavaクラス)が単にクラスパスに追加されるだけとなる。
// プラグインのクラスを個別に呼び出したい場合や、
// マルチプロジェクト構成のルートのプロジェクトではfalseを指定する。
// 特に、マルチプロジェクト構成の場合、ルートではapplyするのはNGであり、
// サブプロジェクトのみでapplyが許可されている。
// see: https://docs.gradle.org/current/userguide/plugins.html#sec:subprojects_plugins_dsl
// 逆に、applyメソッドにtrueを指定すると、Pluginインタフェースのapplyメソッドが実行されるはず。
// 多くの場合、applyメソッドにはProjectオブジェクトが渡され、
// Projectオブジェクトにタスクが追加されるなどのカスタマイズが行われる。
// gradleは http://plugins.gradle.org/ にプラグイン(JARファイル)を探しに行く。
// コアプラグインと呼ばれる一部のプラグインは、id("java")といった短縮名で特定されるが、
// それ以外のプラグインは、上記のid("com.android.application") のように、
// 完全修飾名によって特定される。
id("com.google.protobuf") version "0.8.13" apply false
// kotlinメソッドはKotlin版のPluginDependenciesSpecに定義されている。
// see: https://gradle.github.io/kotlin-dsl-docs/api/org.gradle.kotlin.dsl/org.gradle.plugin.use.-plugin-dependencies-spec/kotlin.html
// パラメータはモジュール名であり、"jvm", "js" (=JavaScript), "android"などを指定できる。
// どこでKotlinコードを動かすか、を指定するということ。
// それぞれで、Gradleでどんな動きをすべきかが異なるので、プラグインも分かれている。
// see: https://kotlinlang.org/docs/reference/using-gradle.html
kotlin("jvm") version "1.3.72" apply false
id("org.jlleitschuh.gradle.ktlint") version "9.2.1"
}
// 独自のプロパティ(Extra Properties)は、Project.extに定義する必要がある。
// see: https://docs.gradle.org/current/dsl/org.gradle.api.Project.html#N15043
ext["grpcVersion"] = "1.32.1"
ext["grpcKotlinVersion"] = "0.2.0" // CURRENT_GRPC_KOTLIN_VERSION
ext["protobufVersion"] = "3.13.0"
// Project.allprojectsメソッドは、
// このビルドスクリプトに対応するプロジェクト(ルートのプロジェクト)とそのサブプロジェクトについて
// Projectオブジェクトのrepositoriesメソッドなどを呼び出し、それぞれのProjectを設定する。
allprojects {
repositories {
mavenLocal()
mavenCentral()
jcenter()
google()
}
apply(plugin = "org.jlleitschuh.gradle.ktlint")
}
// コロンはディレクトリの区切り文字であり、スラッシュみたいなもの。
// 以前はディレクトリの区切り文字はスラッシュだったが、色々と経緯があってコロンとなった。
// see: https://qiita.com/opengl-8080/items/4c1aa85b4737bd362d9e#%E5%9F%BA%E6%9C%AC
// この場合、このbuild.gradleのあるディレクトリを起点として、
// serverサブプロジェクトのinstallDistタスク
// (ApplicationプラグインによりProjectに追加されるタスク)に依存している、ということ。
// 2つ目のコロンはディレクトリと言って良いのか微妙だが。まあ、意味はわかる。
tasks.create("assemble").dependsOn(":server:installDist")
plugins {
// applicationプラグインを適用すると、Java Pluginも自動的に適用される。
application
// ルートプロジェクトで指定したversionが有効となるため、versionを指定する必要なし。
// applyの指定をしていないためデフォルトのtrueが効く。
// なので、Projectオブジェクトにタスクが追加される。
// このKotlin Gradle pluginを導入すると、
// 「api」Configurationを利用できるようになるため、
// Java Libraryプラグインも自動的に適用されるっぽい。
// see: https://kotlinlang.org/docs/reference/using-gradle.html#dependency-types
kotlin("jvm")
}
dependencies {
// Java Pluginにより導入された「implementation」Configurationを利用して、
// 依存ライブラリをグルーピングしている。
implementation(kotlin("stdlib"))
implementation(project(":stub"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8")
// Java Library Pluginにより導入された「api」Configurationを利用して、
// 依存ライブラリをグルーピングしている。
// Java Library Pluginsは、Kotlin Gradle pluginを適用することで
// 自動的に適用されているっぽい。s
// あるライブラリがコンパイル時に必要だが、
// 「このビルドスクリプトでビルドされるアーティファクト」を利用する側でもコンパイル時に必要(ここにAPIという概念が居る)、
// という時にこの「api」Configurationを利用する。依存関係を利用者側にも伝播させる、ということ。
api("com.google.protobuf:protobuf-java-util:${rootProject.ext["protobufVersion"]}")
// runtimeOnlyは、Java Pluginにより導入されたConfiguration。
runtimeOnly("io.grpc:grpc-netty:${rootProject.ext["grpcVersion"]}")
}
// TaskContainer.registerメソッドで、
// 新たなタスクをTaskContainerに登録だけする(生成と構成を行わない)
// see: https://docs.gradle.org/current/userguide/task_configuration_avoidance.html
// JavaExecタスクを登録している。
// これは、JVMを起動してJavaアプリを実行するタスク。クロージャで、その設定をしている。
// see: https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/JavaExec.html
tasks.register<JavaExec>("HelloWorldClient") {
dependsOn("classes")
classpath = sourceSets["main"].runtimeClasspath
main = "io.grpc.examples.helloworld.HelloWorldClientKt"
}
tasks.register<JavaExec>("RouteGuideClient") {
dependsOn("classes")
classpath = sourceSets["main"].runtimeClasspath
main = "io.grpc.examples.routeguide.RouteGuideClientKt"
}
tasks.register<JavaExec>("AnimalsClient") {
dependsOn("classes")
classpath = sourceSets["main"].runtimeClasspath
main = "io.grpc.examples.animals.AnimalsClientKt"
}
// CreateStartScripts型のタスクを登録
val helloWorldClientStartScripts = tasks.register<CreateStartScripts>("helloWorldClientStartScripts") {
mainClassName = "io.grpc.examples.helloworld.HelloWorldClientKt"
applicationName = "hello-world-client"
outputDir = tasks.named<CreateStartScripts>("startScripts").get().outputDir
classpath = tasks.named<CreateStartScripts>("startScripts").get().classpath
}
val routeGuideClientStartScripts = tasks.register<CreateStartScripts>("routeGuideClientStartScripts") {
mainClassName = "io.grpc.examples.routeguide.RouteGuideClientKt"
applicationName = "route-guide-client"
outputDir = tasks.named<CreateStartScripts>("startScripts").get().outputDir
classpath = tasks.named<CreateStartScripts>("startScripts").get().classpath
}
val animalsClientStartScripts = tasks.register<CreateStartScripts>("animalsClientStartScripts") {
mainClassName = "io.grpc.examples.animals.AnimalsClientKt"
applicationName = "route-guide-client"
outputDir = tasks.named<CreateStartScripts>("startScripts").get().outputDir
classpath = tasks.named<CreateStartScripts>("startScripts").get().classpath
}
// ApplicationプラグインのstartScriptsタスクを(生成や構成を経ないで)参照し、
// そのstartScriptsタスクが、上記で定義したhelloWorldClientStartScriptsとかに依存するように設定している。
tasks.named("startScripts") {
dependsOn(helloWorldClientStartScripts)
dependsOn(routeGuideClientStartScripts)
dependsOn(animalsClientStartScripts)
}
import com.google.protobuf.gradle.generateProtoTasks
import com.google.protobuf.gradle.id
import com.google.protobuf.gradle.plugins
import com.google.protobuf.gradle.protobuf
import com.google.protobuf.gradle.protoc
plugins {
kotlin("jvm")
id("com.google.protobuf")
}
dependencies {
protobuf(project(":protos"))
implementation(kotlin("stdlib"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8")
implementation("javax.annotation:javax.annotation-api:1.3.2")
api("com.google.protobuf:protobuf-java-util:${rootProject.ext["protobufVersion"]}")
api("io.grpc:grpc-stub:${rootProject.ext["grpcVersion"]}")
api("io.grpc:grpc-protobuf:${rootProject.ext["grpcVersion"]}")
api("io.grpc:grpc-kotlin-stub:${rootProject.ext["grpcKotlinVersion"]}")
}
java {
sourceCompatibility = JavaVersion.VERSION_1_7
}
// protobuf-gradle-pluginの使い方は以下に記載されている。
// see: https://github.com/google/protobuf-gradle-plugin
protobuf {
// protoc実行ファイルを、Maven Centralから取得する。
protoc {
artifact = "com.google.protobuf:protoc:${rootProject.ext["protobufVersion"]}"
}
// protocはコンパイラであり、その出力として特定の言語のスタブコードを生成する役割は
// codegenプラグインが担う。以下では、そのcodegenプラグインの設定を行っている。
// protocと特定の言語のcodegenを組み合わせて動かすことにより、
// *.protoファイルから特定の言語のソースコードが生成される。
plugins {
id("grpc") {
// Javaコードを生成するcodegenプラグインとして、
// Maven Centralのどのバージョンのライブラリを使うかを宣言している。
// ただ、ここでは宣言しているだけであって、
// この記述だけではプラグインは「適用」されない。
// 適用するには後述のgenerateProtoTasksを設定する必要がある。
artifact = "io.grpc:protoc-gen-grpc-java:${rootProject.ext["grpcVersion"]}"
}
id("grpckt") {
// Kotlinコードを生成するcodegenプラグイン
artifact = "io.grpc:protoc-gen-grpc-kotlin:${rootProject.ext["grpcKotlinVersion"]}:jdk7@jar"
}
}
// protobuf-gradle-pluginは、protocを実行する度にタスクを生成する。
// このタスクに対して、コードジェネレータとして何を使うかとかを設定することができる。
// generateProtoTasksでは、その設定を行う。
generateProtoTasks {
all().forEach {
// itは、protocを実行する度に生成されるタスクを意味する。
// このタスクは、builtins(protocに同梱されているコードジェネレータ)と
// plugins(protocと組み合わせるタイプのコードジェネレータ)
// の2種類のプロパティがあり、お好みの方を設定する。
// ここでは、pluginsとして、
// 上記で宣言したgrpcとgrpcktの2つのcodegenプラグインを設定している。
// この設定によってこれらのcodegenプラグインが「適用」される。
it.plugins {
id("grpc")
id("grpckt")
}
}
}
}
[補足] Java Plugin
Java Pluginを適用すると、Base Pluginも適用される。Base Pluginを適用すると、Projectオブジェクトにassembleとかcheckといったタスクが追加される。
Java Pluginは、Base Pluginのassembleタスクが、自身のjarタスクに依存するようにして、assembleタスクが実行されると、必ず自身のjarタスクが実行されるようにする。
【参考】
https://docs.gradle.org/current/userguide/base_plugin.html#sec:base_plugin_conventions
https://docs.gradle.org/current/userguide/more_about_tasks.html#sec:lifecycle_tasks
GradleではConfigulationという概念があり、全然正確ではないが、ざっくり言うと特定の目的でグルーピングされた依存ライブラリ群を表す。Mavenでいうスコープ。
Java Pluginの「implementation」というConfigurationは、コンパイル時に利用する依存ライブラリ群。Mavenでは何もしなくてもcompileスコープなどが使えていたが、GradleではJava Pluginを適用しないと同様のことはできない。
【参考】
https://docs.gradle.org/current/userguide/declaring_dependencies.html
https://docs.gradle.org/current/userguide/dependency_management_for_java_projects.html