iCloud同期フォルダ配下でネイティブアプリを archive するとビルドが壊れる——症状の全パターンと、非iCloudへ逃がす手順
デスクトップや書類フォルダに置いた React Native / Expo アプリを xcodebuild archive すると、毎回違うファイルで『file not found』が出て失敗する。犯人は iCloud だ。ビルド中に実体ファイルを退避する。非iCloudへrsyncで逃がすのが確実だが、除外パターンとDerivedDataで二重に罠がある。
デスクトップや書類フォルダに置いたネイティブアプリ(React Native / Expo)を xcodebuild archive すると、数分のビルドの途中で「ファイルが無い」と言われて落ちる。しかも毎回違うファイルで落ちる。pod install 直後には確かに在ったはずのファイルが、ビルドの最中に消える。犯人はコードでもCocoaPodsでもなく iCloud だ。
事象
症状はいくつものパターンで出る。共通するのは「実体があるはずのファイルが not found になる」ことだ。
'React/RCTJSThread.h' file not found
'RCTSwiftUI.modulemap' not found
'PrivacyInfo.xcprivacy' file not found
rsync error: .../ReactNativeDependencies.xcframework/ios-arm64/...: No such file
module map '.../ExpoModulesCore.modulemap' not found
厄介なのは 毎回別のファイルで落ちる ことだ。1 つ直しても次のビルドでは別のヘッダが無いと言われる。1 ファイルずつ潰しても終わらない。pod install をやり直した直後は在るのに、archive の最中に消える。
原因
iCloud Drive(「Macストレージを最適化」がオン、あるいはデスクトップ・書類の同期がオン)が、重いビルドの最中に Pods 配下の実体ファイルをクラウドへ退避(evict) する。ローカルにはプレースホルダだけが残り、Xcode がそれを開こうとして not found になる。
さらに悪いことに、iCloud はビルド中のファイルを 重複させる ことがある。prebuilt の xcframework の本物のスライスが、末尾に「 2」の付いた重複ディレクトリ(react-native 2/ のような名前)へ逃げ、正規のパスには Headers だけが残る。だから「ソースはあるのにヘッダだけ無い」という不自然な壊れ方をする。毎回別ファイルなのは、そのとき iCloud がたまたま触ったファイルが違うからだ。
ファイル名の長さやプロジェクト構成は関係ない。同期対象のフォルダに置いていること自体が原因だ。
解決策
一番確実なのは、ビルドを非iCloudの場所でやる ことだ。プロジェクトを同期対象外のディレクトリへ rsync でコピーし、そこで archive する。だがこのコピーと再生成に、二つの罠がある。
罠1:rsync の除外はリーディングスラッシュでアンカーする
build ディレクトリを除外したくて --exclude 'build/' と書くと、rsync はパスのどこにある build でもマッチさせる。結果、node_modules/**/build(各ネイティブモジュールがビルドに使う正規のディレクトリ)まで巻き添えで消え、autolinking が壊れる。
❌ rsync -a --exclude 'build/' src/ dst/ # node_modules内のbuildまで消す
✅ rsync -a --exclude '/build/' src/ dst/ # ルート直下のbuildだけ除外
リーディングスラッシュを付けて ルート直下の build だけ にアンカーする。.git や ios/build、DerivedData も同じ考え方で、消していいのはプロジェクトルート直下のものだけだと意識して除外を書く。
罠2:node_modules に残る .DerivedData は削除する
コピー元の node_modules の中に .DerivedData が残っていると、そこに 旧プロジェクトの絶対パスが焼き込まれている。コピー先でビルドすると、モジュールキャッシュが旧パスを参照しにいって不整合を起こし、SwiftShims あたりで不可解なエラーになる。
コピー後、ビルド前に消しておく。
# コピー先で
find node_modules -name '.DerivedData' -maxdepth 3 -exec rm -rf {} +
Pods を丸ごと arm64 で作り直す
すでに iCloud に壊された Pods を 1 ファイルずつ補完しようとしても追いつかない。丸ごと削除して、arm64 ネイティブで作り直す ほうが速い。Rosetta(x86_64 エミュ)下の pod install は symlink 生成が不完全になりやすいので、アーキテクチャを明示する。
rm -rf Pods Podfile.lock
arch -arm64 env LANG=en_US.UTF-8 pod install
# DerivedData と ModuleCache も消してクリーンビルド
rm -rf ~/Library/Developer/Xcode/DerivedData/*
LANG を付けるのは、CocoaPods が UTF-8 ロケール前提で、未設定だと途中で文字コード関連のエラーを吐くことがあるためだ。
補足
- ここまでは「毎回コピーして逃がす」対症療法だ。根治は プロジェクトを iCloud 同期フォルダの外へ移す こと。デスクトップ・書類の下に置かず、ホーム直下などの同期対象外に置けば、この一連の破損は起きなくなる
- どうしても同期フォルダに置くなら、対象フォルダを Finder で「ダウンロードを保持」にし、「Macストレージを最適化」をオフにすると evict されにくくなる。ただしビルド中の重複退避を完全には防げないので、確実なのは外へ出すことだ
- 「毎回別のファイルで file not found が出る」「pod install 直後は在るのにビルドで消える」——この2点が揃ったら、コードを疑う前に置き場所を疑う
※ 本記事にはアフィリエイトリンクが含まれます。