私は今回の件より前から CircleCI を使っておらず、最近は主に GitHub Actions でCIを構築していてこの件で被害は被っていないのだけど、軽く調べてみたところ Android project を JDK 9+ でビルドすることに強い興味をを持っている自分には結構興味深いことがわかったのでもう少し詳細に調べてまとめることにした。
何があったの
日本時間で 2020-08-18 のお話
原因1: なぜビルドが失敗するようになったの
この blog を書き始めてから調べて知った付け焼き刃な部分もあり正確な説明になっていない部分もあると思うが (詳しい方指摘ください...)、大雑把には JDK 9 で導入された Project Jigsaw*2 と呼ばれる module system に関する変更によって標準の package に破壊的な変更が加わり、それによってこれまでデフォルトでアクセスできた package にアクセスできなくなったことが原因である。
具体的には
- DataBinding -> JAXB
- JaCoCo ->
jdk.internal.reflect
- JDK 9 でデフォルトでアクセス不可能になった*5
という感じ。
原因2: なぜJDKのアップデートが行われたの
この変更は予告なく突然行われたわけではなく、実際には7月から変更のアナウンスが forum で発表されていた。
discuss.circleci.com
この投稿から辿っていくと、以下の issue がきっかけのように思われる。
github.com
この issue は targetSdkVersion
を29にしているため Robolectric を使って実行するテストが Java 8 では動かないので、Java のバージョンを上げてくれというものだ。これは Robolectric 4.3 から適用される制約で、 release note で簡単に触れられているが、SDK 29 以降で Robolectric を使用する場合には runtime に Java 9 以上が要求され、Java 8 で実行するとエラーで止まるようになった。
https://github.com/robolectric/robolectric/releases/tag/robolectric-4.3
Running tests on Android Q requires Java 9.
https://github.com/robolectric/robolectric/releases/tag/robolectric-4.3.1
Running tests on Android API 29 now strictly requires a Java9 runtime or newer.
ところでこの問題自体は JDK のバージョンを上げなくても回避可能で、テスト実行時のSDKのバージョンを28以下に指定すればよく、 robolectric.properties
に以下の設定を追加すれば良い:
sdk=28
これによって robolectric.properties
が配置されている module での Robolectric を使ったテストでは指定した SDK version に基づいてテストを実行してくれる (この場合でも警告は出力される)。この指定をした場合でも、個別に @Config
annotation で指定した SDK version が優先されるので影響は出ない*6。
大半の場合はこれで解決するのでJDKのバージョンを上げる必要はないのだが、一つだけ上記の回避方法では解決できずに Java のバージョンを上げなければ解決しないケースがあり、それは minSdkVersion
が29以上のときである。このケースでは robolectric.properties
で sdk
のバージョンを下げてしまうと targetSdkVersion
が minSdkVersion
を下回る状態になってしまい下記のようなエラーになる。
java.lang.RuntimeException: android.content.pm.PackageParser$PackageParserException: <path to apk for local test>: Requires newer sdk version #29 (current version is #28)
解決方法
動かせた頃の image を使う
CircleCI Japan のアカウントで紹介されているように*7、JDK 8 ベースだった時の docker image の digest を、使用する image の指定に追記することで使う image の固定ができる。今回の件で知ったが、これは document でも best practice として言及されている*8。
docker:
- image: circleci/android:api-29@sha256:<digest>
JDK 11 でもビルドできるように project を修正する
DataBinding
公式のアナウンスはまだなかった気がするが、 Android Gradle Plugin 4.1.0 で修正されているらしく、そちらでは JDK 11 を使っても DataBinding 関連のビルドが成功するようになっている*9*10。手元で動かしてみた感じでは 4.0.0, 4.0.1 でも同様にビルドできたので、4.0.0 にも backport されているらしい。
また、何らかの理由で AGP を 4.0.0 以上に上げられない場合は、JAXB関連の依存を自分の project に追加することでも解決できる:
dependencies {
annotationProcessor 'javax.xml.bind:jaxb-api:2.3.1'
annotationProcessor 'com.sun.xml.bind:jaxb-core:2.3.0.1'
annotationProcessor 'com.sun.xml.bind:jaxb-impl:2.3.2'
kapt 'javax.xml.bind:jaxb-api:2.3.1'
kapt 'com.sun.xml.bind:jaxb-core:2.3.0.1'
kapt 'com.sun.xml.bind:jaxb-impl:2.3.2'
}
JaCoCo
JaCoCo の設定に excludes
という coverage 対象から除外するパターンを追加できるものがあるが、それに以下のように jdk.internal.
から始まる package を追加することで解決可能*11*12。
tasks.withType(Test) {
jacoco {
includeNoLocationClasses = true
excludes = ['jdk.internal.*']
}
}
余談: JDK 11 になって嬉しいこともある
CircleCI で Java project といえばOOMだったりCPUの core を container に割り当てられた数よりも多く認識してしまったりという問題をどう対処するかという話があり、公式でも幾らかの言及がある*13*14。これはJVMの挙動の都合で cgroup による割り当て設定を見ないことが原因の一つなのだが、Java 10 で UseContainerSupport
という VM option がデフォルト有効で導入され、cgroup で割り当てられた設定が反映されるようになり、また同じく追加された MaxRAMPercentage
で割り当てられたRAMの量に対するパーセンテージで heap size の上限を定められるようになった。
この辺の話は本当に詳しくないので適当に記事のリンク貼ってお茶を濁させて。
www.eclipse.org
developers.redhat.com
merikan.com
medium.com
というわけで、もともと JDK 11 でもビルドできたり、先に述べたような対応をして JDK 11 でもビルドできるようにしたりした project は、上記の VM option の恩恵を得られるようになる。とはいえ昨今のCIを構築している Android project は大体が対策として Gradle の worker の数を絞ったり Xmx
を設定したりしていると思うので、今なおRAMやCPUの問題で苦しんでいる人はそれほど多くないような気がするし、それらの対策のためにわざわざ JDK 11 対応をするにしてもモチベーションとしては弱いかもしれない。
雑感
この blog を書き始めた当初は AGP 4.0.0 以上でも JDK 11 で DataBinding がビルドできないものと思っていたので、こんな影響評価のお粗末なサービスは使わないで他のCIサービスに移行した方が良いですよみたいな締めくくりにして、ついでに移行先候補についても幾らか書こうと思っていた程度には個人的には印象が悪かった。その後調べた結果として DataBinding を捨てるなど大規模な修正なしに Android project 側でも対応可能なのがわかって、そこまで言う程ではないという評価に変わってその部分は削ったものの、実際はどうあれ CircleCI (の Android まわりに関わっている人) には Java や Android の情勢に詳しい人はいないんだろうなあ、という不信感は残った。
発端がユーザからのリクエストで、それに素早く対応する姿勢はとても好ましいものだと思う。しかし解決したいこと (Robolectric 4.3 以上を動かせるようにすること) に対してとった手段 (JDK 11 へのアップデート) によって発生した影響が釣り合っていないので、ちゃんと JDK のアップデート内容やそれによって起こりうる影響の調査をしたのだろうかという疑問がある。彼らは docker image の更新をリリースした当時 JDK 9 でJAXBのAPIにアクセスできなくなることが DataBinding のビルドに影響を与えることを知らなかったのだろうし*15、もしかしたら実行時の Android SDK のバージョンが29以上であることが必要でない限り Java 8 でも Robolectric 4.3 以上を使ったテストを実行できるようにする方法があることを今でも知らないかもしれない。そもそもまだ Android Studio に組み込まれている JDK は4.2 Canary でも JDK 8 で、Android アプリ開発の現場の大半では開発環境に JDK 8 を想定していると見て良いはずなのに、1つのライブラリに対応するために自分たちのメインストリームをそこから外していく判断はちょっとよくわからなかった。CircleCI としては過去のバージョンを digest 使って固定できるし、それを将来の変更に備えるための best practice として紹介しているという言い分はあると思うけど。
それはそうと Java 8 のリリースは2014年と6年も前のことであり、現在の Java の最新は14, 来月には Java 15 がリリースされ、来年2021年には Java 11 の次のLTSである Java 17 がリリースされる予定である*16。Java 8 までと Java 9 以降でリリースサイクルが変わったとはいえ、様々なアップデートが行われた、また行われている Java において8はもはやレガシーな環境だと言って良いと思われる。Android OS それ自体は Java そのものを使っているわけではないのでAPIの追従に時間がかかるのは仕方ないことだが、ビルドや local test の実行に関しては Java の後方互換性のおかげで結構なんとかなるし*17、現時点で最も困難な (と自分は思っている) Java 9 の壁を越えられるようになったことがわかったので、今後は Android に対して新しいJDKへのサポートを積極的に要求していっても良いのかもしれない。
www.oreilly.com
www.manning.com