M1 Mac で CocoaPods の pod install しようとすると ffi あたりで落ちて辛い話。

概要

ググると困ってる人の記事は他にも出てきて「ターミナルを Rosetta 2 で起動するようにチェックを入れればいい」という解決策が示されており、それはそれで正しいのだが流石に雑すぎるってのと、環境によっては事故ることも考えられるし、できればコマンドだけでなんとかしたいし、 sudo とか付けたくないし、折角の M1 機のシステム領域に x86_64 のバイナリを入れたくない。

とはいえ大まかな方針としては Rosetta 2 経由で x86_64 の Ruby を使うことに変わりはないです。

ターミナル.app の「Rosetta を使用して開く」にチェックを入れて一時的に対応しても絶対元に戻し忘れるので…。

環境

  • M1 MacBook Air (MacBookAir10,1)
  • macOS 11.4 (20F71)
  • Xcode 12.5 (12E262)
  • cocoapods (1.10.0)
  • ffi (1.15.1)

問題について

CocoaPods 本体でも この issue で上げられているのだが、エラーメッセージ自体は割と自由な感じで出てくるようだ。

僕の環境だと

[!] Couldn't determine repo type for URL: `https://github.com/CocoaPods/Specs.git`: unable to resolve type 'size_t'

unable to resolve type 'size_t' と、まぁまぁ闇の深そうなログが出てきた。これはアカンやつだなという感じ。

イシューのコメント欄でも言われてるが、 Rosetta 2 経由で使ってねということなので、人生で n 度目の CocoaPods を投げ捨てたい気持ちを高めながら作業していくことになる。

macOS Big Sur 標準の Ruby は Universal Binary

macOS にはデフォルトで Ruby が付属しているが、なんと Universal Binary になっているため、 arch コマンド経由で呼び出してやれば x86_64 のバイナリとして使用することができる。

とはいえ普段は rbenv 等を利用していてシステムの Ruby をそのまま使わないようにしている人も多いだろう。

一時的に rbenv local system などしてもいいとは思うが、リポジトリで .ruby-version をバージョン管理していることもあるだろうし、その辺は柔軟にやりましょう。

PATH 上に同名で複数の実行ファイルがあるかを調べる。

普通にプログラミングをしていると PATH 上に複数の ruby が存在する状況にはよくなると思うが、実際にどうなっているのかを調べるには which-a を付けたり type-a を付けると調査できる。 (これは結構ググりにくい情報だと思う)

$ which -a ruby
/opt/homebrew/opt/rbenv/shims/ruby
/usr/bin/ruby
$ type -a ruby
ruby is /opt/homebrew/opt/rbenv/shims/ruby
ruby is /usr/bin/ruby

自分の環境では homebrew で rbenv を入れてるため上記のように出てくるが、個々の環境に拠る。

ちなみに whichtypezsh ではビルトイン関数で、 bash では type は組み込み関数、 which/usr/bin/which と出てくる。

一応言及しておくと /usr/bin/type/usr/bin/which にも同名で同等の機能を持つコマンドが実行ファイルとしても存在する。

file コマンドで詳細を調べる。

バイナリがどうなっているのかなどのファイルの詳細情報は file コマンドで調べられる。

$ file /opt/homebrew/opt/rbenv/shims/ruby
/opt/homebrew/opt/rbenv/shims/ruby: Bourne-Again shell script text executable, ASCII text

Bourne-Again shell script text executable, ASCII text と出てくる。

rbenv の shims 以下に見える ruby はテキストファイルで、実体は以下のような場所にあり

$ file /opt/homebrew/opt/rbenv/versions/2.7.3/bin/ruby
/opt/homebrew/opt/rbenv/versions/2.7.3/bin/ruby: Mach-O 64-bit executable arm64

Mach-O 64-bit executable arm64 と出てくる。

システム標準の Ruby はというと

$ file /usr/bin/ruby
/usr/bin/ruby: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64e:Mach-O 64-bit executable arm64e]
/usr/bin/ruby (for architecture x86_64):  Mach-O 64-bit executable x86_64
/usr/bin/ruby (for architecture arm64e):  Mach-O 64-bit executable arm64e
  • Mach-O 64-bit executable x86_64
  • Mach-O 64-bit executable arm64e

こう出てきて、 fat binary であることが分かる。

ちなみに arm64 と arm64e はちょっと違うらしいが、まぁ大体同じという認識で良さそう。

なお Ruby 自体のバージョンは

$ /usr/bin/ruby --version
ruby 2.6.3p62 (2019-04-16 revision 67580) [universal.arm64e-darwin20]

とのことで macOS 11.4 時点では 2.6.3 らしい。

rbenv で universal binary な ruby を入れられる? (未調査)

調べてもパっと出てこなかったが、 rbenv というか ruby-build にはビルドオプションを結構生のまま渡せる構造になってるので、もしかしたら -arch を渡してクロスコンパイルできるかもしれないし、もしビルドが出来るなら lipo コマンドの -create で成果物をまとめれば出来るかもしれない?ちょっとわからん。

homebrew 管理下に rbenv を置くなら、 x86_64 版の homebrew を別個で入れて、 Rosetta 2 経由でシェルを起動したどうかを検出・場合分けして /usr/local/opt/homebrew の向き先を切り替えるような実装を dotfiles にする方が筋が良いかもしれない。

暇になったら調査したり実験したりしてみます。

システム標準の universal binary となってる Ruby を CocoaPods で良い感じに使う。 (本題)

結論になるが CocoaPods を入れるための bundle install はこういう感じ。

# 環境によっては事前に Gemfile.lock を編集か削除しておく必要があるかもしれない。
$ env -i arch -x86_64 bundle install --path vendor/bundle

bundler execpod install するときはこう。

# 環境によっては事前に `rbenv local system` の必要があるかもしれない。
# pod install に関しては `env -i` で環境変数をクリアしちゃうとコケる。
$ arch -x86_64 bundle exec pod install

解説

  • システム領域を汚したくないので sudo は付けない。
  • env コマンドに -i を付けて完全に環境変数をクリアしておくと
    • そもそも rbenv 管理下の ruby に行きつかなくなる。
    • /usr/binPATH がクリアされてても探してくれる。
      • 結果としてシステムで入っている Universal Binary な ruby に行きつく。
    • LDFLAGSCPPFLAGS に homebrew で入れたものが入ってたりするとネイティブエクステンションのビルド時の事故るので、それも防げる。
  • arch -x86_64 ...x86_64 を指定して Rosetta 2 経由で呼び出す。
  • 既に非推奨だが --path vendor/bundle を指定してシステム領域を汚さないようにする。
    • bundle config で先に設定することが推奨されてるので、そうなってるなら指定しなくてもいいかもしれない。

こうするとネイティブエクステンションで x86_64 のバイナリも vendor/bundle 以下に閉じ込められるので精神衛生が良い。

システム標準じゃなくて自前で用意した Ruby を使いたい? (やってない)

iOS アプリ開発の現場では CocoaPods と fastlane をちょっと使ってる、くらいの環境が大半だろうし、 Ruby のバージョンを厳密に固定したいというモチベも分かるけど、手元で開発環境作れない方が本末転倒なので .ruby-version や Gemfile.lock での Ruby バージョン固定は一旦無視するのがコスパが良いと思う。

どうしても厳密に Ruby のバージョンを固定してやりたいなら頑張って指定されたバージョンの Ruby を x86_64 darwin 向けにビルドする必要が出てくるけど…。

たぶんその内 ffi の方が直って CocoaPods 本体に取り込まれるんじゃないかな…知らんけど。

その CocoaPods 本当に必要ですか?

CocoaPods は Specs を git で管理 しており、内部的な都合で利用者は手元のマシンの ~/.cocoapods/repos/cocoapodsgit clone させられている。

私見だが、この運用はとっくに破綻していると言わざるを得ない。

$ cd ~/.cocoapods/repos/cocoapods

$ git log --oneline --no-merges | wc -l
580189

$ du -h . --max-depth=1
2.0G  ./Specs
16K   ./Scripts
4.0K  ./.github
1.1G  ./.git
3.1G  .

手元で実行したのが上の結果だが、コミット数が 58 万を越え、容量が 3.1 GB にもなるリポジトリを各々のマシンローカルに用意させられる。そしてこのリポジトリは日々成長している。

CocoaPods は主に xcodeproj を良い感じに弄ったり生成して xcworkspace に統合することによってビルド設定 (主にリンカ周り) を自動的にやってくれることが長所でもあるが、ちょっとややこしいことをすると結局 xcodeproj を良い感じに生成するための生に近いフラグを Podfile という Ruby DSL 内で弄ることになり、それは単純にバッドノウハウであると思われる。

結局のところアプリ開発者はリンカ周りなどのビルド設定について xcodeproj や xcodebuild への学習コストを払わざるをえない。それならば最初から自分で設定した方が遥かにマシであると思う。

CocoaPods を使わないで済むならそれに越したことはないし、 CocoaPods に依存しないような努力をすべきというのが個人的な見解です。

しかし Carthage にも細かい問題はあることは否めないし、 Swift Package Manager が全てを解決するかというと…まだそうでもない。

Swift Package Manager で静的ライブラリを上手く扱うために Umbrella Framework を作る必要が出てきたりするし…。

締め

個人的な優先度で言うと

Swift Package Manager > Carthage >>> 越えられない壁 >>> CocoaPods

なのだが、

  • 事実上のデファクトスタンダード (先行者利益)
  • xcodeproj を自分で弄らなくていいという導入のしやすさ (ただのまやかし)
  • 辛いのは初回だけ (のように見える)

という事情もあり、 CocoaPods が無くなることはたぶん無いでしょう。辛い世の中ですね。

どうしても使うなら kishikawakatsumi 先生の CocoaPodsをWorkspaceに自動統合せずに利用する あたりを参考にしつつ integrate_targets: false 指定の Podfile を書くか pod install --no-integrate して、ちゃんと xcodeproj の設定を詰めた方が良い。