Jenkins Pipeline plugin の multibranch プロジェクトで Android アプリのビルドに失敗する問題

## まとめ

* Android アプリのビルドプロセス中の `processDebugManifest` タスクの中で warning などが出力されるとき、フォーマット文字列 (多分 `String.format` の第 1 引数に渡されるやつ) の中に生のファイルパスが含まれるっぽい。 (バグっぽい)
    * すなわち、ファイルパスの中に 「%」 が含まれていると、書式指示子として解釈されうる。
    * 書式指示子として不正なものが入っていると、そこで `UnknownFormatConversionException` 例外が発生してビルドに失敗する。
* [Jenkins Pipeline plugin](https://wiki.jenkins-ci.org/display/JENKINS/Pipeline+Plugin) (旧称 Workflow plugin) の multibranch プロジェクトはブランチ名を基にディレクトリを作るが、ブランチ名にスラッシュが入っているとスラッシュは 「%2F」 に変換される。
* というわけで上の 2 つが重なるとめでたくビルドできなくなる。 (つらい。)

## 背景

* Android アプリを Jenkins でビルドしてテストを実行している。
* 最近 Jenkins の Pipeline plugin の multibranch プロジェクトに移行した。

## 発生する問題

手元では問題なくビルドできるのに、なぜか Jenkins 上でビルド (`./gradlew assemble`) するときに processDebugManifest でコケることがあった。 (もともとうまく動いていたブランチから新たにブランチを切ってテストを実行するとコケて、そのブランチではずっとこけ続ける、みたいな感じ。)

```
$ ./gradlew --stacktrace :app:processDebugManifest
:app:preBuild UP-TO-DATE
:app:preDebugBuild UP-TO-DATE
:app:checkDebugManifest
:app:preReleaseBuild UP-TO-DATE
:app:prepareComAndroidDatabindingAdapters10Rc3Library UP-TO-DATE
:app:prepareComAndroidDatabindingLibrary10Rc3Library UP-TO-DATE
:app:preDebugAndroidTestBuild UP-TO-DATE
:app:prepareComAndroidSupportMultidex101Library UP-TO-DATE
:app:prepareComAndroidSupportSupportV42103Library UP-TO-DATE
:app:prepareDebugDependencies
:app:processDebugManifest FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:processDebugManifest'.
> Conversion = 'F'

* Try:
Run with --info or --debug option to get more log output.

* Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':app:processDebugManifest'.
	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:69)
	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:46)
	(略)
	at org.gradle.wrapper.WrapperExecutor.execute(WrapperExecutor.java:129)
	at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)
Caused by: java.util.UnknownFormatConversionException: Conversion = 'F'
	at com.android.utils.StdLogger.warning(StdLogger.java:116)
	at com.android.manifmerger.MergingReport.log(MergingReport.java:68)
	at com.android.manifmerger.ManifestMerger2.merge(ManifestMerger2.java:300)
	at com.android.manifmerger.ManifestMerger2.access$1100(ManifestMerger2.java:59)
	at com.android.manifmerger.ManifestMerger2$Invoker.merge(ManifestMerger2.java:1095)
	at com.android.builder.core.AndroidBuilder.mergeManifests(AndroidBuilder.java:614)
	at com.android.build.gradle.tasks.MergeManifests.doFullTaskAction(MergeManifests.java:63)
	at com.android.build.gradle.internal.tasks.IncrementalTask.taskAction(IncrementalTask.java:98)
	at org.gradle.internal.reflect.JavaMethod.invoke(JavaMethod.java:75)
	at org.gradle.api.internal.project.taskfactory.AnnotationProcessingTaskFactory$IncrementalTaskAction.doExecute(AnnotationProcessingTaskFactory.java:244)
	at org.gradle.api.internal.project.taskfactory.AnnotationProcessingTaskFactory$StandardTaskAction.execute(AnnotationProcessingTaskFactory.java:220)
	at org.gradle.api.internal.project.taskfactory.AnnotationProcessingTaskFactory$IncrementalTaskAction.execute(AnnotationProcessingTaskFactory.java:231)
	at org.gradle.api.internal.project.taskfactory.AnnotationProcessingTaskFactory$StandardTaskAction.execute(AnnotationProcessingTaskFactory.java:209)
	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeAction(ExecuteActionsTaskExecuter.java:80)
	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:61)
	... 60 more


BUILD FAILED
```

### 環境

環境はこんな感じ。

* Java 実行環境 : Java SE Runtime Environment (build 1.8.0_77-b03)
* Android SDK Tools : 24.4.1
* Android SDK Platform-tools : 23.1
* Android SDK Build-tools : 23.0.2

### ちゃんとビルドできるときの出力

手元で動かしたとき (ちゃんとビルド実行できたとき) は以下のような出力になる。

```
./gradlew :app:processDebugManifest
:app:preBuild UP-TO-DATE
:app:preDebugBuild UP-TO-DATE
:app:checkDebugManifest
:app:preReleaseBuild UP-TO-DATE
:app:prepareComAndroidDatabindingAdapters10Rc3Library UP-TO-DATE
:app:prepareComAndroidDatabindingLibrary10Rc3Library UP-TO-DATE
:app:preDebugAndroidTestBuild UP-TO-DATE
:app:prepareComAndroidSupportMultidex101Library UP-TO-DATE
:app:prepareComAndroidSupportSupportV42103Library UP-TO-DATE
:app:prepareDebugDependencies
:app:processDebugManifest
Warning: (略)\src\main\AndroidManifest.xml:2:1-20:12 Warning:
        manifest@tools:versionCode was tagged at AndroidManifest.xml:2 to replace other declarations but no other declaration present
Warning: (略)\src\main\AndroidManifest.xml:2:1-20:12 Warning:
        manifest@tools:versionName was tagged at AndroidManifest.xml:2 to replace other declarations but no other declaration present
(略)\src\main\AndroidManifest.xml:2:1-20:12 Warning:
        manifest@tools:versionCode was tagged at AndroidManifest.xml:2 to replace other declarations but no other declaration present
(略)\src\main\AndroidManifest.xml:2:1-20:12 Warning:
        manifest@tools:versionName was tagged at AndroidManifest.xml:2 to replace other declarations but no other declaration present

BUILD SUCCESSFUL
```

## 何がだめなのか

一番上のまとめに書いたとおり。

* Jenkins Pipeline plugin の multibranch プロジェクトを使っていて、Git のブランチ名に 「/」 が入っていると、「%2F」 を含むディレクトリが作成されて、そこでジョブの処理が行われる。
* Android アプリのビルドプロセス中の `processDebugManifest` タスクの中の何かの処理がログを出力するとき、AndroidManifest.xml ファイルのパスをログに含めるが、そのパスはエスケープされずにフォーマット文字列の中に含められるぽい。
* なので、フォーマット文字列の中にパスの 「%2F」 が含められて、それが書式指示子として解釈される。
* `F` という指示子は存在しないのでエラーが発生してビルドにこける。

### 関係しそうな場所のソースコード

* [com.android.manifmerger.ManifestMerger2](https://android.googlesource.com/platform/tools/base/+/master/build-system/manifest-merger/src/main/java/com/android/manifmerger/ManifestMerger2.java)
* [com.android.manifmerger.MergingReport](https://android.googlesource.com/platform/tools/base/+/master/build-system/manifest-merger/src/main/java/com/android/manifmerger/MergingReport.java)
* [com.android.utils.StdLogger](https://android.googlesource.com/platform/tools/base/+/master/common/src/main/java/com/android/utils/StdLogger.java)

## 対応

Android SDK のビルドツールか何かのバグだと思うので、もうちょっと調べてバグ報告する。 直せるなら直したい。

あとはとりあえずの対処としては以下のようなことをすればよい。

* とりあえずは AndroidManifest.xml の処理で警告が発生しないようにすればよい。
* もしくはブランチ名にスラッシュ (/) を含めないようにすればよい。

## ところで Pipeline plugin の multibranch プロジェクトが便利

上の方にも書きましたが、最近 Jenkins Pipeline plugin の multibranch プロジェクトに移行しました。 これまでは 1 つの Jenkins プロジェクトで全てのブランチを見るというような感じになっていたのですが、それだとブランチごとの過去の状況などを見ることが難しいなどの問題があったのでした。

Pipeline plugin の multibranch プロジェクトにすると、Git のリポジトリを指定するだけで以下のような感じでブランチごとにサブプロジェクト (?) が作られるので、めっちゃ便利です。

![Pipeline plugin の multibranch プロジェクトの例](https://qiita-image-store.s3.amazonaws.com/0/5721/ca171c4e-f30c-d9e9-c873-773071e0c1a9.png)

ビルド処理自体は、Git で管理されるプロジェクトに Jenkinsfile という名前のファイルを置いて、そこに記述します。 Pipeline plugin の multibranch プロジェクトは、定期的に Git リポジトリ (Git 以外の対応もあります) を見て、Jenkinsfile の存在するブランチがあればそれに対応するサブプロジェクトを作ってくれます。

サブプロジェクトが作成された後、Git リポジトリのブランチを消すと Jenkins の multibranch プロジェクトの方のサブプロジェクトも自動的に削除されます。

あんまり紹介されてないけどめっちゃ便利なので、Pipeline plugin を導入しているところはぜひ使っていきましょう！！