新規 Xcode project を自動生成する技術

## 概要

Xcode プロジェクトをプログラムから自動生成したいと思ったことはありますか？
私はあります。
しかも **Xcode がやるのとまったく同じように動き、Xcode バージョンによって微妙に異なるプロジェクトテンプレートを出し分けたり**もしたいです。
ローカルのマシンだけでなく、**CI上でも動けば完璧**ですね。

ご存知の通り、手動で新規で Xcode プロジェクトを作るには

1. Xcode.app を開く
2. メニューバーの `File` → `New` → `Project...` と辿る、もしくは単に `Shift + ⌘ + n` を押す
3. プロジェクトテンプレートを選ぶ
4. 色々聞かれるので入力する

などと結構手間がかかります。
また、何らかの理由でプログラムからプロジェクトを自動生成したいときにはこの方法ではうまくいきません。
にもかかわらず、Xcode そのものや、また Xcode に同梱されている `xcodebuild` などのコマンドラインツールにはプロジェクト作成のインターフェースが用意されていません。

世の中には自分以外にも同じような悩みを持った人がいるようで、調べるうちに色々な方法があることがわかりました。

## 色々な方法たち

### XcodeGen

https://github.com/yonaskolb/XcodeGen

Cookpad さんの [XcodeGenによる新時代のiOSプロジェクト管理](https://techlife.cookpad.com/entry/2019/04/26/110000) で紹介されていたので調べる前から知ってはいました。
これは一見良さそうなのですが、 **Xcode がやるのとまったく同じように動**かすためには手動で YAML を頑張って書く必要があります。
しかも Xcode がつくるテンプレートファイルなどは作ってくれないので、これも自力で書かねばなりません。

したがって今回の目的には適いませんでした。

### Xgen

https://github.com/JohnSundell/Xgen

ライブラリなので直接コマンドラインから実行はできませんが、以下のような Swift スクリプトから簡単に呼び出すことができます。

```swift:main.swift
import Xgen

let workspace = Workspace(path: "~/MyWorkspace")
workspace.addProject(at: "~/MyProject.xcodeproj")
try workspace.generate()
```

ちなみに作者は [Swift by Sundell](https://www.swiftbysundell.com) という Podcast を運営している方です。

仕組みは単純で、ハードコードされた XML Property List を書き出しているだけです。

この辺→ https://github.com/JohnSundell/Xgen/blob/444b9ad8f9c7720578fc46dbd09e0487eac7ddb5/Sources/Workspace.swift#L68-L88

Workspace か Playground ファイルを書き出すだけなので、Xcode から操作したときのようにプロジェクトテンプレートができるわけではありません。

私は **Xcode がやるのとまったく同じように動く** ものを求めているので、これもやはり違いそうです。

### Apple Events

既存ツールが使えないなら自分で書くしかありません。
幸い Xcode は OSA に準拠しているので、 AppleScript や JavaScript から操作することができます。
OSA を JavaScript で書くのはドキュメントの不足からそれなりに大変なので、手軽にかける AppleScript で試すことにしました。

Single View iOS App 向けのスクリプトを書いたのですが、長いのでリンクを貼ります。
https://github.com/manicmaniac/automate-xcode-project-creation/blob/ac30b3ee50149494c1b8d3e3dbd533bf7b4f7835/create-project.applescript

コードは見ての通り、

> 1. Xcode.app を開く
> 2. メニューバーの `File` → `New` → `Project...` と辿る、もしくは単に `Shift + ⌘ + n` を押す
> 3. プロジェクトテンプレートを選ぶ
> 4. 色々聞かれるので入力する

のように手動でやるときの作業をそのままコードにして、適宜ウェイトを入れたりしているだけです。
AppleScript の標準コーディング規約がタブインデントなこともあって、めちゃくちゃ横に長くて読みにくいですね。

```applescript:create-project.applescript#L46-L49
tell application "System Events"
  tell application process "Xcode"
    set frontmost to true
    click menu item "Project…" of menu 1 of menu item "New" of menu 1 of menu bar item "File" of menu bar 1
```

これはきちんと**Xcode がやるのとまったく同じように動**きます。
Xcode バージョンの変更にもしっかり追従するので、要件を満たせました。

一件落着かと思いきや、実は**このスクリプトは CI 上では動きません**。
なぜかというと、

1. Apple Events へのアクセスを有効にするには OS が出すダイアログに対して許可をする必要があり、これはプログラムからは操作できない
2. OS が出すダイアログを非表示にするには、あらかじめシステム環境設定から許可しておく必要があり、これはプログラムからは操作できない
3. システム環境設定を CLI から操作するには、システム環境設定が保存されているファイル(tcc.db)を直接操作する必要があり、これは SIP で保護されている
4. SIP を無効にするには OS をセーフモードでリブートする必要があり、これは CI では通常許可されていない

というような感じで、散々[ヤクの毛刈り](https://ja.wiktionary.org/wiki/yak_shaving)をした後に不可能ということが判明します。辛いですね。

では、結局最初の要件 **Xcode がやるのとまったく同じように動き、Xcode バージョンによって微妙に異なるプロジェクトテンプレートを出し分けたり、CI上でも動**くような方法はないのでしょうか。

実は、あるんです。

### Xcode のプライベートフレームワークを使う

ご存知の通り、Xcode は複数のプライベートフレームワークから構成されているアプリケーションで、主要な機能の多くがプライベートフレームワーク経由でアクセス可能です。

例えば、今回利用しているプロジェクトテンプレートを吐き出す機能は、以下のフレームワークの機能の一部として実装されています。

- `/Applications/Xcode.app/Contents/Frameworks/IDEFoundation.framework`
- `/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework`

したがって、このプライベートフレームワークを用いてプロジェクトを作成する CLI アプリケーションを作成し、その CLI アプリケーションを実行することで、**Xcode がやるのとまったく同じように動**かすことができそうです。
また、このやり方なら Apple Events を用いないので、CI でも動かすことができますね。

というわけで、[xcnew](https://github.com/manicmaniac/xcnew) という CLI アプリケーションを実装しました。
手動で Xcode プロジェクトを作成するときにいろいろと入力する部分はコマンドラインオプションとして渡すようにしています。

```
    xcnew - A command line tool to create Xcode project.
    
    Usage: xcnew [-h|-v] [-n ORG_NAME] [-i ORG_ID] [-tuco] <PRODUCT_NAME> [OUTPUT_DIR]
    
    Options:
        -h, --help                     Show this help and exit
        -v, --version                  Show version and exit
        -n, --organization-name        Specify organization's name
        -i, --organization-identifier  Specify organization's identifier
        -t, --has-unit-tests           Enable unit tests
        -u, --has-ui-tests             Enable UI tests
        -c, --use-core-data            Enable Core Data template
        -o, --objc                     Use Objective-C (default: Swift)
    
    Arguments:
        <PRODUCT_NAME>                 Required TARGET_NAME of project.pbxproj
        [OUTPUT_DIR]                   Optional directory name of the project
```

作成にあたっては上述のプライベートフレームワークをリバースエンジニアリングする必要がありなかなか難航しましたが、無事 `xcnew` コマンドを使って要件を満たすことができました。

プライベートフレームワークの使い方を調べて、それを利用する方法は[別途記事にする](https://qiita.com/manicmaniac/items/23a2d7b10b1cb9ded4f3)ことにして、この記事はいったん目的を達したところでおしまいです。