FreeBSD で Android アプリのビルドをしてみる

# 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 エミュレーションが必要になります。

```console
# pkg install openjdk8 linux_base-c7
# echo linux_enable=YES >>/etc/rc.conf.d/abi
```

## ラッパースクリプト

障害診断の都合で JVM を色々と切り替えたかったり、とある SDK Manager の不都合もあったりするので、軽いお膳立てをできるようなシェルスクリプトを用意します。

```sh:android-studio
#!/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` してしまえばすみます。

```console
# pkg install intellij-fsnotifier
```

あとは、バンドル品の fsnotifier の代わりにこちらを使うように設定すれば OK.

```~/.AndroidStudio3.2/config/studio64.vmoptions
-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 の共有ライブラリをロードするわけにもいかない(実は完全に不可能というわけでもないのだが今回はパス)ので、そこはうまいことやる必要があります。

```console
# 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 で処理してくれることがわかりました。

```properties:~/.gradle/gradle.properties
android.aapt2FromMavenOverride = /usr/local/google/android-sdk/build-tools/28.0.3/aapt2
```

Android gradle plugin は JVM で動作するものなので、 `javap` で逆アセンブルするとか調べようがいくらでもあるのが幸いしました。

### Android resource linking failed 対策

とあるシンボリックリンクを仕込みます。

```console
# ln -s /home /compat/linux/
```

:thinking:
まるで意味がわからないと思います。なぜこれが必要なのか、どうやって突き止めたかは後述します。

## Android Studio から gradle を起動するときに使われる JVM

Android Studio から gradle を起動しようとすると、デフォルトでは Android Studio にバンドルされた Linux 用の JVM が使われます。
残念なことに、これが `/compat/linux/home` → `/home` なシンボリックリンクと相性が悪いことがわかっているので、バンドル品でなくネイティブの JVM を使うように設定します。

![スクリーンショット_2018-12-08_13-52-30.png](https://qiita-image-store.s3.amazonaws.com/0/314402/c834231d-0765-a9a1-4a9b-3fc2b5aa679f.png)

Project Structure から…

![スクリーンショット_2018-12-08_14-08-17.png](https://qiita-image-store.s3.amazonaws.com/0/314402/caf53aed-bcbe-712d-aad1-3eb2d69c276e.png)

SDK Location で、 Use embedded JDK を解除し、直下のテキストボックスに `/usr/local/openjdk8` を入れます。

# 強敵 aapt2 について
## バカな、 failed to create directory だと?

aapt2 を Google maven repository から取ってこないようにしさえすればうまくいく。そう思っていた時期が私にはありました。

```console
$ ./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'.
```

:thinking: 意味不明な理由で失敗。
作れなかったディレクトリをあらかじめ mkdir しておいても、全く同じエラーになってしまいました。

## 切り口はシステムコール; ソースのないバイナリの挙動を追ってみる

本来なら失敗するはずのないような処理で失敗しているわけで、そうなるとどういう問題があることになっていてうまく行かないのかを突き止めないと先に進めません。

この手の、ビルドツールのように一瞬のうちに起動されて終了するようなプログラムは、デバッガーでの追跡がやりづらいので、今回はプロセスの発行するシステムコールを逐一記録することのできるツールを使って追いかけてみましょう。

まずは、先ほど紹介した gradle プロパティ `android.aapt2FromMavenOverride` を利用して、 aapt2 の起動に介在してうまいことシステムコールの記録を取れるようにします。

```sh:truss/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 でビルドを仕掛けてから失敗するまでをぼーっと眺めてから、システムコールの記録を追ってエラーっぽいものを探します。

```console:aapt2-mkdir-EPERM.truss
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` をとりあえず掘っておくという作戦。

```console
# mkdir /compat/linux/home
```

```console:aapt2-mkdir-EEXIST-EPERM.truss
linux_mkdir("/home",488)                         ERR#-17 'File exists'
linux_mkdir("/home/taku",488)                    ERR#-13 'Permission denied'
```

oh...
これでは、 `/compat/linux` 側に同じディレクトリ階層を掘らないといけないことになるので、解決策とは到底言えないでしょう。

### 作戦その2: 困った時のシンボリックリンク

なら、シンボリックリンクなら?

```console
# ln -s /home /compat/linux/
$ ./gradlew app:assembleDebug

...

BUILD SUCCESSFUL in 13s
28 actionable tasks: 16 executed, 12 up-to-date
```

ようやくビルドに成功しました。
この時のシステムコールのトレースは以下のような感じでした。

```console:aapt2-mkdir-OK.truss
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'
```