nashcft's blog

時々何か書く。

CircleCI の Android Docker Image でJDKが11にアップデートされた件のまとめ

私は今回の件より前から CircleCI を使っておらず、最近は主に GitHub Actions でCIを構築していてこの件で被害は被っていないのだけど、軽く調べてみたところ Android project を JDK 9+ でビルドすることに強い興味をを持っている自分には結構興味深いことがわかったのでもう少し詳細に調べてまとめることにした。

何があったの

日本時間で 2020-08-18 のお話

  • CircleCI の Android Docker Image が更新され、JDK 8 ベースから JDK 11 ベースになった*1
  • これらの image を使ってCIを行っている Android project でビルドが失敗するものが発生した
  • Twitter が少し賑やかになった

原因1: なぜビルドが失敗するようになったの

この blog を書き始めてから調べて知った付け焼き刃な部分もあり正確な説明になっていない部分もあると思うが (詳しい方指摘ください...)、大雑把には JDK 9 で導入された Project Jigsaw*2 と呼ばれる module system に関する変更によって標準の package に破壊的な変更が加わり、それによってこれまでデフォルトでアクセスできた package にアクセスできなくなったことが原因である。

具体的には

  • DataBinding -> JAXB
    • JDK 9 でJAXBの package は deprecated となり、デフォルトで解決可能な classpath から外された*3
    • JDK 11 でJAXBを含む Java EEAPIJDKから削除された*4
  • 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 に以下の設定を追加すれば良い:

# 28未満でも良い
sdk=28

これによって robolectric.properties が配置されている module での Robolectric を使ったテストでは指定した SDK version に基づいてテストを実行してくれる (この場合でも警告は出力される)。この指定をした場合でも、個別に @Config annotation で指定した SDK version が優先されるので影響は出ない*6

大半の場合はこれで解決するのでJDKのバージョンを上げる必要はないのだが、一つだけ上記の回避方法では解決できずに Java のバージョンを上げなければ解決しないケースがあり、それは minSdkVersion が29以上のときである。このケースでは robolectric.propertiessdk のバージョンを下げてしまうと targetSdkVersionminSdkVersion を下回る状態になってしまい下記のようなエラーになる。

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 のアカウントで紹介されているように*7JDK 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 を使っている場合以下も追加する
  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.*' を追加
    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 まわりに関わっている人) には JavaAndroid の情勢に詳しい人はいないんだろうなあ、という不信感は残った。

発端がユーザからのリクエストで、それに素早く対応する姿勢はとても好ましいものだと思う。しかし解決したいこと (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 がリリースされる予定である*16Java 8 までと Java 9 以降でリリースサイクルが変わったとはいえ、様々なアップデートが行われた、また行われている Java において8はもはやレガシーな環境だと言って良いと思われる。Android OS それ自体は Java そのものを使っているわけではないのでAPIの追従に時間がかかるのは仕方ないことだが、ビルドや local test の実行に関しては Java後方互換性のおかげで結構なんとかなるし*17、現時点で最も困難な (と自分は思っている) Java 9 の壁を越えられるようになったことがわかったので、今後は Android に対して新しいJDKへのサポートを積極的に要求していっても良いのかもしれない。

www.oreilly.com

www.manning.com