NIKKEI TECHNOLOGY AND CAREER

Bitrise CIを用いたCIキャッシング・並行実行テクニック

この記事はNikkei Advent Calendar 2021の3日目の記事です。

iOSチームの髙木です。先月紙面ビューアーに書き込みメモ機能を実装しました。みなさんぜひ利用してください!

本記事では、日経電子版紙面ビューアーNikkei Waveといった日経電子版の主要なアプリ群に導入していった開発環境の改善及び、それらを利用したBitrise CIの効率的なキャッシング・並行処理を紹介します。

開発環境改善

Mint

Mintというパッケージマネージャを導入しました。 MintはSwift Package Manager で動くSwift製のCLIツールを管理することが可能です。 具体的には以下のツールを実際に管理しています。

Mintで管理すると以下のような利点があります。

  • バージョン単位で管理できる
  • バイナリが存在しない場合には、swift buildしてバイナリを用意し、それ以降はキャッシュして再利用する
  • プロジェクトをまたいで同一パッケージのキャッシュも共有できる
  • 依存先はXcodeのみ、開発環境に振り回されるリスクを減らせる

特に最後の項は電子版サービスは開発だけでなくQA・テストなど様々なチームが利用するため、安定稼働しつつ不要な再インストールを避けることができる点で非常に良かったです。

XcodeGen

iOSアプリ開発をする上で避けては通れないproject.pbxprojファイルの管理の手間を減らすために、定義ファイル(YAML)からproject.pbxprojを自動生成するXcodeGenを3つのアプリ全てに導入しました。 これは、project.pbxprojファイルを管理する上で避けて通れない以下の手間を削減します。

  • 数万行の構造データ形式のファイル(project.pbxproj)から、数百行の定義ファイル(YAML)を管理するだけで良くなる
  • 変更波及箇所が明瞭で、Conflictが起きない。起きても容易に解決できる
  • 複数のTargetに渡って行う共通処理をTemplate化することで、重複コードを無くせる
  • 意図的に設定しているBuildSettingsの可視化・コメントも可能

特に日経電子版は、様々な施策を元に複数のバージョンを考慮した並行開発が盛んに行われるため、頻繁にproject.pbxprojのConflict、そして解消方法がわからないDiff.......に悩まされていました。 現在は頻繁に複数バージョンの並行開発を行うことが進めやすくなりました。

また、紙面ビューアー・WaveにおいてはライブラリをCarthageからSPMに完全移行することができました。 その結果、現在は

RepositoryをClone → XcodeGen → Build

で開発が可能な環境となっています。ラクチン!

多くの改善ができる上で、1つ問題点を上げるとすればXcodeの最新の機能に追従できていないケースもあることです。 例えば、現状ではXcode 11から導入されたTestPlanには未対応のままとなってしまっています。

Tips

3つのアプリ全てにXcodeGen移行を通して、自分なりのTipsをまとめました。 (3回も移行を行う方が他にもいるかはわかりませんが...) 基本的にBuildを繰り返し行えるように小さい単位で移行することが重要です。

  1. スペックの高い端末を用意する、ホント大事
    • 弊社はエンジニアに最新のMacBook Proのハイスペックモデルなどが貸与される環境だったため、このステップの時間を大幅に減らすことが実際できました。
  2. プロジェクトの規模が最小のものを選択して、大まかな構成・書き方を設計する。共通したBuildScript処理などをまとめる
    • ProjectSpec.mdをひたすら読む。ここが一番苦労します。
  3. 規模が大きいプロジェクトでは以下の優先順位で取り組む
    1. SPMの移行はできるだけ後にする、起動時にライブラリのFetchが毎回発生して時間がかかります!
    2. Carthageを使っている場合など、FrameworkがStatic FrameworkDynamic Frameworkかを整理しておく。Buildができても実行時クラッシュを引き起こしうるため検証・テストを怠らないようにする
    3. Embedded Framework を移行して、Framework単位のBuildが通ることを確認する
    4. Debug/Prodなどの環境ごとの設定をまとめる
    5. App本体を移行して、AppのBuildが通ることを確認する
    6. App Extensionsを移行してBuildが通ることを確認する
    7. Test Targetを移行して、Testが無事に通ることを確認する

CI 環境改善

本編です。 iOSアプリチームは基本的にCIツールにBitriseを利用しています。 Bitriseは動作環境(Stack)を選択する上で、Xcodeの更新が非常に早く、Beta版が導入されたStackも通常数日以内に提供される点で非常に重宝しています。

他にも

  • 特にアプリで利用する汎用処理(署名・アーカイブ・デプロイ・etc...)が公式Stepとして提供されており、非常に簡単に設定できる
  • rbenv / fastlane などの開発支援ツールもプリインストールされている
  • CI設定を行うための優秀なGUI Editorが付属。各Stepのタイミングで利用できる変数がインタラクティブに可視化されることで、環境変数の受け渡しに関する手戻りが少ない。

等様々な良い点があります。 容量が大きいXcodeを事前にプリインストールされているCIサービスの中で更新の早さが唯一無二だと個人的に思っています。

しかし、それらを利用してもiOSアプリのビルドは一般的に非常に時間がかかります。 この項では、そんな長い時間がかかるCIを快適にするためにBitrise上で導入しているいくつかのTipsを紹介します。

キャッシング

定常的に行っていてキャッシュを利用しても弊害が少ない箇所のキャッシュ方法を実際に動作しているスクリプト例と共に紹介します。 以下の項目を対象としています。

  • Homebrew
  • Mint / XcodeGen
  • Ruby Gem
  • Swift Package Manager

Homebrew

Bitriseには公式にHomebrewのStepがあります。 しかしこれは利用しません。 公式のステップでは、あくまでキャッシュを利用しても再インストールを行うステップから繰り返してしまうため、インストールにかかる時間が発生してしまうためです。

代わりにbrew linkコマンドを利用することで、数分かかっていた処理を数秒レベルに短縮することができます。

steps:
- script:
    inputs:
    - content: |-
        #!/usr/bin/env bash
        envman add --key BREW_MINT --value "$(brew --cellar)/mint"
        brew install mint
        brew link mint
    title: install mint

############################

steps:
- cache-push:
    inputs:
    - cache_paths: |-
        $BREW_MINT

Mint / XcodeGen

MintでインストールしたライブラリやXcodeGenが生成したprojectファイルをキャッシュします

steps:
- cache-push:
    inputs:
    - cache_paths: |-
        $MINT_PATH
        $XCODEGEN_CACHE_DIR

############################

app:
  envs:
  - MINT_PATH: /usr/local/lib/mint
  - XCODEGEN_CACHE_DIR: /Users/vagrant/.xcodegen/cache

Ruby Gem (Bundler)

Gemfile.lockの変更を検知してキャッシュすることでBundlerでインストールしたライブラリをキャッシュします

steps:
- cache-push:
    inputs:
    - cache_paths: |-
        $CACHE_BUNDLER -> ./Gemfile.lock

############################

app:
  envs:
  - CACHE_BUNDLER: $BITRISE_SOURCE_DIR/vendor/bundle

Swift Package Manager

Xcode 12.5から、Swift Package ManagerのRepositoryごとにキャッシュが保存できるようになりました! これまでは、ライブラリ全体のビルドキャッシュを保持することしかできなかったため、ライブラリ一つに変更が入るたびにFetchに失敗し、一からやり直さなければいけませんでしたが、12.5以降はSwift Package Managerが格段に安定するようになりました。

steps:
- cache-push:
    inputs:
    - cache_paths: |-
        $SPM_CACHE_DIR

############################

app:
  envs:
  - SPM_CACHE_DIR: /Users/vagrant/Library/Caches/org.swift.swiftpm

並行処理

キャッシュを利用して毎回のビルドの時間を短縮化しました。 しかし、それでもワークフロー全体の処理が長くなってしまうため、いくつか並行処理を行って分散できないか検討してみました。

例えば、デプロイというワークフローを行う上で、以下のような4つのステップを想定してみます。

  1. テスト
  2. 簡易検証用にSimulator向けバイナリを生成してSlackに送信
  3. xcodebuild archiveコマンドを用いて、提出用実機バイナリを生成
  4. Testflightへデプロイ

これらを並行して行ってみます。そうすると、

  1. 並行して行いたい処理
    1. テスト
    2. 簡易検証用にSimulator向けバイナリを生成してSlackに送信
    3. xcodebuild archiveコマンドを用いて、提出用実機バイナリを生成
  2. 1-1 / 1-2 / 1-3 の結果を待ち受けて、成功したら Testflightへデプロイ

このように、最初の3つの処理をCIの別のレーンを利用すれば理想的には作業時間を1/3に減らせそうです。 Bitriseはこのような別のレーンに処理を投げることが簡単に行えるようになっています。

_test_and_build_simulator_async:
  steps:
  - build-router-start:
      inputs:
      - workflows:
        - test
        - build_simulator_app
      - access_token: $BITRISE_ACCESS_TOKEN

test:
  steps:
  - xcode-test: {}

build_simulator_app:
  steps:
  - xcode-build-for-simulator: {}
  - deploy-to-bitrise-io:
      inputs:
      - is_enable_public_page: 'false'
      - is_compress: 'true'

deploy:
  before_run:
  - _test_and_build_simulator_async
  steps:
  - xcode-archive:
      inputs:
      - export_method: app-store
  after_run:
  - _wait_other_workflows # 他のworkflowsを待ち受ける
  - # デプロイする
  - # Slack に通知する

_wait_other_workflows:
  steps:
  - build-router-wait:
      inputs:
      - abort_on_fail: 'yes' # 他のworkflowsがFailしてたらDeployは進めない
      - build_artifacts_save_path: $BITRISE_DEPLOY_DIR/
      - access_token: $BITRISE_ACCESS_TOKEN

このように、公式で提供されているbuild-router-startbuild-router-waitステップと共にBitriseのAPI AccessTokenを利用することで、Workflowを新たに投げて、その結果を待ち受ける処理を容易に記述することができます。 上記スクリプトでは、アーカイブする処理を本流として、「テスト・Simulator用のバイナリアップロード」を別のWorkflowに投げて、両者が成功したときのみTestflightへのUploadを続行するというロジックになっています。

CI上で複雑なことを行おうとすると管理が困難な状態に陥りがちですが、Bitriseでは公式がStepとして提供してくれているものを組み合わせるだけで利用することができ、とても便利です。 契約している価格プランを吟味しつつ、検討してみてください!

並行処理の結果を集約する

1-2. 簡易検証用にSimulator向けバイナリを生成してSlackに送信

Bitriseは別のWorkflowに任せて生成されたArtifactsを共有することができます。 収集したArtifactsをBitriseにdeployした上で、$BITRISE_PERMANENT_DOWNLOAD_URL_MAPを参照することでダウンロード用のLinkを取得することが可能です。

_wait_other_workflows:
  steps:
  - build-router-wait:
      inputs:
      - abort_on_fail: 'yes'
      - build_artifacts_save_path: $BITRISE_DEPLOY_DIR/ # 並行したworkflowのartifactsが保存
      - access_token: $BITRISE_ACCESS_TOKEN
  - deploy-to-bitrise-io:
      inputs:
      - deploy_path: $BITRISE_DEPLOY_DIR/deploy.zip # 上げたいファイルを指定
      - is_enable_public_page: 'false'
  - script:
      inputs:
      - content: >-
          # `key(fileName) => value(url)` で格納されている
          APP_DOWNLOAD_URL=$(echo $BITRISE_PERMANENT_DOWNLOAD_URL_MAP | cut -f 2 -d ">")
          envman add --key APP_DOWNLOAD_URL --value "$APP_DOWNLOAD_URL"
  - slack:
      inputs:
      - buttons: | # slackにLinkボタンを追加
          Simulator App|${APP_DOWNLOAD_URL}

上記スクリプトでは、SlackのNotificationにSimulator用バイナリをDLできるボタンを用意することで、開発チームが簡易的にそのバージョンの動作確認を行うバイナリ配布を実現しました。便利ですね!

Simulator-Link-Slack

終わりに

iOSアプリチームではより良い開発体験へと刷新していくために開発環境の改善を行っています。 本記事ではその一端として、管理しやすいプロジェクト構成やCI環境の整備例を紹介しました。

明日は4日目、伊藤さんによる「日経電子版アクセシビリティの取り組み」です。お楽しみに!

髙木豪
ENGINEER髙木豪

Entry

各種エントリーはこちらから

キャリア採用
Entry
新卒採用
Entry
カジュアル面談
Entry