iOS 開発の頑張らないビルド用 Swift CLI ツール管理

## TL;DR

どうせ macOS 上でしか開発できないので、 直接ビルドツールの実行バイナリをダウンロードして利用する。

## モチベーション

- XcodeGen
- SwiftLint
- LicensePlist

といった最終成果物には含まれないが、ビルドプロセスで利用する Swift 製 CLI ツールの管理について、 [Mint](https://github.com/yonaskolb/Mint)  を利用してプロジェクトでバージョンを固定＋開発者のローカルマシンに依存させないといった設定をよく実施するかと思います。

https://qiita.com/usamik26/items/be813a224a2146daffb7

https://dev.classmethod.jp/articles/package_management_by_mint/

これまでは特に問題がなかったのですが、 Apple Silicon ( M1 Mac ) の登場によって、[Homebrew 経由でインストールしたものを Build Phases で実行する際に PATH を通す必要があった](https://stackoverflow.com/questions/65991508/swiftlint-cant-be-found-on-apple-silicon-xcode%EF%BC%89)り、 [Mint で Permission 上の問題が発生] (https://github.com/yonaskolb/Mint/issues/188) したりで若干のしんどさを感じたので、他の方法を模索してみました。

## どうするのか

### 基本対応

大まかには以下のような感じです。

```sh:SwiftLint&#x20;0.43.1&#x20;の場合
$ curl -fsSL "https://github.com/realm/SwiftLint/releases/download/0.43.1/portable_swiftlint.zip" | bsdtar xf - -C "${インストール先}"
$ "${インストール先}/swiftlint" lint
```

1. curl コマンドで `.zip` ファイルをダウンロード、 `bsdtar` コマンドで展開して、指定の箇所に配置
1. バイナリをそのまま実行

という流れです。

前提として、

- リリースページにて macOS 上でそのまま実行可能なバイナリファイルが配布されている
- macOS 標準の CLIコマンドで展開できる `.zip` 形式等でアーカイブされている

といったあたりを要求されます。
( 有名どころでは [Carthage](https://github.com/Carthage/Carthage) が `.pkg` での配布なのでちょっと厳しそうです ) 

冒頭にも記載していますが、 iOS 開発においては macOS が前提であり、macOS においては Universal Binary や Rosetta 2 等の CPU アーキテクチャ間の差異を吸収して実行する仕組みが整備されているため、 配布されているバイナリをそのまま利用しようというのが主旨となります。

### キャッシュ

上述の通り、基本対応としては  `.zip` ファイルをダウンロード ＋ 展開 ＋ バイナリ実行するだけなのですが、これを Xcode の Build Phases 等で実行すると、ビルド毎にダウンロードが走ってしまうため、一度ダウンロード ＋ 展開した実行バイナリファイルのキャッシュとしての利用を考えます。

とはいえ対応がシンプルな分、キャッシュ利用の判断に使える情報も少ないため、今回は

- 指定展開先にバイナリが展開されているかどうか
- 展開されたバイナリのバージョンが指定バージョンかどうか

でキャッシュ利用を判断する方法を採用しました。

SwiftLint を例にすると、以下のようなシェルスクリプトによって、ダウンロードの抑制が実現できます。

```sh:swiftlint-execute.sh
#!/usr/bin/env bash

INSTALL_PATH="${インストール先}"

NAME="SwiftLint"
VERSION="0.43.1"
ZIP_URL="https://github.com/realm/SwiftLint/releases/download/${VERSION}/portable_swiftlint.zip"
BIN_PATH="${INSTALL_PATH}/swiftlint"

VERSION_CMD="${BIN_PATH} version"
EXPECTED_VERSION_FMT="${VERSION}"

# version サブコマンドを実行して、期待するバージョン値が出力されるかどうかで判断
# バイナリ自体が存在しない場合も、バージョン不一致扱いとなる
if [ "$(${VERSION_CMD} 2>/dev/null)" != "${EXPECTED_VERSION_FMT}" ]; then
  curl -fsSL "${ZIP_URL}" | bsdtar xf - -C "${INSTALL_PATH}"
  # そのままだと実行権限がついていないので
  chmod 755 "${BIN_PATH}"
fi

"${BIN_PATH}" lint --path "${Lint対象パス}"
```

バージョンが一致しているかどうかで確認することによって、ビルドツールのアップデート時には再ダウンロード＋インストールが行われることを期待しています。

### メリット

#### Mint への依存がなくなる

元々のモチベーションではあるのですが、ビルドツールの導入・管理で Mint を利用しないため、多少なりとも環境構築の手間は省けるはずです。

#### 速い

特に CI/CD 環境でのビルドで顕著だと思うのですが、ビルドツールのインストールに関してソースコードからのビルドを実施しないため、インストールに掛かる時間が短いです。

ビルドツールのバイナリサイズについても、大体は数 MB 程度なので、ダウンロード自体も大した負荷とはなってきません。

また、 CI/CD 環境においても、インストール先を指定のディクトリ配下にまとめて、キャッシュ指定することで、ダウンロードをスキップすることが可能です。

### 問題点

#### キャッシュを求めると言うほど簡単にはならない

本来の目的としては、タイトル通り「頑張らない」で管理したいのですが、前述のサンプルコードの通り、キャッシュ動作の整備等を考慮するとだんだんと複雑になってきます。

比較対象の Mint 等では、 `Mintfile` 等のパッケージ管理用のファイルを用意して1行記載するだけなのに対し、今回の対応はシェルスクリプトでの諸々の実装が発生してしまいます。

特にビルドツール毎に多少の差異が存在するため、1行記載で導入完了ということはなく、ある程度のトライ＆エラーが発生してしまうというのが辛いところです。

#### バイナリを信用できるのか

メリットの裏返しにはなってくるのですが、ソースコードからビルドしているわけではないので、バイナリに怪しいコードが含まれている可能性を排除しづらいです。

主には GitHub のリリースページからダウンロードしてくることになるため、明らかに危険な内容ということはないはずなのですが、そのまま利用するかどうかは開発ポリシー次第な感じです。

#### ダウンロードせずとも Git リポジトリにバイナリ追加でよいのでは

途中で気づいたのですが、それでいい気はしています。。

一応、

- Git リポジトリ内のバイナリファイルの出自が明示的ではない
  - 一応、ダウンロードする場合はダウンロード元 URL の情報が残る
- バイナリファイルが頻繁に更新される場合は Git と相性が悪い

といった問題はあるのですが、前者は PR レビューで抑える。後者はユースケース的にそれほど更新頻度は高くないので、影響は軽微だと思っています。

あとは Git でバイナリファイルを管理することに抵抗がないなら、ダウンロード時間の分だけ速度面でも有利になってくる認識です。

## その他

### ビルドツールの Apple Silicon 対応状況

軽く触れてきた通り、Universal Binary で `x86_64` と `arm64` に対応してくれていれば問題ないのですが、 `x86_64` バイナリだった場合は Rosetta 2 が要求されてきます。

とはいえ実際の対応状況が不明だったので、ターゲットとなってきそうな有名どころのツールに対して、 `$ lipo -archs ${調査対象ツール}` で調べてみた結果をまとめてみました。

|ツール|バージョン|`lipo -archs`|
|:-:|:-:|:-|
|[Mint](https://github.com/yonaskolb/Mint)|0.16.0|`x86_64`|
|[XcodeGen](https://github.com/yonaskolb/XcodeGen)|2.24.0|`x86_64 arm64`|
|[SwiftGen](https://github.com/SwiftGen/SwiftGen)|6.4.0|`x86_64`|
|[SwiftLint](https://github.com/realm/SwiftLint)|0.43.1|`x86_64 arm64`|
|[SwiftFormat](https://github.com/nicklockwood/SwiftFormat)|0.48.11|`x86_64 arm64`|
|[IBLinter](https://github.com/IBDecodable/IBLinter)|0.4.27|`x86_64`|
|[LicensePlist](https://github.com/mono0926/LicensePlist)|3.13.0|`x86_64`|

### サンプルプロジェクト

キャッシュ対応含めて一通りの動作を確認するためのサンプルプロジェクトを作ってみています。

https://github.com/hugehoge/example-ios-build-tools-management