CircleCIからAutify for Mobile向けにx86_64アーキでビルドする

# about

Autify for MobileにiOSの`.appファイル`をアップロードする際、
アーキテクチャは`x86_64`である必要があるので、その方法を記録しておきます。

ここではFlutterアプリの場合について記載しますが、`xcodebuild`の書き方等iOSネイティブの場合も参考になるかと思います。

Autifyアップロード方法は以下の記事をご覧下さい。

https://qiita.com/kanari3/items/3254e607ba7aad46a868

# 環境

CircleCI Performanceプラン
config.yml ver 2.1
Xcode 12.5.1 / 13.0.0
Flutter 2.2.1

# 確認方法
最初に`.appファイル`が正しいかの確認方法です。
ここでは`.app`は、Flutterでデフォルトの`Runner.app`とします。

```sh
$ xcrun lipo -info Runner.app/Runner

# NG
Architectures in the fat file: Runner.app/Runner are: armv7 arm64

# GOOD
Architectures in the fat file: Runner.app/Runner is architecture: x86_64
```

デフォルトでは`armv7 arm64`になっていると思うので、まずは`x86_64`でビルドすることを目指します。

## ポイント

`flutter build ios`では、Autifyでの実行に必要な`x86_64`アーキテクチャに対応していないようです。
アーキテクチャの指定が可能な`xcodebuild`を使用します。

オプションとして、`"ARCHS=x86_64"`で指定可能です。

# xcodebuild

今回は最低限のオプションとして`xcworkspace`と`Scheme`を指定します。
また、ここでの必須オプション`ARCHS`でアーキテクチャを指定可能です。

```
xcodebuild -workspace "Runner.xcworkspace" -scheme "Runner" "ARCHS=x86_64"
```

AutifyのSimulatorで実行する分には、 `CODE_SIGN_IDENTITY` `CODE_SIGNING_REQUIRED` `CODE_SIGNING_ALLOWED`は不要でした。

# xcodebuildの.appファイルのパス

ビルドが完了すると、以下のように突然`Runner.app`のパスを知らせられます。

```sh
# xcodebuild log on CircleCI
RegisterExecutionPolicyException /Users/distiller/Library/Developer/Xcode/DerivedData/Runner-bagyappopgcanueqkpdklnbfxkyy/Build/Products/Debug-iphonesimulator/Runner.app (in target 'Runner' from project 'Runner')
    cd /Users/distiller/flutter_app/ios
    builtin-RegisterExecutionPolicyException /Users/distiller/Library/Developer/Xcode/DerivedData/Runner-bagyappopgcanueqkpdklnbfxkyy/Build/Products/Debug-iphonesimulator/Runner.app

Touch /Users/distiller/Library/Developer/Xcode/DerivedData/Runner-bagyappopgcanueqkpdklnbfxkyy/Build/Products/Debug-iphonesimulator/Runner.app (in target 'Runner' from project 'Runner')
    cd /Users/distiller/flutter_app/ios
    /usr/bin/touch -c /Users/distiller/Library/Developer/Xcode/DerivedData/Runner-bagyappopgcanueqkpdklnbfxkyy/Build/Products/Debug-iphonesimulator/Runner.app

** BUILD SUCCEEDED **
```

`flutter build`のように`~/flutter_app/build/ios/iphoneos/Runner.app`に書き出して欲しいところですが、
XcodeのProduct/Runner.appもDerivedData内なのでxcodebuildの仕様として仕方ないようです。

オプションとして、`-derivedDataPath`がありますが、`DerivedData/Runner-{ランダム文字列}/Build`となるので、
書き出し先を固定することは難しそうです。

## Runner.appの特定

私は`find . -name 'Runner.app'`で探すことにしました。 (より良い方法があればコメントお願いします)
この結果を`$APP_DIR_PATH`として環境変数に保存し、利用することにしました。
CircleCIで環境変数を保存するには以下のように記述します

```yaml
export APP_DIR_PATH=`find . -name 'Runner.app'` >> $BASH_ENV
```

複数のRunner.appが検出される場合は、firstで良ければ以下のように書けばOKです。
(flutter buildとxcodebuildだとRunner.appの書き出し先が違うので、問題ないと思いますがご参考までに)

```yaml
export APP_DIR_PATH=`find . -name 'Runner.app' -print -quit` >> $BASH_ENV
```

## Runner.appの親ディレクトリ
`dirname $APP_DIR_PATH`でOKです。

## 完成したconfig.yml


```yaml
- run: flutter build ios --build-number $CIRCLE_BUILD_NUM --no-codesign
- run:
    command: xcodebuild -workspace "Runner.xcworkspace" -scheme "Runner" "ARCHS=x86_64"
    working_directory: ~/flutter_app/ios
- run:
    command: |
      set -ex
      export APP_DIR_PATH=`find . -name 'Runner.app'` >> $BASH_ENV
      cd `dirname $APP_DIR_PATH`
      # 以下Autifyへのアップロードは ※1参考
      zip -r Runner Runner.app
      >-
        curl -X POST "https://mobile-app.autify.com/api/v1/projects/${AUTIFY_PROJECT_ID}/builds" \
          -H "accept: application/json" \
          -H "Authorization: Bearer ${AUTIFY_ACCESS_TOKEN}" \
          -H "Content-Type: multipart/form-data" \
          -F "file=@Runner.zip;type=application/zip"
    working_directory: ~/Library/Developer/Xcode/DerivedData
```

`flutter build`には必要に応じて、以下オプションを追加してください。

- Entry point: `-t lib/main_{ENV}.dart`
- Flavor: `--dart-define=FLAVOR={FLAVOR}`

xcodebuildで生成された`Runner.app`を見つけたいので` ~/Library/Developer/Xcode/DerivedData`をワーキングディレクトリとしました。



## ポイント

予め、ローカル環境で`xcodebuild`でビルドした`Runner.app`を、Simulatorにドラッグして起動するか確認しておきましょう。
私の場合は、以下のパターンで躓きました。

- インストールできない
シミュレータで対応していないアーキテクチャでビルドするとインストールで弾かれることがありました。

- 起動しない
起動してもすぐ終了してしまうことがありました。

- 想定画面に遷移しない
スプラッシュスクリーンのまま次の画面に遷移しないことがありました。
古いFlutterプロジェクトの場合はEntry pointで環境を設定している場合もあるので、正しく想定の画面が表示されるか見ておく必要があります。


# 高速化

Flutterプロジェクトの場合、
一般的には`flutter build`のあとに、`xcodebuild`もしくは、fastlaneで`gym()`/`build_app()`を実行している場合が多いかと思います。

どちらにせよ、内部的には`flutter build`のあとに`xcodebuild`を実行しているので、2回ビルドしていることになります。
(どちらも`.appファイル`が生成されますが、できることやオプションが違います)

片方にできれば、ビルド時間の短縮になるので検証してみます。

## Before

まずは、現状を測定したいので、`flutter build`のあとに、`xcodebuild`を実行した場合の結果を示します。

```
ビルド時間 (flutter build & xcodebuild)

10m 18s flutter build ios
2m 49s xcodebuild
- - - - - - - - - - - - - - - 
13m 7s total
```

## 改善策

前述の通り、`flutter build ios`では、`x86_64`に対応していないので`xcodebuild`は外せないことになります。
`flutter build`をせず、`xcodebuild`だけでできれば時間の短縮になると考え、試してみます。

## After

`flutter build`を省くと、`pod install`されないので、別途実行する必要があります。
それに付随して`flutter precache -ios`を事前に実行します。

その結果が以下になります。Beforeと同条件で比較したかったので並列実行しました。

```
ビルド時間 (xcodebuildのみ)

0m 25s flutter precache -ios
2m 19s pod install
2m 53s xcodebuild
- - - - - - - - - - - - - - - 
5m 37s total
```

7m 30s短縮できました！
そのときのconfig.ymlは以下になります。

```yaml
- run:
    command: |
      flutter precache --ios
      pod install
      xcodebuild -workspace "Runner.xcworkspace" -scheme "Runner" "ARCHS=x86_64"
    working_directory: ~/flutter_app/ios
```

Jobが通ると、Autifyに`Runner.app`がアップロードされているので、
新規シナリオをビルドファイルから作成して動かしてみましょう。

## ポイント
`flutter build`でビルドすると、その後`xcodebuild`したときにEntry pointが設定されているようです。
Entry pointで環境を指定している場合は、書き換えるステップを挿入する、デフォルトEnvを持つなど、対応すべき点はあるかも知れません。

<details><summary>ちょっとした調整の一例 (本題から外れるので折りたたみました)</summary><div>

----
問題解決の糸口になればと思い例を記載します。

Entry pointの編集
デフォルト環境を設定しようかとも思いましたが、一長一短なので、ワンライナーで書き換えました。(それもどうなんだ)
不安ならユニークコメントを付与したり、該当行全てを表現すれば良いかと思います。

```perl
# perl -i -ple 's/{before}/{after}/g;' {target_file}
perl -i -ple 's/Environment\? environment/Environment\? environment = Environment.production/g;' lib/main.dart
```

BundleIDを調整したい場合

```sh
plutil -replace CFBundleIdentifier -string $BUNDLE_IDENTIFIER ios/Runner/Info.plist
```

flutter buildせずxcodebuildするとビルド番号が入らないことへの対応

```sh
plutil -replace CFBundleVersion $CIRCLE_BUILD_NUM ios/Runner/Info.plist
```

その他、xcconfigを差し替えて環境変数を設定するなど、変にこだわらず、柔軟に対応するのが良いと思います。
Autifyのワークフロー内に収めておけば、プロダクトコードに影響を与えずに済みます。

----
</div></details>


# 苦労した点

- Runner.appの起動
最初に`x86_64`アーキでビルドするところからはじめたので、アップロードしたファイルは実はAutifyで起動できませんでした。
その後、ローカルのSimulatorで試しても起動しなかったので`.appファイル`に問題があることが判明。
極力ローカルで試す方向から攻めるのがオススメです。

- DerivedDataの書き出し先がランダム名
実際は毎回同じランダム風の名前にも見えますが、シェル芸で対応するしかなかった。

- flutter build
`flutter build ios`は`x86_64`に対応していないのに、Xcodeの設定で`EXCLUDED_ARCHS`を設定して反映されない...?と思っていた。


# 参考

- How to compile x86_64 with flutter build ios-framework?
https://github.com/flutter/flutter/issues/66943

- xcodebuildのアーキテクチャ設定
https://qiita.com/Arime/items/ee9a41d849b473181728

- DerivedDataの出力先設定
https://stackoverflow.com/questions/33044633/command-line-option-to-change-xcode-deriveddata-location
special thanks: @tarappo

- ※1 CircleCIからAutify for Mobileへアップロードする
https://qiita.com/kanari3/items/3254e607ba7aad46a868