プロジェクトのリポジトリは 40GB 近く、Pods や node_modules、DerivedData を合わせるとソースコード本体より大きいくらいです。ローカルで Xcode を開き、プロジェクトをクラウド Mac の SMB 共有としてマウントしてみたところ、インデックス作成に1分以上かかり、ブランチを切り替えるだけでネットワークが切れたのかと疑うほど固まってしまう——これはクラウド Mac に開発環境を移した多くの人が最初にぶつかる壁です。共有マウントは手軽に見えますが、実際に使ってみると、大規模な iOS プロジェクトのファイルアクセスパターンとネットワークファイルシステムはそもそも相性が悪いことがわかります。この記事では、HireVPS のノード上で Mutagen を使ってローカルとリモートを双方向同期し、ほぼローカルに近い体験を取り戻すまでの過程を記録します。
大規模 iOS プロジェクトに共有マウントが向かない理由
NFS や SMB といったネットワークファイルシステムは「たまにアクセスする大きなファイル」を前提に設計されています。ところが Xcode のインデックス作成、SourceKit、CocoaPods のキャッシュスキャンがやっているのはまさにその逆で、短時間に数万個の小さなファイルへ stat・open・close を繰り返します。1回の操作ごとにネットワークの往復が発生し、レイテンシがたとえ 20ms でも、それが数万回積み重なれば分単位の停止につながります。さらに厄介なのが切断時の挙動です。マウントポイントは接続が不安定になると即座にハングし、ターミナルとエディタが同時に固まって、強制アンマウントして再マウントするしかなくなります。
よく使われる3つの方式を比較すると、違いがはっきりします。
| 方式 | リアルタイム性 | 大量の小ファイルでの挙動 | 切断からの復旧 | 適用シーン |
|---|---|---|---|---|
| SMB/NFS マウント | 強い(見た瞬間が最新) | 悪い、ファイル単位でネットワーク往復 | 悪い、ハングしやすい | 大きなファイルをたまに読み書きする |
| rsync の定期実行 | 弱い、同期の間隔がある | 普通、まとめて転送すれば効率的 | 良い、ジョブとして再実行すればよい | 定期バックアップ、単方向プッシュ |
| Mutagen 双方向同期 | ほぼリアルタイム | 良い、ローカルキャッシュ+差分転送 | 良い、切断時は自動で再接続・再送 | 継続的な開発での双方向コラボレーション |
私たちが痛感した教訓のひとつは、最初は手軽さを優先して普通にマウントしてしまったことです。結果として Xcode の「Building workspace」の段階だけで平均 15〜25 秒余分に待たされ、チーム全体でインデックス待ちに毎日かなりの時間を浪費していました。Mutagen に切り替えたところ、この待ち時間はほぼ消えました。
Mutagen で双方向同期セッションを構築する
インストールと初回接続
Mutagen 自体は単一バイナリで、ローカル・クラウド Mac のどちらにも常駐サービスは不要です。接続を確立する際に、リモート側へ自動的に agent が配布されます。まずローカルにインストールします。
brew install mutagen-io/mutagen/mutagen
mutagen version
クラウド Mac 側で SSH がすでに有効になっている前提で(認証情報は開通時のメールに記載されています)、同期セッションを作成します。
mutagen sync create \
--name=ios-app \
--ignore-vcs \
--symlink-mode=posix-raw \
/Users/me/Projects/ios-app \
ssh://devuser@your-cloud-mac-host/Users/devuser/Projects/ios-app
--ignore-vcs を付けると .git 内部のオブジェクトが自動的にスキップされます(git 自体は独自プロトコルで同期する方が適しており、Mutagen で重複して転送する必要はありません)。--symlink-mode=posix-raw は CocoaPods でよく使われるシンボリックリンクをそのまま保持し、変換しないようにする設定です。
除外ルール:絶対に同期してはいけないディレクトリ
プロジェクトのルートには必ず .mutagenignore を用意してください。用意しないと、初回同期だけで数 GB のキャッシュディレクトリまで転送されてしまいます。
DerivedData/
Pods/
.build/
node_modules/
*.xcuserstate
xcuserdata/
.DS_Store
これらのディレクトリに共通しているのは「どちらの端でもローカルに再生成できる」ことです。同期してしまうと転送量が無駄になるだけでなく、次のセクションで説明する問題を引き起こすこともあります。
Xcode 関連の注意点
- DerivedData は絶対に同期しない:中のモジュールキャッシュ(ModuleCache.noindex)やビルドインデックスは、そのマシンの絶対パスやアーキテクチャに紐づいています。別のマシンに同期すると、ほぼ確実に全体の再インデックスが強制発生し、ひどい場合は意味不明なコンパイルエラーまで出ます。ローカルとクラウドでそれぞれ独立に生成させればよく、そもそも捨てて構わない生成物です。
- Pods と node_modules はそれぞれの端で個別にインストール:同期すべきなのはソースコード内の
Podfile.lockやpackage-lock.jsonであり、依存ディレクトリ自体は両端でそれぞれpod install、npm installを実行します。これにより転送量が節約できるだけでなく、バイナリ産物がアーキテクチャをまたいで不整合を起こすことも避けられます。 .gitignoreと.mutagenignoreを一致させる:2つの除外リストがずれていると、git 上は「変更なし」に見えるのに Mutagen が裏で意味のないファイルを転送し続けるという事態になり、調査に無駄な時間がかかります。変更があるたびに両方の差分をチェックする小さなスクリプトを用意しておくとよいでしょう。- パーミッションビットの違い:クラウド Mac とローカルのアカウントは UID が異なります。権限エラーが出た場合は、まず実行ビットが同期の過程で落ちていないか確認してください。
--permissions-mode=portableを使うとほとんどのケースで緩和できます。
実測パフォーマンスと競合処理
日常的な使い方であれば、保存によって発生する差分同期(数個の Swift ファイルの変更)はだいたい1〜2秒で完了し、感覚的にはローカル保存とほぼ変わりません。一方、初回のフル同期(数万個のソースファイル)はディレクトリツリーをスキャンする必要があるため明らかに時間がかかります。作業を始める前に一度実行して安定させておき、コードを書きながら初回同期を待つような使い方は避けるのがおすすめです。
競合処理については、デフォルトの双方向モードである two-way-safe は、競合が発生すると自動的に上書きせず、処理を止めてユーザーの判断を待ちます。強制上書き方式より安全ですが、両端で同じファイルを同時に編集していると頻繁に停止することになります。チームでの共同作業では、次の設定がより実用的です。
mutagen sync create \
--name=ios-app \
--sync-mode=two-way-resolved \
--default-file-mode-alpha=0644 \
/Users/me/Projects/ios-app \
ssh://devuser@your-cloud-mac-host/Users/devuser/Projects/ios-app
two-way-resolved は競合発生時にデフォルトでローカル側(alpha)を優先させます。「同じタイミングでは1人が片方だけを編集する」というルールと組み合わせれば、変更が失われることは基本的にありません。画像やフォントなどのバイナリリソースで実際に競合が起きた場合は、mutagen sync list で競合パスの一覧が表示されるので、タイムスタンプとファイルサイズを見比べてどちらを残すか手動で判断します。
そのまま使える設定例
Mutagen はプロジェクト単位の設定ファイルに対応しており、プロジェクトルートに mutagen.yml を置いておけば、チームメンバーはリポジトリをクローンした後、コマンド一発で同期を開始できます。各自がパラメータを手打ちする必要はありません。
sync:
ios-app:
alpha: "."
beta: "ssh://devuser@your-cloud-mac-host/Users/devuser/Projects/ios-app"
mode: "two-way-resolved"
ignore:
vcs: true
paths:
- "DerivedData"
- "Pods"
- "node_modules"
- "xcuserdata"
- ".build"
symlink:
mode: "posix-raw"
日常的によく使うコマンドは以下だけ覚えておけば十分です。
mutagen sync list
mutagen sync monitor ios-app
mutagen sync pause ios-app
mutagen sync resume ios-app
mutagen sync terminate ios-app
monitor は「変更が同期されないのはなぜか」を調べるときに特に便利で、スキャン・ステージング・転送の3つの段階のどこで止まっているかをリアルタイムで確認できます。
導入前チェックリスト
.mutagenignoreと.gitignoreは DerivedData、Pods、node_modules、xcuserdata など再生成可能なディレクトリを網羅していますか?- 同期モードは
two-way-safeとtwo-way-resolvedのどちらを選びましたか?チームでの共同作業なら後者を優先し、誰の変更を優先するかをあらかじめ決めておきましょう。 - 初回のフル同期は、急ぎのタスクがない時間帯に行いましたか?数万ファイルの初回スキャンは、コードを書きながら待つのには向いていません。
- クラウド Mac の具体的な機種とノードは、コンソールで現在選択できる構成を確認済みですか?帯域幅とディスク仕様が同期速度の上限に影響します。
mutagen sync monitorを確認する習慣はついていますか?ファイルが失われてから初めてログを見るのでは遅すぎます。
これらの項目を一通り確認すれば、クラウド Mac での編集体験はほぼローカルに近づき、チームでの共同作業でも「誰の変更が同期されていないのか」で責任を押し付け合うこともなくなるはずです。
よくある質問
SMB や NFS でマウントすればいいのでは?
ネットワークマウントは小さなファイルへの毎回のアクセス(Xcode のインデックス作成や Pods の参照)がすべて往復通信になり、大規模プロジェクトではプロジェクトを開くだけで数十秒かかることもある。Mutagen はローカルキャッシュを持ちながら差分同期するため、編集時の応答性がローカルとほぼ変わらず、接続が切れてもファイルシステムが固まらない。
DerivedData も同期すべき?
しない方がいい。DerivedData のモジュールキャッシュやインデックスはマシンのアーキテクチャや絶対パスに依存しているため、同期すると再インデックスやビルドエラーを招くことが多い。.mutagenignore に加えて、各マシンで個別に生成させるのが安全。
双方向同期で競合が起きたら?
mutagen sync list で競合しているパスを確認する。ソースコードの競合はほとんどの場合 two-way-resolved モードでローカル(alpha)側を優先すれば解決できる。バイナリ素材は手動で diff して判断し、同期を始める前に git status で作業ツリーを確認しておくと競合自体を減らせる。
HireVPS(HireVPS)
今すぐ専有クラウド Mac mini を試してみましょう
日単位でレンタル可能、2分で SSH/VNC の認証情報を取得、プランはいつでもアップグレードできます。