ちょっと Java(Kotlin) でツールでも作ろうと思ったので、勉強のためにチュートリアルやってみた。やってみたという話なので何も深いことは書いてない。
3 行でまとめると
- Picocli のドキュメントの introduction にある checksum コマンドを作ってみた
- GraalVM の
native-image
コマンドでネイティブバイナリーにした - Gradle の
graalvm-native-image
というプラグインがよく出来てる
JVM ベースのコマンドラインアプリを作りたくなったので、前々から気になっていた Picocli を試してみることにした。
Picocli のページの先頭にある checksum
コマンドを写経することで雰囲気を確かめる。
まず build.gradle
を作る。 gradle init --type kotlin-application --dsl groovy
でできた build.gradle
に Picocli を追加する。
plugins { id 'org.jetbrains.kotlin.jvm' version '1.3.72' id 'org.jetbrains.kotlin.kapt' version "1.3.72" id 'application' } repositories { mavenCentral() jcenter() } dependencies { kapt 'info.picocli:picocli-codegen:4.2.0' implementation platform('org.jetbrains.kotlin:kotlin-bom') implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' implementation 'info.picocli:picocli:4.2.0' testImplementation 'run.ktcheck:ktcheck:v0.1.0' }
次に checksum
のコードを写経する。
import picocli.CommandLine import java.io.File import java.math.BigInteger import java.nio.file.Files import java.security.MessageDigest import java.util.concurrent.* @CommandLine.Command(name = "checksum", mixinStandardHelpOptions = true, version = ["0.0"]) class App: Callable<Int> { @CommandLine.Option(names = ["-a", "--algorithm"], description = ["MD5 SHA-1, SHA-256"]) var algorithm: String = "MD5" @CommandLine.Parameters(index = "0", description = ["target file"]) lateinit var file: File override fun call(): Int = file.toPath() .let { Files.readAllBytes(it) } .let { it to MessageDigest.getInstance(algorithm) } .let { it.second.digest(it.first) } .let { "%0${it.size * 2}x" to BigInteger(1, it) } .let { it.first.format(it.second) } .let { println(it) } .let { 0 } } @Suppress("ReplaceJavaStaticMethodWithKotlinAnalog") fun main(args: Array<String>) = System.exit(CommandLine(App()).execute(*args))
少しむずかしいと感じたのは、 CommandLine
のコンストラクターに渡すのが Any
であったため、何を渡せるのかわからなかった。 コードを読むと、 Runnable
あるいは Callable<Int>
が渡せるとのことで、上のようなコードになっているが、 Kotlin の関数いけるだろうと思って渡してみたところ、次のような例外が発生した。
picocli.CommandLine$ExecutionException: Parsed command (org.mikeneck.instant.App@5d6f64b1) is not a Method, Runnable or Callable at picocli.CommandLine.executeUserObject(CommandLine.java:1822) at picocli.CommandLine.access$900(CommandLine.java:145) at picocli.CommandLine$RunLast.executeUserObjectOfLastSubcommandWithSameParent(CommandLine.java:2150) at picocli.CommandLine$RunLast.handle(CommandLine.java:2144) at picocli.CommandLine$RunLast.handle(CommandLine.java:2108) at picocli.CommandLine$AbstractParseResultHandler.execute(CommandLine.java:1975) at picocli.CommandLine.execute(CommandLine.java:1904) at org.mikeneck.instant.AppKt.main(App.kt:32)
まあ、当然であるといえば、当然である。 CommandLine
にドキュメントに書いてあるとおりの Callable<Integer>
を受け取るコンストラクターと Runnable
を受け取るコンストラクターがあると良さそうだと思いましたが、多分誰もが思っていて、特にイシューとしてあげていない様子を見ると、おそらくこれがメインの使い方ではないのだろうなと思う。
話はもとに戻して、まずはこれを Java のアプリケーションとして起動する。 Gradle に application
プラグインを入れてはあるが、パラメーターを渡しづらいので、 java
コマンドを表示するタスクを追加する。
task appCopy(type: Copy, group: 'build') { from configurations.runtimeClasspath.asFileTree from jar destinationDir file("$buildDir/jars") } task showCommand(dependsOn: 'appCopy', group: 'application') { doLast { println("java -cp ${project.fileTree("$buildDir/jars").asPath} ${application.mainClassName}") } }
雑なので、全部のライブラリーとビルドしたアプリケーションの jar を一旦コピーして、コピー先にある jar の一覧をパス形式で表示するだけにしてみた。
これを実行すると、次のように表示されるので、コピペする
java -cp /Users/mike/ghq/github.com/mike-neck/instant/build/jars/picocli-4.2.0.jar:/Users/mike/ghq/github.com/mike-neck/instant/build/jars/annotations-13.0.jar:/Users/mike/ghq/github.com/mike-neck/instant/build/jars/kotlin-stdlib-jdk7-1.3.72.jar:/Users/mike/ghq/github.com/mike-neck/instant/build/jars/kotlin-stdlib-common-1.3.72.jar:/Users/mike/ghq/github.com/mike-neck/instant/build/jars/instant-0.jar:/Users/mike/ghq/github.com/mike-neck/instant/build/jars/kotlin-stdlib-1.3.72.jar:/Users/mike/ghq/github.com/mike-neck/instant/build/jars/kotlin-stdlib-jdk8-1.3.72.jar org.mikeneck.instant.AppKt
で、実行した結果がこちら
次に、これを GraalVM で native-image コマンドでネイティブバイナリーにしていく。
まずは GraalVM の最新版をインストールする。 Java のバージョンは 11 とする。
sdk i java `sdk l java | grep grl | head -n 1 | awk '{print $NF}'`
2020/04/24 0:42 (Asia/Tokyo) だと、 20.0.0.r11-grl がインストールされる。なお、graalVM のダウンロードには数時間かかる。
GraalVM では native-image
はオプションのツールなので、 gu
(GraalVM updater) でインストールする必要がある。
gu install native-image
これはすぐインストールできる。
ここで native-image
コマンドを叩いてとなるが、やっぱり面倒なので、そのあたりのコマンドをラップしている Gradle のプラグインがないか探してみると…ちょうど今のユースケースにピッタリのプラグインがあるようです。
では README に書いてあるとおりにやってみましょう。
次のような記述を build.gradle
に追加します(ほぼコピペ)。
plugins { // 追加部分のみ id 'org.mikeneck.graal-native-image' version '0.3.0' } nativeImage { graalVmHome = "${System.getProperty('java.home')}" mainClass = application.mainClassName executableName = 'instant' arguments( '--no-fallback' ) }
さて、 Gradle を実行する Java を 先程インストールした GraalVM のものに変更して、 Gradle を実行します。
Build file '/Users/mike/ghq/github.com/mike-neck/instant/build.gradle' line: 5 Plugin [id: 'org.mikeneck.graal-native-image', version: '0.3.0'] was not found in any of the following sources:
おっと、なんだぁ、そんなプラグインがないとか言われているぞ!?
もう一度、 Gradle Plugins repository の記述を比較しながら確認してみると、どうやら README に書いてある内容が間違えているようです。 graal-native-image
プラグインではなく、 graalvm-native-image
プラグインですね…あとで他の人が間違えないように、このレポジトリーに PR を出してみました。
というわけで、記述を修正して次のようなスクリプトになりました。
plugins { // 追加部分のみ id 'org.mikeneck.graalvm-native-image' version '0.3.0' } // application ブロックの下に追記する nativeImage { graalVmHome = "${System.getProperty('java.home')}" mainClass = application.mainClassName executableName = 'instant' arguments( '--no-fallback' ) }
実行すると次のような出力が得られる。
$ ./gradlew nativeImage > Task :kaptGenerateStubsKotlin UP-TO-DATE > Task :kaptKotlin UP-TO-DATE > Task :compileKotlin UP-TO-DATE > Task :compileJava NO-SOURCE > Task :processResources NO-SOURCE > Task :classes UP-TO-DATE > Task :inspectClassesForKotlinIC UP-TO-DATE > Task :jar UP-TO-DATE > Task :nativeImage Build on Server(pid: 30651, port: 61202)* [instant:30651] classlist: 1,386.61 ms, 1.42 GB [instant:30651] (cap): 3,010.14 ms, 1.42 GB [instant:30651] setup: 4,103.60 ms, 1.42 GB [instant:30651] (typeflow): 6,363.44 ms, 2.83 GB [instant:30651] (objects): 6,652.70 ms, 2.83 GB [instant:30651] (features): 313.80 ms, 2.83 GB [instant:30651] analysis: 13,661.96 ms, 2.83 GB [instant:30651] (clinit): 312.94 ms, 2.83 GB [instant:30651] universe: 725.00 ms, 2.83 GB [instant:30651] (parse): 1,243.97 ms, 2.83 GB [instant:30651] (inline): 2,551.83 ms, 2.83 GB [instant:30651] (compile): 10,601.32 ms, 3.77 GB [instant:30651] compile: 15,247.51 ms, 3.77 GB [instant:30651] image: 1,996.63 ms, 3.77 GB [instant:30651] write: 513.38 ms, 3.77 GB [instant:30651] [total]: 37,844.85 ms, 3.77 GB BUILD SUCCESSFUL in 39s 6 actionable tasks: 1 executed, 5 up-to-date
README によると成功すると、 build/native-image
ディレクトリーに指定した executableName
の値の名前を持つファイルができているとのこと。
$ ls -l build/native-image/ total 25280 -rwxr-xr-x 1 mike staff 12943244 Apr 24 01:02 instant
ではこれを実行してみると…
という感じで上手く実行できているようです。
以上、 Picocli と GraalVM native-image(Gradle graalvm-native-image プラグイン) を用いて Kotlin で CLI アプリケーションを作ってみました