(2018/01/28 追記)
書いたときから2年半たってXcodeのバージョンも2つあがったけどまだちょこちょこストックしてもらえているので、
Xcode9でのproject.pbxprojと照らし合わせて、記事の内容が古くなっているところ確認しました。
結果、特に構造が変わったところはなかったのでXcode9.1 (9.2じゃなくて申し訳ない)時代でも安心してお読み下さい。
(2019/10/05 追記)
Xcode 11 GM seedで構造に変化がないことを確認しました
(追記おわり)
Xcodeプロジェクトのブラックボックス、{プロジェクト名}.xcodeproj/project.pbxproj
の中身を読んでみたら意外と読めた。
これで明日からはもう、コンフリクトしてもプロジェクト設定がなんか変になっても怖くない。
サマリーになってないサマリー
図解って難しい。
以下、順を追って解読。
準備
解読対象として、新規にXcodeでプロジェクトを生成してまっさらなproject.pbxprojファイルを作る。
項目 | 設定値 |
---|---|
Product Name | Example |
Organization Name | yokomotod |
Organization Identifier | com.yokomoto |
Language | Swift |
Device | Universal |
Use Core Data | No |
Include Unit Tests | Yes |
Include UI Tests | Yes |
ファイル全体はgistにおいた。Example.xcodeproj/project.pbxproj
(2018/1/28 gistの内容をXcode9.1でのものに更新しました。revesion見るとおもしろい)
(2019/10/05 gistの内容をXcode11 GM seedのものに更新しました)
アウトライン
予想以上に長い記事になってしまったので、以降の流れを書いてみる。
- まえがき
- サマリ
- 準備
- アウトライン ・・・いまここ
- 前半
- ファイルの構造を解読する
- ファイルの構造を解読する
- フォーマット
- objects 要素の中身
- rootObject
- リンク構造
- リレーション関係 全体図
- ファイルの構造を解読する
- 後半
- 各クラスの中身の解読
- ファイルやディレクトリを表すクラス
- PBXGroup
- PBXFileReference
- PBXVariantGroup
- ターゲットを表すクラス
- PBXNativeTarget
- PBXTargetDependency
- PBXContainerItemProxy
- Build Settingsタブを表すクラス
- XCConfigurationList
- XCBuildConfiguration
- Build Phasesタブを表すクラス
- PBXSourcesBuildPhase
- PBXFrameworksBuildPhase
- PBXResourcesBuildPhase
- PBXBuildFile
- ファイルやディレクトリを表すクラス
- 各クラスの中身の解読
ざっくり、前半と後半の2章立て。
ファイルの構造を解読する
まず前半。project.pbxproj
がどういう形式で表現されたファイルなのか見ていく。
フォーマット
objects
の要素が鬼のように長くてぱっと見はごちゃごちゃしているけど、よく見たらJSONに近い感じのフォーマットで、要素が5つだけのDictionary。
{
archiveVersion = 1;
classes = {};
objectVersion = 46;
objects = {
//// このセクションがファイル内容のほとんどを占める ///
}
rootObject = 9D6B60051BC4FC8A0034855E /* Project object */;
}
objects 要素の中身
objectsの中身は
9D6B60111BC4FC8A0034855E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D6B60101BC4FC8A0034855E /* AppDelegate.swift */; };
9D6B60131BC4FC8A0034855E /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D6B60121BC4FC8A0034855E /* ViewController.swift */; };
のようなレコードが延々続いている。↑だと1行に折りたたまれてるけど、内容としては
<id> = {
<key> = <value>;
<key> = <value>;
....
};
...
となっていて第1階層とおなじフォーマットで入れ子なだけ。
/*
*/
で囲まれているところはコメント。
9D6B60111BC4FC8A0034855E
とかは、あるオブジェクトを指すユニークなキー。こいつらがときどきガッツリ変わりやがるから・・・。
isa
要素は大抵どのオブジェクトにも付いていて、そのオブジェクトのタイプを表している。(例:isa = PBXBuildFile
)
isa
属性ごとにセクション分けされていて、わかりやすくコメントもついてる。
生成直後のプロジェクトだと下のような構成。
objects = {
/* Begin PBXBuildFile section */
...
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
...
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
...
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
...
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
...
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
...
/* End PBXNativeTarget section */
/* Begin PBXProject section */
...
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
...
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
...
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
...
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
...
/* End PBXVariantGroup section */
...
/* Begin XCBuildConfiguration section */
...
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
...
/* End XCConfigurationList section */
}
rootObject
ファイルの一番末尾、第1階層の最後の要素rootObject
が.pbxprojの構造の起点
{
archiveVersion = 1;
classes = {};
objectVersion = 46;
objects = {
...
}
rootObject = 9D6B60051BC4FC8A0034855E /* Project object */;
}
この9D6B60051BC4FC8A0034855E
が大量にあるobjects
下の要素の1つを指している。
(↓細かい内容は流し読み可)
9D6B60051BC4FC8A0034855E /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 0700;
ORGANIZATIONNAME = example.com;
TargetAttributes = {
9D6B600C1BC4FC8A0034855E = {
CreatedOnToolsVersion = 7.0.1;
};
9D6B60201BC4FC8A0034855E = {
CreatedOnToolsVersion = 7.0.1;
TestTargetID = 9D6B600C1BC4FC8A0034855E;
};
9D6B602B1BC4FC8A0034855E = {
CreatedOnToolsVersion = 7.0.1;
TestTargetID = 9D6B600C1BC4FC8A0034855E;
};
};
};
buildConfigurationList = 9D6B60081BC4FC8A0034855E /* Build configuration list for PBXProject "Example" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 9D6B60041BC4FC8A0034855E;
productRefGroup = 9D6B600E1BC4FC8A0034855E /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
9D6B600C1BC4FC8A0034855E /* Example */,
9D6B60201BC4FC8A0034855E /* ExampleTests */,
9D6B602B1BC4FC8A0034855E /* ExampleUITests */,
);
};
このPBXProjectクラスのオブジェクトがプロジェクト設定の大元。
ORGANIZATIONNAME = example.com;
とか developmentRegion = English;
のようなシンプルな設定項目がありつつ、
buildConfigurationList
や mainGroup
、productRefGroup
、targets
などはさらに別のオブジェクトを指してるのがわかる。
リンク構造
本来なんていうのかわからないけど、以降、便宜的にこういう他のオブジェクトのユニークキーを指してるのをここでは「リンク」と仮称。
たとえば
buildConfigurationList = 9D6B60081BC4FC8A0034855E /* Build configuration list for PBXProject "Example" */;
のリンクが指す先を探すと
/* Begin XCConfigurationList section */
9D6B60081BC4FC8A0034855E /* Build configuration list for PBXProject "Example" */ = {
isa = XCConfigurationList;
buildConfigurations = (
9D6B60331BC4FC8A0034855E /* Debug */,
9D6B60341BC4FC8A0034855E /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
さらに2つのコンフィギュレーション(Debug用とRelease用)へのリンクになっているのがわかる。
なんとなくツリー構造っぽいけど、複数から非リンクしてるオブジェクトもあったりするのでツリーというわけでもない。
リレーション関係 全体図
そんな感じで各オブジェクトのリンクを辿っていくと
冒頭の図のリンク構造になってる。
勝手な分類
ファイル全体がだいたいどんな構造しているかがわかったので、今度は中身。
プロジェクト生成直後の.pbxprojに含まれていたクラスを、個人の主観で分類してみた。
-
プロジェクトを表すクラス (rootObject)
- PBXProject
-
ファイルやディレクトリを表すクラス
- PBXGroup
- PBXFileReference
- PBXVariantGroup
-
ターゲットを表すクラス
- PBXNativeTarget
- PBXTargetDependency
- PBXContainerItemProxy
-
Build Settingsタブを表すクラス
- XCConfigurationList
- XCBuildConfiguration
-
Build Phasesタブを表すクラス
- PBXSourcesBuildPhase
- PBXFrameworksBuildPhase
- PBXResourcesBuildPhase
- PBXBuildFile
飽くまで個人的な主観にもとづく分類。
次はこれらの中身を見ていく。
各クラスの中身の解読
後半。各クラスの詳細。
PBXProject
プロジェクトを表すクラス
project.pbxproj
のルートオブジェクト。前半の rootObjectの項で見た通り、これを起点にすべてのオブジェクトがぶら下がる。
ORGANIZATIONNAME = example.com;
compatibilityVersion = "Xcode 3.2";
なんかはXcode上からも見える
しかし
developmentRegion = English;
とかはXcode上には表示もされなければ編集もできないんだろうか・・・?
PBXGroup, PBXFireReference, PBXVariantGroup
ファイルやディレクトリを表すクラス。
PBXGroup
Xcodeはファイルシステムとは独立に論理的なファイルのグループが作れたりするアレを表現しているクラスがPBXGroup
。
グループに属す中身を格納するchildren
要素を持っていて、その中にはまたPBXGroup
, PBXFireReference
, PBXVariantGroup
とかのオブジェクトが入る。
PBXProject
オブジェクトの中に
mainGroup = 9D6B60041BC4FC8A0034855E;
という名前でルートのグループが格納されていて
9D6B60041BC4FC8A0034855E = {
isa = PBXGroup;
children = (
9D6B600F1BC4FC8A0034855E /* Example */, ・・・PBXGroup
9D6B60241BC4FC8A0034855E /* ExampleTests */, ・・・PBXGroup
9D6B602F1BC4FC8A0034855E /* ExampleUITests */, ・・・PBXGroup
9D6B600E1BC4FC8A0034855E /* Products */, ・・・PBXGroup
);
sourceTree = "<group>";
};
この中のExampleグループを更に見ると
9D6B600F1BC4FC8A0034855E /* Example */ = {
isa = PBXGroup;
children = (
9D6B60101BC4FC8A0034855E /* AppDelegate.swift */, ・・・PBXFireReference
9D6B60121BC4FC8A0034855E /* ViewController.swift */, ・・・PBXFireReference
9D6B60141BC4FC8A0034855E /* Main.storyboard */, ・・・PBXFireReference
9D6B60171BC4FC8A0034855E /* Assets.xcassets */, ・・・PBXFireReference
9D6B60191BC4FC8A0034855E /* LaunchScreen.storyboard */, ・・・PBXVariantGroup
9D6B601C1BC4FC8A0034855E /* Info.plist */, ・・・PBXFireReference
);
path = Example;
sourceTree = "<group>";
};
こんな感じで
Xcode上から見たファイルリストを表してるのが見て取れる。
論理グループとファイルシステムは綺麗に対応付けるのがベストプラクティスということになってると思うので、
常に
path = ディレクトリ名;
sourceTree = "<group>";
になるように気をつければ良いはず。path = "xxx/yyy/zzz/ディレクトリ名"
とかになってると変な取り込み方をしている可能性大。
PBXFireReference
↑でも出てきているように、
ソースコードはもちろん、frameworkファイルやプロダクトファイル(***.a
なアレ)などファイルシステム上のすべてのファイルは表現しているクラス。
9D6B60101BC4FC8A0034855E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
敢えて改行はせずに、1ファイル1行になるようにしてくれている。
これも常に
path = ファイル名; sourceTree = "<group>";
になるはず。ただし次に出てくるPBXVariantGroup
絡みのファイルだけは別。
PBXVariantGroup
PBXVariantGroup
は、Localizationされているファイルを表現するクラスで、実際は日本語版・英語版のファイルあるのに、Xcode上で見ると1つの.strings
ファイル、みたいなアレ。
9D6B60141BC4FC8A0034855E /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
9D6B60151BC4FC8A0034855E /* Base */, ・・・PBXFileReference
);
name = Main.storyboard;
sourceTree = "<group>";
};
LocalizationでBaseを持っていて、対応するファイルは
9D6B60151BC4FC8A0034855E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
ローカライズされていると言語.lproj/
下に置かれて、path = Base.lproj/Main.storyboard;
となるらしい。
こうなっているせいで、一度間違ってローカライズを解除してしまったり、ローカライズされたファイルを移動させてしまうと元に戻すのに死ぬ思いをさせられる。
Xcode上から復旧するの無理なんじゃないだろうかアレ。
解読できた今なら、.pbxprojファイルを直接編集してスパっと復旧できること間違いなし。
PBXNativeTarget, PBXTargetDependency, PBXContainerItemProxy
ターゲットや、ターゲット間の依存関係を定義するクラス
PBXNativeTarget
ターゲットを表すクラス。
PBXProjectオブジェクトに格納されてる。
targets = (
9D6B600C1BC4FC8A0034855E /* Example */,
9D6B60201BC4FC8A0034855E /* ExampleTests */,
9D6B602B1BC4FC8A0034855E /* ExampleUITests */,
);
リンク先は
9D6B600C1BC4FC8A0034855E /* Example */ = {
isa = PBXNativeTarget;
buildConfigurationList = 9D6B60351BC4FC8A0034855E /* Build configuration list for PBXNativeTarget "Example" */;
buildPhases = (
9D6B60091BC4FC8A0034855E /* Sources */, ・・・
9D6B600A1BC4FC8A0034855E /* Frameworks */,
9D6B600B1BC4FC8A0034855E /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = Example;
productName = Example;
productReference = 9D6B600D1BC4FC8A0034855E /* Example.app */;
productType = "com.apple.product-type.application";
};
PBXProjectと同じくbuildConfigurationList
としてXCConfigurationList
オブジェクトを持っている。
(Build Settigsはプロジェクト設定、ターゲット設定両方ある)
一方で、buildPhases
はPBXProjectにはなかった項目。
(Build Phasesはターゲットにだけあってプロジェクトにはない)
PBXTargetDependency
おそらく、Build PhaseのTarget Dependenciesの元情報
↑の例にしたExample
はメインのターゲットなのでdependencies
が空。
単体テストターゲットやUIテストターゲットは、メインのターゲットに依存する設定になってる。
9D6B60201BC4FC8A0034855E /* ExampleTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 9D6B60381BC4FC8A0034855E /* Build configuration list for PBXNativeTarget "ExampleTests" */;
buildPhases = (
9D6B601D1BC4FC8A0034855E /* Sources */,
9D6B601E1BC4FC8A0034855E /* Frameworks */,
9D6B601F1BC4FC8A0034855E /* Resources */,
);
buildRules = (
);
dependencies = (
9D6B60231BC4FC8A0034855E /* PBXTargetDependency */,
);
name = ExampleTests;
productName = ExampleTests;
productReference = 9D6B60211BC4FC8A0034855E /* ExampleTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
dependenciesのリンク先のPBXTargetDependency
が依存の情報を保持している。
9D6B60231BC4FC8A0034855E /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 9D6B600C1BC4FC8A0034855E /* Example */;
targetProxy = 9D6B60221BC4FC8A0034855E /* PBXContainerItemProxy */;
};
よくわからないのが、target = 9D6B600C1BC4FC8A0034855E /* Example */;
だけで依存先の情報は十分な気がするのに、
謎のtargetProxy = 9D6B60221BC4FC8A0034855E /* PBXContainerItemProxy */;
という項目も存在している。次項。
PBXContainerItemProxy
謎。↑のPBXTargetDependency
からtargetProxy
として参照されてるけど、
存在意義がわからない。
9D6B60221BC4FC8A0034855E /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9D6B60051BC4FC8A0034855E /* Project object */; ・・・ PBXProjext:rootObject
proxyType = 1;
remoteGlobalIDString = 9D6B600C1BC4FC8A0034855E; ・・・ PBXNativeTarget:Example
remoteInfo = Example;
};
これがなくても、依存の情報は揃っている気がする・・・。
XCConfigurationList, XCBuildConfiguration
Build Settingsの項目を表すクラス。
PBXProject, PBXNativeTargetそれぞれが持っていて、合計4つ(プロジェクト, Exampleターゲット, ExampleTestsターゲット, ExampleUITestsターゲット)のXCConfigurationList
がある。
さらにデフォルトでDebugとReleaseの2つのコンフィギュレーションが用意されるので、合計8つ(4 x 2 = 8)のXCBuildConfiguration
がある。
同じような記述が何回も繰り返し出てくることになって、コンフリクトしたときとかによく混乱させてくれる。
XCConfigurationList
プロジェクトの分。
9D6B60081BC4FC8A0034855E /* Build configuration list for PBXProject "Example" */ = {
isa = XCConfigurationList;
buildConfigurations = (
9D6B60331BC4FC8A0034855E /* Debug */,
9D6B60341BC4FC8A0034855E /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
Example
ターゲットの分。
9D6B60351BC4FC8A0034855E /* Build configuration list for PBXNativeTarget "Example" */ = {
isa = XCConfigurationList;
buildConfigurations = (
9D6B60361BC4FC8A0034855E /* Debug */,
9D6B60371BC4FC8A0034855E /* Release */,
);
defaultConfigurationIsVisible = 0;
};
プロジェクトのでもターゲットのでもフォーマットは同じ。
Debug/ReleaseそれぞれのXCBuildConfiguration
を格納している。
XCBuildConfiguration
Build Settings欄で見慣れた設定値たち。
9D6B60331BC4FC8A0034855E /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
TARGETED_DEVICE_FAMILY = "1,2";
...省略...
};
name = Debug;
};
同じ名前の設定項目が何個もあってどれを変えるのが正しいのかわからなかったのは過去の話。
これからは、必要に応じてターゲットオブジェクトから辿りつつ、目的のXCBuildConfiguration
オブジェクトを探せばOK。
PBXSourcesBuildPhase, PBXFrameworksBuildPhase, PBXResourcesBuildPhase と PBXBuildFile
Build Phasesのうち、Target Dependencies以外の内容を表すクラス
プロジェクト生成直後だとこの3つだけしかないけど、CocoaPodsとか使うとPBXScriptBuildPhase
が増えたり、他にもまだありそう。
8つもあったXCBuildConfiguration
と違って、
- プロジェクトはBuild Phasesの設定は持たない
- Debug/Releaseとかのコンフィギュレーションにも依らない
ので、ターゲットの数=3つずつオブジェクトが存在する。だいぶ少ない気分になれる。
PBXSourcesBuildPhase
ビルドに含めるソース。
こんな感じで各ターゲット分で3つ。
/* Begin PBXSourcesBuildPhase section */
9D6B60091BC4FC8A0034855E /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9D6B60131BC4FC8A0034855E /* ViewController.swift in Sources */, ・・・PBXBuildFile
9D6B60111BC4FC8A0034855E /* AppDelegate.swift in Sources */, ・・・PBXBuildFile
);
runOnlyForDeploymentPostprocessing = 0;
};
9D6B601D1BC4FC8A0034855E /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9D6B60261BC4FC8A0034855E /* ExampleTests.swift in Sources */, ・・・PBXBuildFile
);
runOnlyForDeploymentPostprocessing = 0;
};
9D6B60281BC4FC8A0034855E /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9D6B60311BC4FC8A0034855E /* ExampleUITests.swift in Sources */, ・・・PBXBuildFile
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
Testsターゲット用のソースをまちがってメインターゲットに取り込んでしまう、などのミスがないように気をつける。
他のPhaseでも同様。
PBXFrameworksBuildPhase
ビルドに含めるframework。
プロジェクト生成当初はどのターゲットもframeworkは空だった。
9D6B600A1BC4FC8A0034855E /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
未確認だけど、なんかXcode8あたりからframeworkをわざわざ指定しなくても自動で取り込んでくれるようになったとかそんなニュースがあったような・・・?
PBXResourcesBuildPhase
ビルドする際に取り込むリソース。アプリのバイナリにバンドルされる(はず)
9D6B600B1BC4FC8A0034855E /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9D6B601B1BC4FC8A0034855E /* LaunchScreen.storyboard in Resources */, ・・・PBXBuildFile
9D6B60181BC4FC8A0034855E /* Assets.xcassets in Resources */, ・・・PBXBuildFile
9D6B60161BC4FC8A0034855E /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
注意事項として、Info.plist
はリソースとして取り込む必要がない。
一度Info.plist
をXcodeから論理削除してもう一度取り込み直すときに、ターゲットに含めるチェックボックスをオンにしたりしてると意図せずここに含まれてしまうことがよくある気がする。
PBXBuildFile
○○BuildPhaseはこのクラスのオブジェクトを経由して、ビルドするソースやリソースのPBXFileReferenceやPBXVariantGroupを参照している。
なんで中間が必要なのかはわからないけど。
9D6B60111BC4FC8A0034855E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D6B60101BC4FC8A0034855E /* AppDelegate.swift */; }; ・・・ PBXFileReference
9D6B60161BC4FC8A0034855E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9D6B60141BC4FC8A0034855E /* Main.storyboard */; }; ・・・PBXVariantGroup
おわり
以上!
最後にもう一度全体図を見ると・・・なんとなくイメージが伝わるようになっていると嬉しいです。
長文になりましたが、ここまで読んで頂いた奇特な方、どうもありがとうございます。
これでgit diff
したときもソースレビューのときも、.pbxprojファイルを読み飛ばさずにしっかりチェックできるはず。
プロジェクト設定がなんかカオスな感じにおかしくなってるプロジェクトを引き継いだって怖くない。