Xcodeのビルド時にアーキテクチャが異なるframeworkファイルを差し替える

[qnote Advent Calendar 2020](https://qiita.com/advent-calendar/2020/qnote) の2日目です。

とあるプロジェクトで使用している外部ライブラリが実機用とシミュレーター用の2つのframeworkファイルがありプロジェクトファイルには実機用がリンクされています。
実機用のファイルには実機のアーキテクチャが含まれているバイナリファイルがあり、シミュレータ用のファイルにはシミュレータのアーキテクチャがあり、実機のアーキテクチャにはシミュレータが含まれていないためシミュレーターで実行ができない状態になっているのでシミュレーターで実行ができるようにしたいと思います。
シミュレータで実行する際にビルド時にエラーになってしまうのでビルド前差し替えてビルド後に戻せば良いのではと思いましたのでこの方法で進んでみます。

#####frameworkのバイナリに含まれているアーキテクチャについて
詳しくは以下のサイトをご確認ください。
・https://qiita.com/fuwamaki/items/ce1cd438ec5dfd4ccb9a#simulator%E3%81%A7%E3%81%AF%E3%83%93%E3%83%AB%E3%83%89%E3%81%A7%E3%81%8D%E3%82%8B%E3%81%AE%E3%81%AB%E5%AE%9F%E6%A9%9F%E3%81%A0%E3%81%A8%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84
・https://blog.ymyzk.com/2015/12/ios-tvos-watchos-architectures/

###どのように差し替えるか
・Build PhaseのRun Script
・スキームのPre-actionsとPost-actions
のどちらかで差し替えをします。
調べると大体シェルスクリプトが出てきますがせっかくなのでSwiftでやっていこうと思います。
Run Scriptはドラッグ&ドロップで移動できるのでビルド前の方はCompile Sourcesより前に配置します。

#####注意点
どちらもビルドが失敗するとビルド後の処理が実行されないので万が一のことを考えてGit等で元に戻せるようにしておいた方が良いです。

###プロジェクトフォルダの状態
プロジェクトを新規作成してデフォルトでできる(プロジェクト名).xcodeprojファイルがあるディレクトリに
`(プロジェクト名)/SDK/hoge/lib/iphoneos/hoge.framework`
`(プロジェクト名)/SDK/hoge/lib/iphonesimulator/hoge.framework`
に各frameworkファイル(iphoneosが実機、iphonesimulatorがシミュレータ)と
`script/script.swift`
にスクリプト用Swiftファイルがある状態です。

###Swiftのファイルの実行
ビルド前

```
#ログを書き出して確認する場合は下の行を実行する
#exec > ${PROJECT_DIR}/script/prebuild.log 2>&1

/usr/bin/env xcrun --sdk macosx swift ${SRCROOT}/script/script.swift "pre" ${PROJECT_DIR} ${PLATFORM_NAME}
```
ビルド後

```
#ログを書き出して確認する場合は下の行を実行する
#exec > ${PROJECT_DIR}/script/postbuild.log 2>&1

/usr/bin/env xcrun --sdk macosx swift ${SRCROOT}/script/script.swift "post" ${PROJECT_DIR} ${PLATFORM_NAME}
```

preの部分がpostになっています。

script.swiftの中身は以下の内容になっています。

```script.swift
#!/usr/bin/swift

import Cocoa
import Foundation

let actionType = CommandLine.arguments[1]       // 引数1 "pre" または "post"
let projectDirectory = CommandLine.arguments[2] // 引数2 ${PROJECT_DIR}
let platform = CommandLine.arguments[3]         // 引数3 ${PLATFORM_NAME}

let frameworkFileName = "hoge.framework"
let tempFrameworkFileName = "temp_\(frameworkFileName)"
let libraryDirectory = "\(projectDirectory)/(プロジェクト名)/SDK/hoge/lib/"
let iphoneFrameworkDirectory = "\(libraryDirectory)/iphoneos/"
let iphoneOriginalPath = "\(iphoneFrameworkDirectory)\(frameworkFileName)"
let iphoneTempPath = "\(iphoneFrameworkDirectory)\(tempFrameworkFileName)"
let simulatorFrameworkDirectory = "\(libraryDirectory)/iphonesimulator/"
let simulatorOriginalPath = "\(simulatorFrameworkDirectory)\(frameworkFileName)"

class Script {
    
    var isSimulator: Bool {
        return platform == "iphonesimulator"
    }
    
    func preAction() {
        reverseActionIfNeeded()
        guard isSimulator else {
            return
        }
        let _ = rename(iphoneFrameworkDirectory, oldName: frameworkFileName, newName: tempFrameworkFileName)
        let _ = copy(simulatorOriginalPath, toPathName: iphoneOriginalPath)
    }
    
    func postAction() {
        guard isSimulator else {
            return
        }
        let _ = remove(iphoneOriginalPath)
        let _ = rename(iphoneFrameworkDirectory, oldName: tempFrameworkFileName, newName: frameworkFileName)
    }
    
    func reverseActionIfNeeded() {
        guard FileManager.default.fileExists(atPath: iphoneTempPath) else {
            return
        }
        let _ = remove(iphoneOriginalPath)
        let _ = rename(iphoneFrameworkDirectory, oldName: tempFrameworkFileName, newName: frameworkFileName)
    }
    
    func copy(_ atPathName: String, toPathName: String) -> Bool {
        do {
            try FileManager.default.copyItem(atPath: atPathName, toPath: toPathName)
        } catch {
            print(error)
            return false
        }
        return true
    }
    
    func rename(_ pathName: String, oldName: String, newName: String) -> Bool {
        let atPathName = "\(pathName)/\(oldName)"
        let toPathName = "\(pathName)/\(newName)"
        do {
            try FileManager.default.moveItem(atPath: atPathName, toPath: toPathName)
        } catch {
            return false
        }
        return true
    }
    
    func remove(_ pathName: String) -> Bool {
        do {
            try FileManager.default.removeItem(atPath: pathName)
        } catch {
            return false
        }
        return true
    }
}

if actionType == "pre" {
    Script().preAction()
} else if actionType == "post" {
    Script().postAction()
}
```

大まかに動作内容を解説します。
ビルド前のpreAction()でシミュレータの場合に
`iphoneos/hoge.framework`を`iphoneos/temp_hoge.framework`にリネームして
`iphonesimulator/hoge.framework`を`iphoneos/hoge.framework`にコピーします。
これでプロジェクトファイルにリンクされている実機用のhoge.frameworkをシミュレータ用のファイルと入れ替えることができ、ビルドが成功するようになります。
ビルド後のpostAction()にて
`iphoneos/hoge.framework`を削除して
`iphoneos/temp_hoge.framework`を`iphoneos/hoge.framework`にリネームしています。
これで元の状態に戻ります。

注意点でも記載しましたがビルドに失敗した場合にpostAction()が実行されないので
preAction()のreverseActionIfNeeded()にて`iphoneos/temp_hoge.framework`が存在していた場合`iphoneos/hoge.framework`がシミュレータ用のままなので最初に元の状態に戻してから処理を行うようにしています。

参考
https://qiita.com/kuluna/items/2225298e8a9a7b3b3ef6


中々こういう状況にはならないかと思いますが、もし誰かが同じことで困った時の助けになればと思います。