以前、Scalaを使ってCLIツールを作るという記事を書いたのですが、やはりJVMベースのアプリケーションには起動時間のオーバーヘッドが付き物なのでちょっとしたCLIツールをScalaで書こうという感じにはなかなかなりません。そこで今回はPicocliというJava向けのCLIツール用ライブラリとGraalVMを使ってネイティブCLIコマンドをScalaで作る方法を試してみました。
sbtプロジェクトの作成
今回はPicocliのドキュメントで例として掲載されていたchecksumコマンドをScalaで実装してみます。まずは以下の内容でbuild.sbt
を作成します。
name := "picocli-scala-example" version := "0.1" scalaVersion := "2.12.7" libraryDependencies ++= Seq( "info.picocli" % "picocli" % "4.2.0" )
checksumコマンドのソースコードはこんな感じです。JavaコードをそのままScalaに書き換えただけです。コマンドやオプションの情報をアノテーションで宣言するのですが、フィールドをvar
で宣言しておかないといけないのがちょっと微妙かもしれないですね。
package com.github.takezoe import java.io.Fileimport picocli.CommandLine._ import java.math.BigInteger import java.nio.file.Files import java.security.MessageDigest import java.util.concurrent.Callable import picocli.CommandLine @Command(name = "checksum", mixinStandardHelpOptions = true, version = Array("checksum 4.0"), description = Array("Prints the checksum (MD5 by default) of a file to STDOUT.")) class CheckSum extends Callable[Int] { @Parameters(index = "0", description = Array("The file whose checksum to calculate.")) private var file: File = null @Option(names = Array("-a", "--algorithm"), description = Array("MD5, SHA-1, SHA-256, ...")) private var algorithm = "MD5" override def call(): Int = { val fileContents = Files.readAllBytes(file.toPath) val digest = MessageDigest.getInstance(algorithm).digest(fileContents) printf("%0" + (digest.length * 2) + "x%n", new BigInteger(1, digest)) 0 } } object CheckSum extends App { val exitCode = new CommandLine(new CheckSum()).execute(args:_*) System.exit(exitCode) }
Annotation Processorの実行
PicoliではGraalVMによるnative-image対応のためにAnnotation Processorを使用したコード生成が必要です。Annotation Processorはpicoli-codegenという別ライブラリとして提供されているのでこれをprovided
で依存関係に追加します。
libraryDependencies ++= Seq( "info.picocli" % "picocli" % "4.2.0", // Add a line below "info.picocli" % "picocli-codegen" % "4.2.0" % "provided" )
build.sbt
に以下のような感じの設定を追加して、processAnnotations
タスクでAnnotation Processorが実行されるようにします。
lazy val processAnnotations = taskKey[Unit]("Process annotations") processAnnotations := { val log = streams.value.log log.info("Processing annotations ...") val classpath = ((products in Compile).value ++ ((dependencyClasspath in Compile).value.files)) mkString ":" val destinationDirectory = (classDirectory in Compile).value val processor = "picocli.codegen.aot.graalvm.processor.NativeImageConfigGeneratorProcessor" val classesToProcess = Seq("com.github.takezoe.CheckSum") mkString " " val command = s"javac -cp $classpath -proc:only -processor $processor -XprintRounds -d $destinationDirectory $classesToProcess" runCommand(command, "Failed to process annotations.", log) log.info("Done processing annotations.") } def runCommand(command: String, message: => String, log: Logger) = { import scala.sys.process._ val result = command !if (result != 0) { log.error(message) sys.error("Failed running command: " + command) } }
なお、Picocliにはアノテーションを使わないAPIも用意されており、そちらを使用すればAnnotation Processorを使わなくてもネイティブイメージの生成に対応できます。
GraalVMのインストール
続いてGraalVMをインストールします。以下からGraalVMのCommunity Editionをダウンロードして適当なディレクトリに展開し、bin
ディレクトリを環境変数PATH
に追加しておきます。
ネイティブイメージの作成に必要なnative-image
コマンドは以下のようにして別途インストールする必要があります。
$ gu install native-image
ネイティブイメージの作成
sbtでのネイティブイメージの作成にはsbt-native-packagerを使用しました。
project/plugins.sbt
に以下の設定を追加します。
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.6.1")
また、build.sbt
に以下の設定を追加し、GraalVMNativeImagePlugin
を有効にするとともにパッケージ作成時にprocessAnnotations
タスクが実行されるようにしておきます。
packageBin in Compile := (packageBin in Compile dependsOn (processAnnotations in Compile)).value enablePlugins(GraalVMNativeImagePlugin)
これで準備は完了です。sbtを以下のように実行するとネイティブイメージが作成されます。
$ sbt graalvm-native-image:packageBin
ネイティブイメージはtarget/graalvm-native-image
ディレクトリにプロジェクトと同じ名前で生成されます。試しに実行してみましょう。
$ ./target/graalvm-native-image/picocli-scala-example Missing required parameter: <file> Usage: checksum [-hV] [-a=<algorithm>] <file> Prints the checksum (MD5 by default) of a file to STDOUT. <file> The file whose checksum to calculate. -a, --algorithm=<algorithm> MD5, SHA-1, SHA-256, ... -h, --help Show this help message and exit. -V, --version Print version information and exit.
こんな感じで動作します。
まとめ
ScalaとPicocliを使用してGraalVMでネイティブイメージを作成できることを確認できました。ScalaでCLIツールを書きたいけどJVMの起動時間が気になるという場合には選択肢になるのではないかと思います。また、以下の記事ではdeclineなどScala製のライブラリを活用したGraalVMによるネイティブCLIアプリケーションの作成方法を解説されています。ライブラリの選択など参考になります。
Scalaでのネイティブアプリケーションの作成にはscala-nativeという選択肢もありますが、活発に開発が行われているとは言い難い状況で現時点ではScala 2.11のサポートに留まっています。また、利用可能なライブラリもかなり限られており、実用にはちょっと厳しいかなという感じがあります。