CircleCI 2.1 と fastlane で Flutter の iOS と Android をビルドして DeployGate で配布する

# 3行で

- 会社のCI環境をCircleCIで統一しているため、BitriseやCodemagicではなくCircleCIでやりたかった
- AndroidはDockerイメージで楽に環境構築できるが、iOSは色々対応が必要です
- AndroidとiOSでビルドする手順に違いはあるが、CircleCI2.1の機能でうまく対応できました :tada: 

# config.yml と Fastfile の完成形

　今回は、リリースビルドで開発環境向け（Release-Development）のアプリを配布する処理を例にして書きます。Release-Developmentについては、 @mono0926さんの[Flutterで環境ごとにビルド設定を切り替える — iOS編
](https://medium.com/flutter-jp/flavor-b952f2d05b5d) を参考にしてください。

　少々長いのでGistにしました。これらのファイルを定義することで、タイトルどおりのCI環境が構築できるはずです。

## ~/.circleci/config.yml

- https://gist.github.com/masashi-sutou/2d2cf584214f5b8d740d0dcc3cdd27c5
- circleciの設定ファイルです


## ~/Android/fastlane/Fastfile

- https://gist.github.com/masashi-sutou/c0f3d03ce8587d65c97a318de5161249
- Androidをビルドするために必要な処理が書かれたFastfileです


## ~/ios/fastlane/Fastfile

- https://gist.github.com/masashi-sutou/ae4bf7c83f29e6bb0523960a65fdc74a
- iOSをビルドするために必要な処理が書かれたFastfileです
- fastlane match で証明書を管理する必要があるので `~/ios/fastlane/Matchfile` も必要です。fastlane matchの説明は[公式ドキュメント](https://docs.fastlane.tools/actions/match/)を参考にしてください。 

```ruby:~/ios/fastlane/Matchfile
# 証明書を保存するリポジトリを用意してください
git_url("git@github.com:example/example-ios-cert.git")
git_branch("master")

storage_mode("git")

type("development")
```


## github.com/example-fastlane/util/Fastfile

- https://gist.github.com/masashi-sutou/511799849bc7e9f48f1004fa5dc5cdab

　AndroidとiOSの共通変数や共通メソッドは、別リポジトリのFastfileをインポートして利用しています。実際のFastfileはもう少し分割していますが、今回は例なので一つにまとめています。

　別リポジトリのFastfileをインポートすることについては、@star__hoshiさんの [Fastfile の import 機能を使い、複数のプロジェクトで共通の Fastfile を使う](https://qiita.com/star__hoshi/items/7b7d367cb0e0cab612cf) を参考にしてください。

　なお、flutter build コマンドはこのFastfileに定義しており、それぞれのFastfileから引数でパラメータを受け取り、ビルドモードとflavorとtargetを指定しています。iOSに限り、 `--no-codesign` にして、後から fastlane match で管理している証明書を差し込めるようにしています。

　ローカルとCIの両方の環境で実行できるように `is_ci?` で環境を判定しています。CI環境の場合に `$FLUTTER_HOME/bin` にしているのは、[Androidで利用するDockerイメージのFlutterSDKのPATHが$FLUTTER_HOME](https://github.com/cirruslabs/docker-images-flutter/blob/master/sdk/Dockerfile#L5) になっているためです。 

```ruby:github.com/example-fastlane/util/Fastfile
# Flutter App をビルドする
private_lane :build_flutter_in_util do |options|

  codesign = options[:platform] == "ios" ? "--no-codesign" : ""

  if is_ci?
    sh("cd ../../ && $FLUTTER_HOME/bin/flutter build #{options[:platform]} --release --flavor #{options[:flavor]} --target #{options[:target]} #{codesign}")
  else
    sh("cd ../../ && flutter build #{options[:platform]} --release --flavor #{options[:flavor]} --target #{options[:target]} #{codesign}")
  end
end
```


# CircleCI 2.1で Flutterのアプリをビルドする

　CircleCI 2.1の機能を有効にするには、CircleCIのプロジェクトの設定画面で **Settings -> Advanced Settings** の **Enable build processing (preview)** を **On** にしてください。

　有効にすると、 `executors`、 `commands`、 `parameters` の機能が利用できます。簡単に説明すると、 `executors` は環境、 `commands` は処理、 `parameters` は変数を任意の組み合わせで定義できる機能です。

　他にもCircleCI 2.0の機能ですが、 `working_directory:` で処理ごとに実行するディレクトリを指定できます。Flutterプロジェクトはプラットフォームごとにスクリプトを実行するディレクトリが違うので、 `working_directory:` はとても便利です。

```yaml:~/.circleci/config.yml
  setup_ios_build_setting:
    steps:
      - run:
         name: Run pod setup from cocoapods-specs.circleci.com
         command: curl https://cocoapods-specs.circleci.com/fetch-cocoapods-repo-from-s3.sh | bash -s cf
         working_directory: ~/flutterApp/ios
```

## Androidをビルドする場合

　Androidのビルドに必要な環境は、 `image: cirrusci/flutter:latest` に用意されています。FlutterとAndroidのSDKがインストールされてPATHも通っているので、CircleCI側ではクローンして、 `flutter doctor` と `flutter analyze` の後にビルドしてDeployGateに配信するだけです。とても楽です。

```yaml:~/.circleci/config.yml
  beta_development_android:
    executor:
      name: default_android
    steps:
      - checkout
      - setup_flutter
      - setup_bundle:
          platform: android
      - flutter_build:
          platform: android
          configuration: development
```


## iOSをビルドする場合

　iOSのビルドには macOSが必要なので、CircleCIはmacOSの環境を選択してください[（有料です）](https://circleci.jp/pricing/#build-os-x)。先ほど、Androidのビルドに利用したDockerイメージは、Linuxなので証明書でビルドが失敗します。したがって、iOSではFlutterのSDKをクローンしてPATHを通す必要があります。

```yaml:~/.circleci/config.yml
  install_flutter:
    steps:
      - run:
          name: Install flutter SDK
          command: mkdir -p ~/sdks/flutter && git clone -b stable https://github.com/flutter/flutter.git ~/sdks/flutter
      - run:
          name: Set flutter SDK PATH in bash
          command: echo 'export FLUTTER_HOME=~/sdks/flutter' >> $BASH_ENV && source $BASH_ENV
```

### CircleCI 2.1の新機能で実行環境を別にして処理を共通化

　AndroidとiOSの実行環境を別に定義したいので、`executors` を使います。 iOSでは、 `executor:` に `default_ios` を指定したので、Dockerイメージではなく、CircleCIのmacOSをつかってビルドします。

　`commands:` の機能で、`flutter doctor` と `flutter analyze`の処理は共通化できますし、 `parameters:` の機能で `flutter_build:` で必要な引数もプラットフォームごとにわけて渡せます。

```yaml:~/.circleci/config.yml
  beta_development_ios:
    executor:
      name: default_ios
    steps:
      - checkout
      - install_flutter
      - setup_flutter
      - setup_bundle:
          platform: ios
      - setup_ios_build_setting
      - flutter_build:
          platform: ios
          configuration: development
```

### disable_automatic_code_signing で証明書を設定

　iOSでは `--no-codesign` でビルドしたので、 fastlaneの `disable_automatic_code_signing` を呼んで手動で証明書を設定します。このような手動の管理が面倒だと思う方は、 [Bitrise](https://devcenter.bitrise.io/getting-started/getting-started-with-flutter-apps/)や[Codemagic](https://blog.codemagic.io/automatic-code-signing-for-ios-that-doesnt-require-a-mac/)を検討すると良いです。

　`options[:app_configs]` が配列なのは、全てのターゲットに一度に証明書を設定可能にするためです。iOSでは複数のターゲットをもつアプリが存在するので、このようにしておくと楽なのですが、今回の例では不要なループ処理です（消すのが面倒でした :bow: ）。

```ruby:github.com/example-fastlane/util/Fastfile

# Code Signing Style を Manual に変更
private_lane :change_manual_code_signing_style_in_util do |options|

  code_sign_identity = ""
  profile_name_prefix = ""

  if options[:mode] == "development"
    code_sign_identity = ENV["CERT_DEVELOPER_ID"]
    profile_name_prefix = "match Development"
  end

  options[:app_configs].each { |app_config|
    disable_automatic_code_signing(
      targets: app_config[:target],
      code_sign_identity: code_sign_identity,
      profile_name: "#{profile_name_prefix} #{app_config[:profile_name_app_id]}"
    )
  }
end
```


# 最後に

　モバイルアプリ開発でCI環境がない場合、アプリの検証サイクルを走らせるのが一気に面倒になります。できるだけ早くCI環境の構築は終わらせておき、開発に集中したいアプリエンジニアは多いと思います。特に私はそうです :smile: 

　Flutterで開発するとき「CI環境どうしよう」と不安になっていた時期もありましたが、今の所は問題なく、ネイティブアプリの開発フローと変わらずに検証と開発ができています。さらに、本番・ステージング・開発など、環境別にアプリを分けて配信して、同じスマートフォンに複数のアプリをインストールさせることも可能です。

　あとは、DartとFlutterのスキルさえ上がれば・・・。Flutter開発がんばろう :muscle: 

# 参考

- https://flutter.dev/docs/deployment/fastlane-cd