記録

主にAndroidアプリ開発に関する知見やその周辺に関わることについて記事をかいています

Androidアプリのマルチモジュールでlintチェック結果をまとめてDangerでコメントする

Androidアプリ開発でマルチモジュールによる開発をすすめていた場合、Lintを実行すると各モジュールのbuildフォルダ配下にlintチェック結果のファイルが生成されます。もの挙動自体は正しくまったく問題はないのですが、GitHubを利用していてプルリクにDanger等でlintチェックの結果をコメントしていた場合、アプリケーションモジュールの結果のみがコメントされたり、特定のモジュールの結果のみがコメントされてしまいます。多くの場合、マルチモジュール開発でのモジュールは同じリポジトリ内に配置されていると思うので、これはなんとかして解決したいです。

解決の選択肢

この問題を解決する選択肢はいくつかある。

  1. モジュール毎にGitHubリポジトリを作成し、それぞれCIを設定する
  2. モジュールの数だけDangerを実行する
  3. lintチェック結果をまとめて一度だけDangerを実行する

選択肢1

モジュール毎にGitHubリポジトリを作成することで、そもそもプルリクがモジュール毎に行われるため、問題が発生しないという考え方です。ただ、自分の開発している環境ではモジュールが20近くあるのでこれは現実的じゃないかなと。また、Androidアプリ開発のマルチモジュールでモジュール毎にリポジトリを切る運用はあまりみたことがない。なぜなら、各モジュールがコアなモジュールに依存していたりとモジュール間の依存が結構発生するのと、リファクタリング時に他のモジュールのリファクタリングが走らなくなるので、モジュール毎にちゃんとライブラリ的な管理が必要になるからとかいろいろある。とりあえずこの選択肢はなし。

選択肢2

モジュールの数だけDangerを実行する場合は、それぞれDangerfileを作成して結果のXMLファイルそれぞれ指定してあげる必要があります。これはモジュールが増えるたび、Dangerfileの作成とCIの設定が必要になるのでやりたくない。また、ひとつのプルリクに対して複数のDangerを実行するとコメントが最後の1回で上書きされるか、ちゃんと設定したとしても実行した回数分コメントがされるので、見た目的にもちょっと嫌だ。GitHubとSlackを連携していたりすると大量に通知されることになったりもする。この選択もなし。

選択肢3

linkチェックで生成される結果のファイルを1つのファイルとしてまとめ、1度だけDangerを実行する。これは、マルチモジュールじゃなかった場合のDangerの利用と変わらない結果になるため理想。やりたいことがやれる感じがするので、この選択肢の実現を目指す。

対応

lintの設定

lintチェック結果のXMLファイルが、指定した場所に、指定した名前、で生成されるよう build.gradle で設定しておきます。この設定は、モジュール単位で設定してあげる必要がある ので、androidブロックを共通化させているファイル等で設定してあげると良さそうです。

android {
    lintOptions {
        xmlReport true
        xmlOutput rootProject.file("./build-reports/lint-results-${project.getDisplayName()}.xml")
    }
}

書き出されるファイル名がモジュール毎に変わるようにすることで、結果のファイルが上書きされるのを防いでいます。今回の例でモジュールが :modules:feature:home という階層にあった場合、lint-results-project ':modules:feature:home'.xml という名前でファイルが書き出されます。

この設定で ./gradlew lint 等でlintのGradleタスクを実行してあげると結果のファイルが ./build-reports 配下に生成されます。

lintチェック結果のXMLファイルをマージする

android_lintで結果をプルリクにコメントするために、結果のXMLファイルをマージして1つのXMLファイルを作ります。結果のXMLファイルは以下のような中身になっています。

<?xml version="1.0" encoding="UTF-8"?>
<issues format="5" by="lint 3.4.2">

    <issue
        id="…"
        severity="Warning"
        message="…"
        category="Lint"
        priority="…"
        summary="…"
        explanation="…"></issue>

</issues>

なので全ての <issue><issues> にまとめてあげれば良さそう。ということで以下のようなRubyスクリプトを作成した。

require 'nokogiri'

Dir.chdir('build-reports')

new_doc = nil

Dir::glob('lint-results-*.xml') do |item|
  file = File.new(item)

  if new_doc.nil?
    new_doc = Nokogiri.XML(file)
  else
    doc = Nokogiri.XML(file)
    issues = doc.search('issue')
    new_doc.at('issues').add_child(issues)
  end
end

File.open('lint-results.xml', 'w') do |file|
  unless new_doc.nil?
    file.puts(new_doc.to_xml(indent: 4))
  end
end

1つ目に読み込んだファイルをベースに、他のファイルの <issue><issues> にまとめて、lint-results.xml というファイルで出力します。実行にはnoogiriが必要なので予め gem install nokogiri しておきます。これをプロジェクトのrootディレクトリで実行すると結果が1つにマージされたXMLファイルがbuild-reportsに作成されています。

Dangerでファイルを指定する

あとは Dangerfile で生成されたXMLファイルを指定してあげるだけ。

android_lint.skip_gradle_task = true
android_lint.filtering = false
android_lint.report_file = "build-reports/lint-results.xml"
android_lint.lint(inline_mode: true)

ここまでの一連の流れをCIで実行してあげる。

Bitriseのワークフローを設定する

Bitriseでは以下の流れのワークフローを作成します。

  1. Gradle Taskのlintを実行し
  2. XMLをマージするRubyスクリプトを実行
  3. Dangerを実行するRubyスクリプトを実行

f:id:m4kvn:20190728020543p:plain

ちなみに、Rubyスクリプトを実行するためには bitrise-steplib/steps-ruby-script を選択しています。理由はGemfileのサポートがあり、Bitriseで直接Rubyをかけるからです。Bitriseの設定を変えてビルドを実行するために毎回コミットをするのがつらくて一旦こういう形にしています。安定したらスクリプトファイルをGitの管理化に配置する予定です。Rubyスクリプトの設定は以下のようになっています。

f:id:m4kvn:20190728020956p:plain

f:id:m4kvn:20190728020808p:plain

また、忘れずに DANGER_GITHUB_API_TOKEN をSecretsに設定しておきます。

f:id:m4kvn:20190728023116p:plain

あとはTriggersのPULL REQUESTに作成したワークフローを設定しておきます。

danger-android_lintの実行結果

プルリクエスト時にBitrise上でDangerが実行されると以下のようにlintチェック結果をコメントしてくれます。

f:id:m4kvn:20190728023507p:plain