LINE iOSアプリにおけるMergeable Libraryの段階的導入
LY Corp は大規模 iOS プロジェクトのビルド効率と起動性能を両立するため、Xcode 15 で導入された Mergeable Library を段階的に採用し、開発体験とリリース品質の向上を図っている。
キーポイント
Mergeable Library の仕組み
Debug ビルド時は Dynamic Framework として動作してビルド速度と Xcode Previews の安定性を確保し、Release ビルド時は Static Framework 同様にバイナリをマージして起動時間とサイズを最適化する。
大規模プロジェクトの課題
LINE iOS は 250 万行以上のコードを持つ巨大な Static Framework 構成であり、従来の方式では Xcode Previews の利用が不安定で開発効率が低下していた。
導入戦略と手法
自動マージ(automatic)ではなく手動制御(manual)を選択し、依存関係の複雑さを管理しながら段階的に移行を進めることでリスクを最小化している。
直接依存の明示的宣言と自動検証
Static Linker は直接依存関係のみをマージするため、transitive dependency の Mergeable Library を明示的に direct dependency として宣言する必要がある。数百のターゲットを手動管理するのは困難なため、依存グラフを解析する自動検証ツールを開発して導入した。
App Extension 特有の設定とリスク
App Extension では Debug ビルド時に ReexportedBinaries の検索パスが自動追加されないため手動設定が必要であり、依存グラフの違いからフレームワークの消失(クラッシュ)が発生しやすい。メインアプリでは顕在化しにくい問題が App Extension で表面化するため、特に注意と検証が必要である。
Debug と Release の両環境での検証
Dynamic Link によるリソースバンドルの参照方法の違いや、マージの挙動の違いにより、Debug ビルドでは問題がなくても Release ビルド(ベータ配布)でクラッシュするケースがあるため、両方の構成で厳密にテストを行う必要がある。
重要な引用
Mergeable Library にすることで Debug ビルド時は Dynamic Framework として振る舞うため、Xcode Previews の安定性が向上します。
Release ビルド時:Static Framework のようにバイナリがマージされ、起動時間とアプリサイズを最適化
250 万行以上のコードと 600 を超える Xcode プロジェクトから構成される巨大なプロジェクトです
The static linker only merges direct dependencies. So, to include more mergeable libraries, you should set them as explicit link dependencies.
大規模プロジェクトへの新技術導入は、一発で成功させることが難しいものです。重要なのは、各リバートから学びを得て、同じ問題を繰り返さない仕組みを構築することです。
影響分析・編集コメントを表示
影響分析
この記事は、単なる新機能の紹介にとどまらず、数百万行規模の複雑なコードベースを持つ大規模アプリ開発における具体的な課題解決策を示しています。Mergeable Library の実装により、従来の Static/Dynamic Framework のトレードオフを打破し、開発効率と製品品質の両立を実現する標準的なアプローチとして業界に示唆を与えるものです。
編集コメント
大規模モバイルアプリ開発の現場における、具体的な技術的課題と解決策を詳細に記述した貴重な記事です。特に「automatic」ではなく「manual」を選択した判断プロセスは、類似規模のプロジェクトを持つ開発チームにとって非常に参考になります。
はじめに
LINE アプリ開発 SBU・モバイルエクスペリエンス開発ディビジョンの ikesyo です。普段は LINE iOS アプリのビルドシステムや開発基盤の改善を担当しています。
LINE iOS は 250 万行以上のコードと 600 を超える Xcode プロジェクトから構成される巨大なプロジェクトです。これだけの規模のプロジェクトでは、ほとんどのモジュールが Static Framework としてビルドされています。Dynamic Framework だと dyld による動的リンクの負荷が起動時間に影響するためです。
しかし、Static Framework 中心の構成にも課題がありました。Xcode 16 から Static Framework でも Xcode Previews が利用可能になりましたが、依存先に Static Framework が多いモジュールでは、プレビューのビルドが終わらなかったり、プレビューが表示されないことがありました。この問題の詳細については、筆者の iOSDC Japan 2025 での発表 『大規模アプリにおける Xcode Previews 実用化までの道のり』 でも触れています。
この課題を解消するアプローチとして、Xcode 15 で導入された Mergeable Library の段階的な導入を進めています。Mergeable Library にすることで Debug ビルド時は Dynamic Framework として振る舞うため、Xcode Previews の安定性が向上します。さらに、Debug ビルドのリンク時間削減による開発者体験の改善にもつながります。
この記事では、Mergeable Library の概要と、LINE iOS のような大規模プロジェクトで実際に導入を進める中で遭遇した課題や、それらをどのように解決したかを紹介します。
Mergeable Library — Debug/Release でリンク方式を切り替える
Mergeable Library は、Xcode 15 から利用可能になったリンク方式です。従来、iOS アプリのフレームワークは Static Framework か Dynamic Framework のどちらかを選ぶ必要がありましたが、Mergeable Library はこの二者択一を解消します。
従来の方式にはそれぞれトレードオフがありました。
- Dynamic Framework: ビルドは高速だが、起動時に dyld による動的リンクが発生し、フレームワーク数に比例して起動時間が悪化する
- Static Framework: 起動時のリンクコストがないが、ビルド時間が長く、Xcode Previews が使えない(Xcode 16 以前)などの制約がある
Mergeable Library は、メタデータを持った特殊な Dynamic Library です。ビルド構成に応じてリンク方式を自動的に切り替えることができます。
- デバッグビルド時:ダイナミックフレームワークとして振る舞います。マージは行われず、バンドル内の ReexportedBinaries/ディレクトリに再エクスポートされた状態で配置されます。これにより高速なインクリメンタルビルドと Xcode Previews を実現します
- リリースビルド時:静的フレームワークのようにバイナリがマージされ、起動時間とアプリサイズを最適化します
この仕組みにより、開発時の快適さとリリース時のパフォーマンスを両立できます。Mergeable Library の基本的な仕組みについては、同僚の giginet さんが iOSDC Japan 2024 で発表した『Mergeable Library で高速なアプリ起動を実現しよう!』も参照してください。
LINE iOS での導入背景 — Static Framework の依存グラフと Xcode Previews
前述の通り、LINE iOS では Static Framework 中心の構成を採用していますが、Static Framework が多数連なる依存グラフの中で Xcode Previews を安定して動作させることが困難でした。Mergeable Library へ移行すればこの問題の解消が期待できるうえ、リリースビルドではバイナリがマージされるため、起動時間やアプリサイズへの悪影響もありません。こうした背景から、段階的な移行を開始することにしました。
段階的な移行を進めるにあたって
2 種類のマージ方式 — automatic ではなく manual を選択
Mergeable Library のマージ方式には automatic と manual の 2 種類があります(Apple 公式ドキュメント参照)。automatic の場合、リリースビルド時にアプリターゲットの直接依存であるダイナミックフレームワーク/ダイナミックライブラリ(同一プロジェクト内のターゲットが生成するもの)が自動的に Mergeable Library として扱われ、マージされます。一方 manual は、MERGEABLE_LIBRARY: YES が明示的に設定されたライブラリのみを選択的にマージします。
LINE iOS のように数百を超えるモジュールを持つプロジェクトでは、全てのモジュールを一度に Mergeable Library に変換することは現実的ではありません。移行は段階的に進める必要があり、どのモジュールをマージ対象にするかを明示的に制御できる MERGED_BINARY_TYPE: manual を採用しています。
シンボル重複を防ぐため依存ツリーの末端から移行する
段階的に移行するとして、どのモジュールから手をつけるべきでしょうか。ここで注意が必要なのは、依存ツリーの途中にあるモジュールを単独で Mergeable Library(= ダイナミックライブラリ)に変換すると、シンボル重複が発生するという点です。
Static Framework に依存するターゲット(アプリ等)では、依存先のシンボルがそのターゲットのバイナリに静的リンクされます。あるモジュールを Mergeable Library へ変換すると、そのモジュールの依存先シンボルは Mergeable Library 内にも含まれる一方で、アプリターゲット側にも同じ依存先が静的リンクされたままとなり、同じシンボルが二重に存在してしまうのです。
以下の図は、依存ツリーの途中にあるモジュール B を Mergeable Library へ変換した場合にシンボル重複が発生する様子を示しています。

この場合、C のシンボルが図のように B と App 双方に存在してしまいます。これを避けるには、C を先に Mergeable Library 化する(依存ツリーの末端から移行していく)必要があります。
この制約から、移行は依存ツリーのリーフ(末端)からルート(上流)に向かって進める必要があります。具体的には以下の順序です。
- 依存先を持たない(または全ての依存先がすでに Mergeable Library である)モジュールから変換する
- そのモジュールに依存する上流モジュールは、自身の依存先が全て Mergeable Library 化された後に変換できるようになる
- これを依存ツリーの上方向に繰り返す
LINE iOS ではモジュールを大きく以下の 3 つの層に分けて管理しています。
- Utility モジュール:他のモジュールへの依存が少ない基盤ユーティリティ群。依存ツリーの末端に位置する
- Shared モジュール:複数の機能モジュールから共通して利用されるモジュール群。主に Utility モジュールに依存する
- Feature モジュール:個別の機能を担うモジュール群。依存ツリーの上位に位置し、Utility や Shared モジュールに依存する
各層には多数のモジュールが存在しており、前述の「依存ツリーの末端からルートへ」の原則に従うと、移行は Utility → Shared → Feature の順に進めることになります。これに基づき、移行を 3 つのフェーズに分けて計画しました。

Phase 1 では、まず他のモジュールへの依存が一切ないモジュールの一つから移行を開始しました。
マイグレーション候補の特定
各層に多数のモジュールが存在するため、どのモジュールが現時点で移行可能かを手動で判断するのは現実的ではありません。そこで、依存ツリーを解析して Mergeable Library 化の候補を一覧化する CLI ツールを社内で用意しています。各モジュールの依存関係を解析し、現時点で安全に移行できるモジュールをリストアップする仕組みです。
XcodeGen での Mergeable Library 設定
LINE iOS では XcodeGen を用いて Xcode プロジェクトを生成しています。Mergeable Library への移行は、Project Spec(project.yml)の設定変更が中心です。
MergeableLibrary テンプレート
共通設定を XcodeGen のテンプレートとして定義しています。
MergeableLibrary:
settings:
base:
MACH_O_TYPE: mh_dylib
MERGEABLE_LIBRARY: YES
GENERATE_INFOPLIST_FILE: YES
PRODUCT_BUNDLE_IDENTIFIER: com.example.$(TARGET_NAME)
モジュールを Mergeable Library に変換するには、そのモジュールの project.yml にテンプレートを追加するだけです。
targets:
MyUtilityModule:
templates:
- Framework
- MergeableLibrary # この行を追加
アプリターゲットの設定
メインアプリや App Extension など、Mergeable Library をマージする側のターゲットには MERGED_BINARY_TYPE: manual を設定します。
settings:
base:
MERGED_BINARY_TYPE: manual
この設定については後述する「落とし穴 3」で重要な意味を持ちます。
落とし穴 1: App Extension が ReexportedBinaries を見つけられない
最初に遭遇したのは、Debug ビルド時の App Extension に関する問題でした。App Extension が実行時に Dynamic Framework を検索するパス(LD_RUNPATH_SEARCH_PATHS)が正しく設定されていなかったのです。
前述の通り、Debug ビルド時に Mergeable Library はマージされず、バンドル内の ReexportedBinaries/ディレクトリに配置されます。このディレクトリは、通常 embed された Dynamic Framework がコピーされる Frameworks/ディレクトリと隣り合って存在しています。
MyApp.app/
├── MyApp (実行バイナリ)
├── Frameworks/
│ └── SomeDynamic.framework (通常の embed された Dynamic Framework)
├── ReexportedBinaries/
│ └── MyUtilityModule.framework (Mergeable Library: Debug ビルド時に re-export される)
└── PlugIns/
└── MyExtension.appex/
├── MyExtension (Extension 実行バイナリ)
├── Frameworks/
└── ReexportedBinaries/
アプリケーションターゲットの場合、Xcode のビルドシステム(swift-build)が自動的に@loader_path/ReexportedBinariesをLD_RUNPATH_SEARCH_PATHSに追加してくれるため、特に何もする必要はありません。
しかし、App Extensionターゲットにはこの自動追加が行われないことを確認しました。そのため、App ExtensionのProject Specに手動でLD_RUNPATH_SEARCH_PATHSを追加する必要がありました。
settings:
base:
LD_RUNPATH_SEARCH_PATHS:
- "$(inherited)"
- "@executable_path/ReexportedBinaries"
- "@executable_path/Frameworks"
- "@executable_path/../../ReexportedBinaries"
- "@executable_path/../../Frameworks"
この設定がないと、Debugビルド時にApp Extensionが起動した際、ReexportedBinaries内のMergeable Libraryを見つけられずクラッシュします。
落とし穴2: Dynamic Linkでリソースバンドルが見つからなくなる
もう一つのDebugビルド時の問題として、リソースバンドルの解決があります。
LINE iOSではこれまでStatic Framework前提で開発してきました。Static Linkの場合、Bundle(for: ...)はアプリケーションのバンドルを返すため、アプリ内にembedされたリソースバンドルに問題なくアクセスできます。
しかし、Mergeable Library化によりDebugビルドがDynamic Linkになると、Bundle(for: ...)はそのモジュール自身のバンドルを返すようになります。モジュールバンドル内にはリソースバンドルが含まれていないため、リソースが見つけられなくなってしまうのです。
この問題に対しては、Bundle(for: ...)でリソースバンドルが見つからなかった場合のフォールバックとしてBundle.mainを探すようにする対応を行いました。これにより、Static Link(Release)でもDynamic Link(Debug)でも正しくリソースにアクセスできるようになりました。
落とし穴3: transitive dependency(推移的依存)のMergeable Libraryがマージされず消失する
Debugビルドでの問題を解決した後、さらにいくつかのユーティリティモジュールのMergeable Library化を進めました。するとReleaseビルドのベータ配布でApp Extensionがクラッシュする問題が発生しました。
原因は、WWDC2023 のセッション『Meet mergeable libraries』でも言及されている、Mergeable Library の重要な制約でした。
**
The static linker only merges direct dependencies. So, to include more mergeable libraries, you should set them as explicit link dependencies.
Static Linker は直接の依存関係(direct dependencies)のみをマージする**のです。MERGED_BINARY_TYPE: manual が設定されたターゲットにとって、transitive dependencies(間接的な依存)の中にある Mergeable Library はマージ対象になりません。Release ビルド時、マージされなかった Mergeable Library はバンドルに埋め込まれず、ReexportedBinaries へのエクスポートもされないため、ランタイムにはそもそも存在しない状態になり、シンボル解決に失敗してクラッシュします。
メインアプリ:Module C を direct dependency として宣言 → マージ成功

App Extension:Module C が transitive dependency のまま → マージされず消失

メインアプリでは Module C を direct dependency として明示的に宣言しているためマージされますが、App Extension では宣言がないため Module C はマージも埋め込みもされず、ランタイムに存在しない状態になります。
LINE アプリには Share Extension、Notification Service Extension など複数の App Extension が存在し、それぞれが異なる依存グラフを持っています。メインアプリのターゲットでは問題なくマージされても、App Extension のターゲットでは依存構造が異なるためマージされないケースが発生したのです。
対処:direct dependency の明示的な宣言
この問題に対処するには、MERGED_BINARY_TYPE: manual を持つ全てのターゲットにおいて、transitive dependencies に含まれる Mergeable Library を direct dependency として明示的に宣言する必要があります。
targets:
MyWidgetExtension:
settings:
base:
MERGED_BINARY_TYPE: manual
dependencies:
# Mergeable Library は direct dependency(直接依存)として宣言し、
# MERGED_BINARY_TYPE: manual ターゲットに正しくマージされるようにする
- framework: MyUtilityModule.framework
implicit: true
なお、Static Framework 時に設定していた既存の依存宣言の embed: false フラグは削除する必要はありません。Debug ビルド時、Mergeable Library は ReexportedBinaries(再エクスポートされたバイナリ)を通じて動的リンクされるため、embed 設定の変更は不要です。
しかし、LINE iOS には数百を超えるターゲットが存在します。この設定を手動で管理し続けるのは現実的ではなく、自動検証ツールの開発につながりました。
direct dependency(直接依存)宣言漏れを防ぐ自動検証ツール
落とし穴 3 で述べた問題は、MERGED_BINARY_TYPE: manual を持つ全てのターゲットに対して、transitive dependencies(間接依存)の中の Mergeable Library を正しく direct dependency として宣言し続けなければならないという点で、手動での管理が困難です。
そこで、Mergeable Library の依存関係を自動検証する Lint ツールを開発しました。前述の通り LINE iOS では XcodeGen を使っているため、全ての Project Spec(project.yml)を読み込み、各ターゲットの dependencies 配列から依存グラフを構築・解析しています。このツールは、MERGED_BINARY_TYPE: manual が設定された全ターゲットを対象に、前述の direct dependency 宣言が漏れていないかを検証し、不足があればエラーとして報告します。
このツールはプロジェクト生成時に実行されるため、依存関係の不整合は CI(継続的インテグレーション)はもちろん、開発者のローカル環境でも即座に検出されます。新しいモジュールの追加や依存関係の変更があった場合も、人間が見落とすことなく問題を検出できるようになりました。
なお、XcodeGen を使わず Xcode プロジェクトをソース管理にコミットして手動管理しているプロジェクトでも、XcodeProj のようなライブラリを使えば.xcodeproj から同様に依存グラフを解析し、自動検証ツールを構築することが可能です。
リバートの連続から学んだこと
今回の導入過程では、Mergeable Library 化 → 問題発覚 → リバートというサイクルを複数回経験しました。大規模プロジェクトへの新技術導入は、一発で成功させることが難しいものです。重要なのは、各リバートから学びを得て、同じ問題を繰り返さない仕組み(今回は自動検証ツール)を構築すること。そして、この記事のように学びを言語化して共有することも、チームや社外の同じ課題に取り組む方々にとって価値があると考えています。
モジュール変換時に確認すべきポイント
自動検証ツールに加えて、モジュールを変換した際には以下の点を確認することをおすすめします。
- デバッグビルドの確認:アプリがローカルで正常に起動・動作するか
- リリースビルドの確認:ベータビルドが正常に作成・配布できるか(App Store Connect のバリデーションを含む)
- テストの実行:対象モジュールのユニットテストが通るか
- Xcode プレビューの確認:該当モジュールのプレビューが正しく動作するか
特にリリースビルド(ベータビルド)の検証は重要です。デバッグビルドでは問題がなくてもリリースビルドで初めて顕在化する問題があります。
まとめ
Mergeable Library は、大規模 iOS プロジェクトにおける Xcode プレビューの安定性やデバッグビルドのリンク時間といった課題を解消しつつ、リリースビルドのパフォーマンスも維持できる強力な仕組みです。しかし、実際に導入を進めると、ドキュメントだけでは読み取れない落とし穴がいくつも存在しました。
本記事で紹介した主な落とし穴をまとめます。
問題ビルド構成原因対処
LD_RUNPATH_SEARCH_PATHS の不一致デバッグApp Extension に ReexportedBinaries の検索パスが自動追加されない手動で LD_RUNPATH_SEARCH_PATHS を設定
リソースバンドルが見つからないデバッグダイナミックリンク時に Bundle(for:) がモジュールバンドルを返すBundle.main へのフォールバック追加
App Extension でフレームワークが消失リリース静的リンカーが direct dependencies しかマージしないdirect dependency の明示的宣言 + 自動検証ツール
大規模プロジェクトでの Mergeable Library 導入で特に強調したいのは以下の 3 点です。
- リーフからルートへ:依存ツリーの末端から順に移行しないとシンボル重複が発生する。ツールで候補を機械的に特定すべき
- App Extension に特に注意:direct dependency の明示的宣言は MERGED_BINARY_TYPE: manual を持つ全てのターゲットに必要だが、メインアプリは多くのモジュールを直接依存として持つため問題が顕在化しにくい。App Extension は依存グラフが異なるため表面化しやすく、加えて LD_RUNPATH_SEARCH_PATHS の手動設定も App Extension でのみ必要になる。依存関係の自動検証ツールの導入をおすすめする
- デバッグとリリースの両方で検証する:デバッグビルドでは見えない問題、リリースビルドでは見えない問題がそれぞれ存在する
現時点で LINE iOS で Mergeable Library 化が完了しているのはまだ数モジュールですが、自動検証ツールやマイグレーション候補の特定ツールといった基盤が整ったことで、今後は徐々に対象を広げていく予定です。
この記事が、Mergeable Library の導入を検討されている方の参考になれば幸いです。
原文を表示
はじめに
LINEアプリ開発SBU・モバイルエクスペリエンス開発ディビジョンのikesyoです。普段はLINE iOSアプリのビルドシステムや開発基盤の改善を担当しています。
LINE iOSは250万行以上のコードと600を超えるXcodeプロジェクトから構成される巨大なプロジェクトです。これだけの規模のプロジェクトでは、ほとんどのモジュールがStatic Frameworkとしてビルドされています。Dynamic Frameworkだとdyldによる動的リンクの負荷が起動時間に影響するためです。
しかし、Static Framework中心の構成にも課題がありました。Xcode 16からStatic FrameworkでもXcode Previewsが利用可能になりましたが、依存先にStatic Frameworkが多いモジュールでは、プレビューのビルドが終わらなかったり、プレビューが表示されないことがありました。この問題の詳細については、筆者のiOSDC Japan 2025での発表『大規模アプリにおけるXcode Previews実用化までの道のり』でも触れています。
この課題を解消するアプローチとして、Xcode 15で導入されたMergeable Libraryの段階的な導入を進めています。Mergeable LibraryにすることでDebugビルド時はDynamic Frameworkとして振る舞うため、Xcode Previewsの安定性が向上します。さらに、Debugビルドのリンク時間削減による開発者体験の改善にもつながります。
この記事では、Mergeable Libraryの概要と、LINE iOSのような大規模プロジェクトで実際に導入を進める中で遭遇した課題や、それらをどのように解決したかを紹介します。
Mergeable Library — Debug/Releaseでリンク方式を切り替える
Mergeable Libraryは、Xcode 15から利用可能になったリンク方式です。従来、iOSアプリのフレームワークはStatic FrameworkかDynamic Frameworkのどちらかを選ぶ必要がありましたが、Mergeable Libraryはこの二者択一を解消します。
従来の方式にはそれぞれトレードオフがありました。
- Dynamic Framework: ビルドは高速だが、起動時にdyldによる動的リンクが発生し、フレームワーク数に比例して起動時間が悪化する
- Static Framework: 起動時のリンクコストがないが、ビルド時間が長く、Xcode Previewsが使えない(Xcode 16以前)などの制約がある
Mergeable Libraryは、メタデータを持った特殊なDynamic Libraryです。ビルド構成に応じてリンク方式を自動的に切り替えることができます。
- Debugビルド時: Dynamic Frameworkとして振る舞う。マージは行われず、バンドル内のReexportedBinaries/ディレクトリにre-exportされた状態で配置される。これにより高速なインクリメンタルビルドとXcode Previewsを実現する
- Releaseビルド時: Static Frameworkのようにバイナリがマージされ、起動時間とアプリサイズを最適化
この仕組みにより、開発時の快適さとリリース時のパフォーマンスを両立できます。Mergeable Libraryの基本的な仕組みについては、同僚のgiginetさんがiOSDC Japan 2024で発表した『Mergeable Libraryで高速なアプリ起動を実現しよう!』も参照してください。
LINE iOSでの導入背景 — Static Frameworkの依存グラフとXcode Previews
前述の通り、LINE iOSではStatic Framework中心の構成を採用していますが、Static Frameworkが多数連なる依存グラフの中でXcode Previewsを安定して動作させることが困難でした。Mergeable Libraryへ移行すればこの問題の解消が期待できるうえ、Releaseビルドではバイナリがマージされるため、起動時間やアプリサイズへの悪影響もありません。こうした背景から、段階的な移行を開始することにしました。
段階的な移行を進めるにあたって
2種類のマージ方式 — automaticではなくmanualを選択
Mergeable Libraryのマージ方式にはautomaticとmanualの2種類があります(Apple公式ドキュメント参照)。automaticの場合、Releaseビルド時にアプリターゲットの直接依存であるDynamic Framework / Dynamic Library(同一プロジェクト内のターゲットが生成するもの)が自動的にMergeable Libraryとして扱われ、マージされます。一方manualは、MERGEABLE_LIBRARY: YESが明示的に設定されたライブラリのみを選択的にマージします。
LINE iOSのように数百を超えるモジュールを持つプロジェクトでは、全てのモジュールを一度にMergeable Libraryに変換することは現実的ではありません。移行は段階的に進める必要があり、どのモジュールをマージ対象にするかを明示的に制御できるMERGED_BINARY_TYPE: manualを採用しています。
シンボル重複を防ぐため依存ツリーの末端から移行する
段階的に移行するとして、どのモジュールから手をつけるべきでしょうか。ここで注意が必要なのは、依存ツリーの途中にあるモジュールを単独でMergeable Library(= Dynamic Library)に変換すると、シンボル重複が発生するという点です。
Static Frameworkに依存するターゲット(アプリ等)では、依存先のシンボルがそのターゲットのバイナリに静的リンクされます。あるモジュールをMergeable Libraryに変換すると、そのモジュールの依存先シンボルはMergeable Library内にも含まれる一方で、アプリターゲット側にも同じ依存先が静的リンクされたままとなり、同じシンボルが二重に存在してしまうのです。
以下の図は、依存ツリーの途中にあるモジュールBをMergeable Libraryに変換した場合にシンボル重複が発生する様子を示しています。

この場合、Cのシンボルが図のようにBとApp双方に存在してしまいます。これを避けるには、Cを先にMergeable Library化する(依存ツリーの末端から移行していく)必要があります。
この制約から、移行は依存ツリーのリーフ(末端)からルート(上流)に向かって進める必要があります。具体的には以下の順序です。
- 依存先を持たない(または全ての依存先がすでにMergeable Libraryである)モジュールから変換する
- そのモジュールに依存する上流モジュールは、自身の依存先が全てMergeable Library化された後に変換できるようになる
- これを依存ツリーの上方向に繰り返す
LINE iOSではモジュールを大きく以下の3つの層に分けて管理しています。
- Utilityモジュール: 他のモジュールへの依存が少ない基盤ユーティリティ群。依存ツリーの末端に位置する
- Sharedモジュール: 複数の機能モジュールから共通して利用されるモジュール群。主にUtilityモジュールに依存する
- Featureモジュール: 個別の機能を担うモジュール群。依存ツリーの上位に位置し、UtilityやSharedモジュールに依存する
各層には多数のモジュールが存在しており、前述の「依存ツリーの末端からルートへ」の原則に従うと、移行はUtility → Shared → Featureの順に進めることになります。これに基づき、移行を3つのフェーズに分けて計画しました。

Phase 1では、まず他のモジュールへの依存が一切ないモジュールの一つから移行を開始しました。
マイグレーション候補の特定
各層に多数のモジュールが存在するため、どのモジュールが現時点で移行可能かを手動で判断するのは現実的ではありません。そこで、依存ツリーを解析してMergeable Library化の候補を一覧化するCLIツールを社内で用意しています。各モジュールの依存関係を解析し、現時点で安全に移行できるモジュールをリストアップする仕組みです。
XcodeGenでのMergeable Library設定
LINE iOSではXcodeGenを用いてXcodeプロジェクトを生成しています。Mergeable Libraryへの移行は、Project Spec(project.yml)の設定変更が中心です。
MergeableLibraryテンプレート
共通設定をXcodeGenのテンプレートとして定義しています。
MergeableLibrary:
settings:
base:
MACH_O_TYPE: mh_dylib
MERGEABLE_LIBRARY: YES
GENERATE_INFOPLIST_FILE: YES
PRODUCT_BUNDLE_IDENTIFIER: com.example.$(TARGET_NAME)モジュールをMergeable Libraryに変換するには、そのモジュールのproject.ymlにテンプレートを追加するだけです。
targets:
MyUtilityModule:
templates:
- Framework
- MergeableLibrary # この行を追加アプリターゲットの設定
メインアプリやApp Extensionなど、Mergeable Libraryをマージする側のターゲットにはMERGED_BINARY_TYPE: manualを設定します。
settings:
base:
MERGED_BINARY_TYPE: manualこの設定については後述する「落とし穴3」で重要な意味を持ちます。
落とし穴1: App ExtensionがReexportedBinariesを見つけられない
最初に遭遇したのは、Debugビルド時のApp Extensionに関する問題でした。App Extensionが実行時にDynamic Frameworkを検索するパス(LD_RUNPATH_SEARCH_PATHS)が正しく設定されていなかったのです。
前述の通り、Debugビルド時にMergeable Libraryはマージされず、バンドル内のReexportedBinaries/ディレクトリに配置されます。このディレクトリは、通常embedされたDynamic FrameworkがコピーされるFrameworks/ディレクトリと隣り合って存在しています。
MyApp.app/
├── MyApp (実行バイナリ)
├── Frameworks/
│ └── SomeDynamic.framework (通常のembedされたDynamic Framework)
├── ReexportedBinaries/
│ └── MyUtilityModule.framework (Mergeable Library: Debugビルド時にre-exportされる)
└── PlugIns/
└── MyExtension.appex/
├── MyExtension (Extension実行バイナリ)
├── Frameworks/
└── ReexportedBinaries/アプリケーションターゲットの場合、Xcodeのビルドシステム(swift-build)が自動的に@loader_path/ReexportedBinariesをLD_RUNPATH_SEARCH_PATHSに追加してくれるため、特に何もする必要はありません。
しかし、App Extensionターゲットにはこの自動追加が行われないことを確認しました。そのため、App ExtensionのProject Specに手動でLD_RUNPATH_SEARCH_PATHSを追加する必要がありました。
settings:
base:
LD_RUNPATH_SEARCH_PATHS:
- "$(inherited)"
- "@executable_path/ReexportedBinaries"
- "@executable_path/Frameworks"
- "@executable_path/../../ReexportedBinaries"
- "@executable_path/../../Frameworks"この設定がないと、Debugビルド時にApp Extensionが起動した際、ReexportedBinaries内のMergeable Libraryを見つけられずクラッシュします。
落とし穴2: Dynamic Linkでリソースバンドルが見つからなくなる
もう一つのDebugビルド時の問題として、リソースバンドルの解決があります。
LINE iOSではこれまでStatic Framework前提で開発してきました。Static Linkの場合、Bundle(for: ...)はアプリケーションのバンドルを返すため、アプリ内にembedされたリソースバンドルに問題なくアクセスできます。
しかし、Mergeable Library化によりDebugビルドがDynamic Linkになると、Bundle(for: ...)はそのモジュール自身のバンドルを返すようになります。モジュールバンドル内にはリソースバンドルが含まれていないため、リソースが見つけられなくなってしまうのです。
この問題に対しては、Bundle(for: ...)でリソースバンドルが見つからなかった場合のフォールバックとしてBundle.mainを探すようにする対応を行いました。これにより、Static Link(Release)でもDynamic Link(Debug)でも正しくリソースにアクセスできるようになりました。
落とし穴3: transitive dependency(推移的依存)のMergeable Libraryがマージされず消失する
Debugビルドでの問題を解決した後、さらにいくつかのユーティリティモジュールのMergeable Library化を進めました。するとReleaseビルドのベータ配布でApp Extensionがクラッシュする問題が発生しました。
原因は、WWDC2023のセッション『Meet mergeable libraries』でも言及されている、Mergeable Libraryの重要な制約でした。
The static linker only merges direct dependencies. So, to include more mergeable libraries, you should set them as explicit link dependencies.
Static Linkerは直接の依存関係(direct dependencies)のみをマージするのです。MERGED_BINARY_TYPE: manualが設定されたターゲットにとって、transitive dependencies(間接的な依存)の中にあるMergeable Libraryはマージ対象になりません。Releaseビルド時、マージされなかったMergeable Libraryはバンドルにembedされず、ReexportedBinariesへのエクスポートもされないため、ランタイムにはそもそも存在しない状態になり、シンボル解決に失敗してクラッシュします。
メインアプリ: Module Cをdirect dependencyとして宣言 → マージ成功

App Extension: Module Cがtransitive dependencyのまま → マージされず消失

メインアプリではModule Cをdirect dependencyとして明示的に宣言しているためマージされますが、App Extensionでは宣言がないためModule Cはマージもembedもされず、ランタイムに存在しない状態になります。
LINEアプリにはShare Extension、Notification Service Extensionなど複数のApp Extensionが存在し、それぞれが異なる依存グラフを持っています。メインアプリのターゲットでは問題なくマージされても、App Extensionのターゲットでは依存構造が異なるためマージされないケースが発生したのです。
対処: direct dependencyの明示的な宣言
この問題に対処するには、MERGED_BINARY_TYPE: manualを持つ全てのターゲットにおいて、transitive dependenciesに含まれるMergeable Libraryをdirect dependencyとして明示的に宣言する必要があります。
targets:
MyWidgetExtension:
settings:
base:
MERGED_BINARY_TYPE: manual
dependencies:
# Mergeable Libraryはdirect dependencyとして宣言し、
# MERGED_BINARY_TYPE: manual ターゲットに正しくマージされるようにする
- framework: MyUtilityModule.framework
implicit: trueなお、Static Framework時に設定していた既存の依存宣言のembed: falseフラグは削除する必要はありません。Debugビルド時、Mergeable LibraryはReexportedBinariesを通じて動的リンクされるため、embed設定の変更は不要です。
しかし、LINE iOSには数百を超えるターゲットが存在します。この設定を手動で管理し続けるのは現実的ではなく、自動検証ツールの開発につながりました。
direct dependency(直接依存)宣言漏れを防ぐ自動検証ツール
落とし穴3で述べた問題は、MERGED_BINARY_TYPE: manualを持つ全てのターゲットに対して、transitive dependenciesの中のMergeable Libraryを正しくdirect dependencyとして宣言し続けなければならないという点で、手動での管理が困難です。
そこで、Mergeable Libraryの依存関係を自動検証するLintツールを開発しました。前述の通りLINE iOSではXcodeGenを使っているため、全てのProject Spec(project.yml)を読み込み、各ターゲットのdependencies配列から依存グラフを構築・解析しています。このツールは、MERGED_BINARY_TYPE: manualが設定された全ターゲットを対象に、前述のdirect dependency宣言が漏れていないかを検証し、不足があればエラーとして報告します。
このツールはプロジェクト生成時に実行されるため、依存関係の不整合はCIはもちろん、開発者のローカル環境でも即座に検出されます。新しいモジュールの追加や依存関係の変更があった場合も、人間が見落とすことなく問題を検出できるようになりました。
なお、XcodeGenを使わずXcodeプロジェクトをソース管理にコミットして手動管理しているプロジェクトでも、XcodeProjのようなライブラリを使えば.xcodeprojから同様に依存グラフを解析し、自動検証ツールを構築することが可能です。
リバートの連続から学んだこと
今回の導入過程では、Mergeable Library化 → 問題発覚 → リバートというサイクルを複数回経験しました。大規模プロジェクトへの新技術導入は、一発で成功させることが難しいものです。重要なのは、各リバートから学びを得て、同じ問題を繰り返さない仕組み(今回は自動検証ツール)を構築すること。そして、この記事のように学びを言語化して共有することも、チームや社外の同じ課題に取り組む方々にとって価値があると考えています。
モジュール変換時に確認すべきポイント
自動検証ツールに加えて、モジュールを変換した際には以下の点を確認することをおすすめします。
- Debugビルドの確認: アプリがローカルで正常に起動・動作するか
- Releaseビルドの確認: ベータビルドが正常に作成・配布できるか(App Store Connectのバリデーションを含む)
- テストの実行: 対象モジュールのユニットテストが通るか
- Xcode Previewsの確認: 該当モジュールのプレビューが正しく動作するか
特にReleaseビルド(ベータビルド)の検証は重要です。Debugビルドでは問題がなくてもReleaseビルドで初めて顕在化する問題があります。
まとめ
Mergeable Libraryは、大規模iOSプロジェクトにおけるXcode Previewsの安定性やDebugビルドのリンク時間といった課題を解消しつつ、Releaseビルドのパフォーマンスも維持できる強力な仕組みです。しかし、実際に導入を進めると、ドキュメントだけでは読み取れない落とし穴がいくつも存在しました。
本記事で紹介した主な落とし穴をまとめます。
問題ビルド構成原因対処
LD_RUNPATH_SEARCH_PATHSの不一致DebugApp ExtensionにReexportedBinariesの検索パスが自動追加されない手動でLD_RUNPATH_SEARCH_PATHSを設定
リソースバンドルが見つからないDebugDynamic Link時にBundle(for:)がモジュールバンドルを返すBundle.mainへのフォールバック追加
App Extensionでフレームワークが消失ReleaseStatic Linkerがdirect dependenciesしかマージしないdirect dependencyの明示的宣言 + 自動検証ツール
大規模プロジェクトでのMergeable Library導入で特に強調したいのは以下の3点です。
- リーフからルートへ: 依存ツリーの末端から順に移行しないとシンボル重複が発生する。ツールで候補を機械的に特定すべき
- App Extensionに特に注意: direct dependencyの明示的宣言はMERGED_BINARY_TYPE: manualを持つ全てのターゲットに必要だが、メインアプリは多くのモジュールを直接依存として持つため問題が顕在化しにくい。App Extensionは依存グラフが異なるため表面化しやすく、加えてLD_RUNPATH_SEARCH_PATHSの手動設定もApp Extensionでのみ必要になる。依存関係の自動検証ツールの導入をおすすめする
- DebugとReleaseの両方で検証する: Debugビルドでは見えない問題、Releaseビルドでは見えない問題がそれぞれ存在する
現時点でLINE iOSでMergeable Library化が完了しているのはまだ数モジュールですが、自動検証ツールやマイグレーション候補の特定ツールといった基盤が整ったことで、今後は徐々に対象を広げていく予定です。
この記事が、Mergeable Libraryの導入を検討されている方の参考になれば幸いです。
関連記事
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み