TL;DR
- Android Studio やら Android SDK やらを FreeBSD で動かしてみた
- 案の定色々とハマる
- 手探りでうまいこと回避 (つまり、やればできる!)
- 中で何やってるかわかったもんじゃない野良バイナリの挙動を見なくちゃいけなくなったらシステムコールトレース (FreeBSD では
truss
(1)) - 誰得感ありすぎるが気にしてはいけない
前置き
弊社エンジニアのコンソールマシンは macOS か Linux か、といった感じなのですが、私の場合帰宅すると FreeBSD 一色なので、趣味の1つの Android アプリ作りも同じ環境でやりたいと思うわけです。
加えて私、ネイティブ動作至上者だったりするので、可能な限り Linux エミュレーションを使わずに済ませたいとも思うわけで、結果こんな目標にしてみました。
目標
- Android Studio 上からと、コマンドラインから直接 gradle を叩く両方のやり方で、 Android アプリのビルドができるようにする
- Android emulator は、 Linux 版バイナリが KVM 必須となっているのでスコープ外とする
- Android Studio と、 Android SDK のうち JVM ベースのプログラムについては、ネイティブの JDK で動作させる
How-to
JVM, Linux エミュレーション関係等
Android SDK には Linux バイナリのプログラムが含まれているので、どうしても Linux エミュレーションが必要になります。
# pkg install openjdk8 linux_base-c7
# echo linux_enable=YES >>/etc/rc.conf.d/abi
ラッパースクリプト
障害診断の都合で JVM を色々と切り替えたかったり、とある SDK Manager の不都合もあったりするので、軽いお膳立てをできるようなシェルスクリプトを用意します。
#!/bin/sh -
PREFIX=/usr/local/google/android-studio
#export STUDIO_JDK=/usr/local/linux-oracle-jdk1.8.0
export JDK_HOME=/usr/local/openjdk8
export ANDROID_HOME=/usr/local/google/android-sdk
case "${STUDIO_JDK-$JDK_HOME}" in
*linux*) BASH=/compat/linux/bin/bash ;;
*) BASH=/usr/local/bin/bash
export REPO_OS_OVERRIDE=linux ;;
esac
exec $BASH - "$PREFIX/bin/studio.sh" "$@"
ここで、REPO_OS_OVERRIDE
について。
ネイティブ版の JDK を使って Android Studio を起動するようにした場合、 Android Studio は当然 FreeBSD 上で動作していると認識することになるわけですが、いざ JVM ではないバイナリを含んだツール類を SDK Manager でダウンロードしようとするとなると困ってしまうわけです。
REPO_OS_OVERRIDE
環境変数を適切に設定することで、 JVM の報告してくる OS とは違う OS 向けのバイナリをダウンロードしてくれるようになりました。
fsnotifier
Android Studio は、編集中のファイルを含めてプロジェクトに関連するファイルが外部によって変更されたら検知する能力を持っています。
この、ファイル変更を検知する役割を担った小さなデーモンが Android Studio に付属していて、 Android Studio を起動すると自動でそいつも動き出すのですが、これが FreeBSD の Linux エミュレーションではうまく動かないという問題があります。
幸い、 fsnotifier は FreeBSD に移植されていて ports 化もされているので、 pkg install
してしまえばすみます。
# pkg install intellij-fsnotifier
あとは、バンドル品の fsnotifier の代わりにこちらを使うように設定すれば OK.
-Didea.filewatcher.executable.path=/usr/local/intellij/bin/fsnotifier
注意点としては、この fsnotifier は信じられないくらい大量の vnode を消費するので、 maxvnodes (sysctl kern.maxvnodes
で読み書き可)が 100000 に満たないような環境だとスラッシングに似たような状態に陥ります。
(私が X1 Carbon のメモリ16GB品を購入することになった原因の1つです)
pty4j
Android Studio の Terminal タブなんて使わないよという方は読み飛ばしてもかまいません。
JVM で端末エミュレータ的なことをやりたい場合、どうしても pty(4) の制御は避けて通れないのですが、このライブラリはそれを JVM から呼べる native メソッドとして提供してくれているもののようです。
当然、 FreeBSD ネイティブ版の JVM に Linux の共有ライブラリをロードするわけにもいかない(実は完全に不可能というわけでもないのだが今回はパス)ので、そこはうまいことやる必要があります。
# pkg install intellij-pty4j
$ ln -s /usr/local/intellij/lib/libpty/freebsd /usr/local/google/android-studio/lib/libpty/
aapt2
今回最も苦戦したコンポーネント。
そもそもネイティブバイナリを maven から取ってこようとすんな問題
Android アプリをビルドする際、画像などのリソースを処理したり ID を振ったりと色々な処理をする、 aapt (もしくは aapt2)というプログラムが走ります。
と、ここまではいいのですが、 Android gradle plugin のバージョン 3.2 以降は、 aapt2 のバイナリを Google の maven repository からダウンロードしてくる仕様になってしまいました。
勘の良い方なら気付かれたかもしれませんが、 REPO_OS_OVERRIDE と似たような(しかもよりタチの悪い)問題で、 aapt2 がダウンロードできずアプリのビルドに失敗します。
色々調べ回してみた結果、こんな gradle のプロパティーを仕込んでおくと固定の aapt2 で処理してくれることがわかりました。
android.aapt2FromMavenOverride = /usr/local/google/android-sdk/build-tools/28.0.3/aapt2
Android gradle plugin は JVM で動作するものなので、 javap
で逆アセンブルするとか調べようがいくらでもあるのが幸いしました。
Android resource linking failed 対策
とあるシンボリックリンクを仕込みます。
# ln -s /home /compat/linux/
まるで意味がわからないと思います。なぜこれが必要なのか、どうやって突き止めたかは後述します。
Android Studio から gradle を起動するときに使われる JVM
Android Studio から gradle を起動しようとすると、デフォルトでは Android Studio にバンドルされた Linux 用の JVM が使われます。
残念なことに、これが /compat/linux/home
→ /home
なシンボリックリンクと相性が悪いことがわかっているので、バンドル品でなくネイティブの JVM を使うように設定します。
Project Structure から…
SDK Location で、 Use embedded JDK を解除し、直下のテキストボックスに /usr/local/openjdk8
を入れます。
強敵 aapt2 について
バカな、 failed to create directory だと?
aapt2 を Google maven repository から取ってこないようにしさえすればうまくいく。そう思っていた時期が私にはありました。
$ ./gradlew app:assembleDebug
...
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':app:processDebugResources'.
> Android resource linking failed
Output: error: failed to create directory '/home/taku/work/git/lists4muzei/app/build/generated/not_namespaced_r_class_sources/debug/processDebugResources/r/net/homeip/tackymt/android/app/lists4muzei'.
意味不明な理由で失敗。
作れなかったディレクトリをあらかじめ mkdir しておいても、全く同じエラーになってしまいました。
切り口はシステムコール; ソースのないバイナリの挙動を追ってみる
本来なら失敗するはずのないような処理で失敗しているわけで、そうなるとどういう問題があることになっていてうまく行かないのかを突き止めないと先に進めません。
この手の、ビルドツールのように一瞬のうちに起動されて終了するようなプログラムは、デバッガーでの追跡がやりづらいので、今回はプロセスの発行するシステムコールを逐一記録することのできるツールを使って追いかけてみましょう。
まずは、先ほど紹介した gradle プロパティ android.aapt2FromMavenOverride
を利用して、 aapt2 の起動に介在してうまいことシステムコールの記録を取れるようにします。
#!/bin/sh -
PREFIX="$(dirname "$0")/.."
exec truss -o /tmp/aapt2.$$.truss "$PREFIX/aapt2" "$@"
truss
(1) コマンドをこのように使うと、呼ばれたシステムコールを /tmp/aapt2.
プロセスID.truss
のような名前のファイルに記録しつつ、ターゲットのプログラム(この場合は aapt2
)を実行できます。
ところで、 Android gradle plugin の仕様なのか、 android.aapt2FromMavenOverride
に指定できるものはファイル名が必ず aapt2
でなければならないようなので、上のラッパースクリプトを準備する際にはファイル名に注意。
いざ鎌倉
普通に gradlew でビルドを仕掛けてから失敗するまでをぼーっと眺めてから、システムコールの記録を追ってエラーっぽいものを探します。
linux_mkdir("/home",488) ERR#-13 'Permission denied'
ファッ?!?
存在するディレクトリを mkdir
(2) しようとすると、本来なら EEXIST
(File exists)エラーになるはずですが、これは…
種明かしをすると、 FreeBSD の Linux エミュレーション機能で動作しているプロセスからは、通常のファイルシステムではなく、あたかも /compat/linux
が /
であるかのようなファイルシステムが見えつつも、通常のファイルシステムにしかないファイルやディレクトリもなぜかアクセスできるような、そんな不思議なファイルシステムが見えるのです。
仮説: /compat/linux/home
の mkdir
(2) が EEXIST
で失敗するような状況にするとうまくいく
作戦その1: とりあえず mkdir
(1) しておいてみる
まず思いつくのが、 /compat/linux/home
をとりあえず掘っておくという作戦。
# mkdir /compat/linux/home
linux_mkdir("/home",488) ERR#-17 'File exists'
linux_mkdir("/home/taku",488) ERR#-13 'Permission denied'
oh...
これでは、 /compat/linux
側に同じディレクトリ階層を掘らないといけないことになるので、解決策とは到底言えないでしょう。
作戦その2: 困った時のシンボリックリンク
なら、シンボリックリンクなら?
# ln -s /home /compat/linux/
$ ./gradlew app:assembleDebug
...
BUILD SUCCESSFUL in 13s
28 actionable tasks: 16 executed, 12 up-to-date
ようやくビルドに成功しました。
この時のシステムコールのトレースは以下のような感じでした。
linux_mkdir("/home",488) ERR#-17 'File exists'
linux_mkdir("/home/taku",488) ERR#-17 'File exists'
linux_mkdir("/home/taku/work",488) ERR#-17 'File exists'
linux_mkdir("/home/taku/work/git",488) ERR#-17 'File exists'
linux_mkdir("/home/taku/work/git/lists4muzei",488) ERR#-17 'File exists'
linux_mkdir("/home/taku/work/git/lists4muzei/app",488) ERR#-17 'File exists'