正常なAndroidアプリをビルドできない問題とその対策

モバイルファースト室の山下(@tomorrowkey)です。
先日撮るレシピというAndroidアプリをリリースしました。
f:id:tomorrowkey:20141027085113p:plain

みなさんの自宅には開かずにずっとおいてあるレシピ雑誌はないでしょうか。その中でも作ってみたいと思うレシピは何品あるでしょうか。
また母親や友達から教えてもらったレシピを付箋に書いて冷蔵庫に貼っていたりしませんか。冷蔵庫が付箋だらけになっていませんか。
このアプリはそんなレシピたちを写真に撮って残せるアプリです。雑誌や冷蔵庫のドアなどちらばったレシピを1つにまとめることができます!

f:id:tomorrowkey:20141027085224p:plain

そんなとっても便利なアプリなのですが、今回このアプリをリリースするときにGoogle Playからインストールできなくなるという現象に遭遇しました。
同じ轍を踏む人がでてこないように、その原因と対策を紹介します。

ビルド環境

この問題が発生したのは以下の環境です。
例えばantなどの他のビルド環境では検証していません。

./gradlew --version

------------------------------------------------------------
Gradle 2.1
------------------------------------------------------------

Build time:   2014-09-08 10:40:39 UTC
Build number: none
Revision:     e6cf70745ac11fa943e19294d19a2c527a669a53

Groovy:       2.3.6
Ant:          Apache Ant(TM) version 1.9.3 compiled on December 23 2013
JVM:          1.7.0_45 (Oracle Corporation 24.45-b08)
OS:           Mac OS X 10.9.4 x86_64

問題の発覚

リリース日の昼ごろにGoogle PlayのDeveloper Consoleの公開ボタンを押し、夕方には配布開始されました。
以前は公開ボタンを押したらすぐに公開されていましたが、最近は時間がかかるようになりましたね。
早速手元の端末でインストールしようと思ったのですが、インストールボタンが無効になっていてインストールできません。
いろんな端末で試してみたのですが、どれもインストールできませんでした。

Developer ConsoleでAPKの詳細を見ることで、配布するAPKファイルのデータを見ることができます。
正常なAPKファイルはこのようになっています。

f:id:tomorrowkey:20141027085256p:plain

対応するAndroid搭載端末に、対応するAndroid端末の数が表示されます。
このスクリーンショットのアプリは4000種類以上のAndroid端末に対応しています。
問題のあったAPKファイルの詳細を見てみると…

f:id:tomorrowkey:20141027085308p:plain

対応するAndroid搭載端末が0になっていました。つまり、どの端末でもインストールすることはできません。
そしてネイティブプラットフォームという欄が増え、commons-io-2.4.jarと表示されています。

原因

結論から話すと、変な構成のライブラリを参照していたためどんな端末でもインストールできなくなってしまってました。
問題のあったライブラリはこれです。
org.apache.directory.studio:org.apache.commons.io:2.4

通常のライブラリはルートディレクトリからパッケージのディレクトリが切られ、クラスファイルが配置されています。

.
├── META-INF
│   ├── LICENSE.txt
│   ├── MANIFEST.MF
│   ├── NOTICE.txt
│   └── maven
│       └── commons-io
│           └── commons-io
│               ├── pom.properties
│               └── pom.xml
└── org
    └── apache
        └── commons
            └── io
                ├── ByteOrderMark.class
                ├── Charsets.class
                ├── CopyUtils.class
                ...

問題のあったライブラリはルートディレクトリのlibディレクトリにjarファイルが入っていました。

./
├── META-INF
│   ├── DEPENDENCIES
│   ├── LICENSE
│   ├── MANIFEST.MF
│   └── NOTICE
└── lib
    └── commons-io-2.4.jar

この構成がそのままマージされてしまったためlibディレクトリにcommons-io-2.4.jarが入り込んでしまったようです。

apkファイルのlibディレクトリ直下にはサポートするプラットフォームの名前でディレクトリが作られます。
ネイティブコードを含む正常なapkファイルをunzipすると以下のようなディレクトリ構成になっています。

./
├── AndroidManifest.xml
├── META-INF
│   └── ...
├── assets
│   └── ...
├── classes.dex
├── lib
│   ├── armeabi
│   │   └── ...
│   ├── armeabi-v7a
│   │   └── ...
│   └── x86
│       └── ...
├── manifest
└── res
    └── ...

armやx86などに対応していると解釈されます。

問題のあったapkファイルをunzipすると以下のようなディレクトリ構成になっていました。

./
├── AndroidManifest.xml
├── META-INF
│   └── ...
├── assets
│   └── ...
├── classes.dex
├── lib
│   └── commons-io-2.4.jar
├── manifest
└── res
    └── ...

問題のあるライブラリを参照していたためlibディレクトリにcommons-io-2.4.jarが入り込んでいました。
このようなapkファイルをリリースしようとすると対応するネイティブプラットフォームがcommons-io-2.4.jarになってしまいます。

対策

リリースする前に対応するAndroid端末を見ればリリースに失敗することはありませんが、使っているライブラリが実は使えないものだとリリースする直前に発覚しても遅いです。
なんとか開発中に気づけないものかと仕組みを考えました。

CookpadではCIツールにJenkinsを使っています。
開発中に何度かJenkinsでビルドをするので、ここで意図しないファイルが含まれていないかチェックすることができそうです。

if [ `unzip -l ./app/build/outputs/apk/app-debug.apk | grep -e "lib/" | grep -v "lib/armeabi" | grep -v "lib/x86" | wc -l` -eq 1 ]; then
    echo "The apk may include invalid library"
    exit 1
fi

完成したapkファイルをunzipし、libディレクトリ配下にarmもしくはx86以外のファイルまたはディレクトリがないかチェックしています。
もし不正なライブラリが入っていると判断された場合はビルドが中断します。

このような事態を防ぐためには「リリースする前に表示を確認する」ということで防ぐことはできそうですが、作業するのはやはり我々人間なのでついつい忘れがちですし注意しないといけないことが増えてくるとだんだんストレスになってきます。
このスクリプトを使うことによりリリース失敗するリスクが減りました。

まとめ

CIにスクリプトを追加することによって、不正なapkファイルの検出を自動化することができるようになりました。
この不具合にはいくつかパターンがあるらしく、不正なライブラリを参照する以外にも再現する可能性があるので、ぜひビルドスクリプトに組み込んでみてはいかがでしょうか。

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/