macOSの.appが起動直後にクラッシュする — SwiftPMのrpath問題と直し方
App Store 外で配布する macOS アプリを SwiftPM でビルドし、.app/Contents/Frameworks/ に framework(Sparkle など)を同梱したら、ダブルクリックしても一瞬で落ちる——。これは SwiftPM が吐くバイナリの rpath が原因だ。結論を先に言うと、framework コピー後・署名前に install_name_tool で rpath を1行追加するだけで直る。実際に踏んで解決した手順を共有する。
何が起きているか
swift build が生成する実行バイナリの LC_RPATH(ライブラリ検索パス)には、@loader_path や /usr/lib/swift などは入るが、@executable_path/../Frameworks は入らない。
そのため .app/Contents/Frameworks/ に framework を置いても、dyld が @rpath/Sparkle.framework/... を解決できず、起動直後に必ずクラッシュする。
Library not loaded: @rpath/Sparkle.framework/Versions/B/Sparkle
tried: '.../Contents/MacOS/Sparkle.framework/...' (no such file)
@loader_path(=MacOS ディレクトリ)しか見ていないので、一つ上の Frameworks に到達できないわけだ。
直し方: rpath を1行追加する
framework をコピーした後、コード署名する前に、実行バイナリへ rpath を追加する。
install_name_tool -add_rpath "@executable_path/../Frameworks" \
"$APP/Contents/MacOS/$BIN"
これで dyld が .app/Contents/Frameworks/ を探しに行けるようになる。
⚠️ 必ず署名前に実行すること。 署名後に書き換えると署名が無効化され、Gatekeeper に弾かれる。
冪等にしたいなら、既に追加済みかを事前チェックするとよい。
if ! otool -l "$BIN" | grep -q "path @executable_path/../Frameworks"; then
install_name_tool -add_rpath "@executable_path/../Frameworks" "$BIN"
fi
診断のコツ
「ダブルクリックしても起動しない」は情報が少なく原因を見失いがちだ。次の順で当たると早い。
- クラッシュログを読む:
~/Library/Logs/DiagnosticReports/<App>-*.ipstermination.namespace = DYLDやLibrary missingが出ていればこの問題。 - rpath の現状を確認:
otool -l <bin> | grep -A2 LC_RPATH
@executable_path/../Frameworks が一覧に無ければ、上の手順で追加すればよい。
配布時のもう一つの罠: ._* ファイル混入
ついでに配布段階の沼も一つ。拡張属性付きのバンドルを zip -r で固めると、各ファイルが ._Foo(AppleDouble)として混入し、解凍後に署名の封印と不一致になって Gatekeeper に弾かれることがある。配布 zip は メタデータを除外して 作るのが安全だ。
ditto -c -k --keepParent --norsrc --noextattr "$App.app" "$App.zip"
厄介なのは、Finder の解凍では ._* が復元されず手元では再現しない点。検証は必ず ditto -x -k で展開し、find App.app -name '._*' | wc -l が 0 か確認する。
さらに学ぶなら
dyld・コード署名・公証まわりは、断片的な対処の積み重ねよりも仕組みを腰を据えて理解した方が結局は速い。手元に1冊リファレンスを置いておくと、こうした沼で消耗しなくなる。
まとめ
- SwiftPM のバイナリには
@executable_path/../Frameworksの rpath が付かない - framework 同梱時は 署名前に
install_name_tool -add_rpathで追加する - クラッシュは
.ipsログのDYLD/Library missingで診断できる - 配布 zip は
ditto --norsrc --noextattrで._*混入を防ぐ