はじめに
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のモジュール構造と移行戦略
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の不一致 | Debug | App ExtensionにReexportedBinariesの検索パスが自動追加されない | 手動でLD_RUNPATH_SEARCH_PATHSを設定 |
| リソースバンドルが見つからない | Debug | Dynamic Link時にBundle(for:)がモジュールバンドルを返す | Bundle.mainへのフォールバック追加 |
| App Extensionでフレームワークが消失 | Release | Static 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の導入を検討されている方の参考になれば幸いです。


