@hotchemiです。みなさんは、やっていますか?
今回は、業務とは関係ない趣味で作ったkompile-testingの話をします。
概要
Googleが開発しているcompile-testingというツールがあります。これは、Javaのコンパイル及びAnnotation Processingというコンパイル時にAnnotationのメタ情報を元にコードやファイル生成をする仕組みをテストする為のライブラリで、Daggerなどポピュラーなライブラリのテストに利用されています。以下の用に書く事でコンパイル後の状態をテストする事ができます。
Compilation compilation = javac() .withProcessors(new MyAnnotationProcessor()) .compile(JavaFileObjects.forResource("HelloWorld.java")); assertThat(compilation).succeeded(); assertThat(compilation) .generatedSourceFile("GeneratedHelloWorld") .hasSourceEquivalentTo(JavaFileObjects.forResource("GeneratedHelloWorld.java"));
このライブラリはGoogleのJava Core Libraries Teamが開発しているだけあって素晴らしいのですが、昨今の(特にAndroid界の)時代の趨勢としてはKotlinがその勢いを増してきています。
そうなれば当然Kotlinのコードを入力としてKotlinのコードを出力したいという要望が出てきますが、残念ながらcompile-testingはこの問題に対応していません。理由は単純で、Kotlinはkaptという独自のシステムをjavacとは別のタイミングで動作させる為です。
そこでこの問題に対応したものが、kompile-testingです。compile-testingとほぼ同じAPIインタフェースでKotlinのコード生成をテストできます。やりましたね。
kotlinc() .withProcessors(YourProcessor()) .addKotlin("input.kt", """ import kompile.testing.TestAnnotation @TestAnnotation class TestClass """.trimIndent()) .compile() .succeededWithoutWarnings() .generatedFile("generatedKtFile.kt") .hasSourceEquivalentTo("class GeneratedKtFile")
Under the hood
これだけだと味気無いので、どういう仕組みで実現しているか簡単に解説します。Compiler.ktの処理をざっくり分類すると、以下の様な感じになります。
- withProcessorsでプロセッサのclass情報を保持しておく
- addKotlinで入力値をfileとして書き出す
- compileでクラスパスとkaptのオプションを設定してあげて
K2JVMCompiler#exec
を呼んで実際にコンパイラを起動する。コンパイラのexit codeと出力を戻り値として返してあげる
要するに実際にKotlin Compilerをプログラマブルに実行させてしまおう、という事ですね。実際そんなに難しい事はしていないのですが、compile
の処理はもう少し説明をしてみます。また、前提としてKotlin compilerやkaptのオプションに関する詳細は以下のドキュメントに書かれています。
- https://kotlinlang.org/docs/tutorials/command-line.html
- https://kotlinlang.org/docs/reference/kapt.html#using-in-cli
Compiler#compile
- -dオプションでclassの出力先を指定しています。
- -no-stdlibはクラスパスにstdlibが含まれていないとwarningが出るのをsuppressする為のオプションです。
- -classpathでクラスパスを指定しています。生成ロジックに関してはこちらにまとまっています。classLoaderから引っ張り出してきて
aar
の場合はjarだけ抽出してあげています。力技ですね。 - この辺でFileに書き出したinputファイルをコンパイル対象として指定しています。
- annotationProcessorArgs()(定義)でkaptの実行及び関連オプションについて指定しています。公式ドキュメントにありますが、kaptはcompiler pluginという機構の一種として指定されており
Xplugin
オプションでjarを指定する事で実行できます。kaptのjarはkompile-testingをgradle dependenciesとして設定すると自動でクラスパスに追加されるようになっているのでそこから持ってきています。後のオプションは公式ドキュメントのコピペですが、apclasspath
で指定するprocessorのpathはjarとして書き出さないといけない為writeServicesJarで保持しているProcessorのクラス情報をjarに書き出しています。力技ですね。 - この辺で、kapt自体に渡すオプションの設定をしています。
kapt.kotlin.generated
というオプションで指定されたディレクトリの中にKotlinファイルを書き出すとコンパイル対象になる為、Processor側でこのオプションを見ている事が殆どなのでテスト用に追加しています。 - 最後に、今まで生成したオプションを
K2JVMCompiler#exec
に渡します。K2JVMCompiler
にはerrorを受け取るstreamを指定できる為コンパイル時に吐かれるerrorやwaningを受け取る事ができます。
最後に
現在このライブラリのversionは0.1.1となっており最低限の機能を実装した段階です。今後は主にパフォーマンス面の問題とcompile-testingに存在するAPIを補完する方向で改善を進めていく予定となっています。
また、リリースした所早速arrow-ktの開発チームに利用してもらっている様で、嬉しいですね。CompilerのAPIというのは業務では触れる事が少ない分野ですが、触れてみると沢山の楽しい発見がある為まとまった時間を取ってトライしてみると良い事があるかもしれません。
現場からは以上です。