Timers Tech Blog

カップル専用アプリ「Pairy」, 子育て夫婦アプリ「Famm」を運営している Timers inc. の公式Tech Blogです。

Carthage、CocoaPods経由で導入したライブラリのライセンス一覧をまとめてplist形式で出力するSwift Script

Timers Advent Calendar 2016の1日目、iOSエンジニアのすーです!
まさかのトップバッターで驚いています。
最近自転車通勤するのがつらくなってきました。。。

FammでもCarthageで導入しているライブラリが増えてきて、CocoaPodsで入れているライブラリも少なくなってきました。
しかし、Carthageで入れたライブラリはCocoaPodsと違って、ライセンスの一覧をplist形式で書き出すものがなく、またCococaPodsのライブラリと合わせて一覧としてplistに書き出すとなると難しいです。
そこで、Carthage、CocoaPods両方で導入したライブラリをCocoaPodsのplist形式での出力と同じようにライセンスの一覧を出力するScriptを Swift で書いてみました。
(実はもうscript作成して運用して約半年くらい経過してて、今回書くことを決意しました。笑)

目標

scriptを実行することで、CocoaPods、Carthageで入れたライブラリのライセンスファイルを抽出し、CocoaPodsでこちらの方法で作成することができる acknowledgement.plist と同じ形式のplistファイルを作成するのを目標にします。
ちなみに、acknowledgement.plist は以下のような形式のplistファイルになっています。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>PreferenceSpecifiers</key>
        <array>
                <dict>
                        <key>FooterText</key>
                        <string>This application makes use of the following third party libraries:</string>
                        <key>Title</key>
                        <string>Acknowledgements</string>
                        <key>Type</key>
                        <string>PSGroupSpecifier</string>
                </dict>
                <dict>
                        <key>FooterText</key>
                        <string>ライセンス文章</string>
                        <key>Title</key>
                        <string>ライブラリ名</string>
                        <key>Type</key>
                        <string>PSGroupSpecifier</string>
                </dict>
                ...(以降ライブラリの一覧が<dict></dict>で続く
        </array>
        <key>StringsTable</key>
        <string>Acknowledgements</string>
        <key>Title</key>
        <string>Acknowledgements</string>
</dict>
</plist>

実装してみる

こんな感じで実装してみました。

(ここにコードをべた張りすると長くなるので、Gistのリンクを貼っておきます!)

使い方

使い方としては、

./acknowledgement_generator.swift <プロジェクトのrootディレクトリ> <出力先>

としてあげると、プロジェクトのrootディレクトリを起点として、CocoaPods/Carthageのライブラリのライセンスをかき集めてplistを生成します。

project
├── Carthage
├── Pods
├── project
│   ├── AppDelegate.swift
│   ├── Assets.xcassets
│   │   └── AppIcon.appiconset
│   │       └── Contents.json
│   ├── Base.lproj
│   │   ├── LaunchScreen.storyboard
│   │   └── Main.storyboard
│   ├── Info.plist
│   ├── Resources
│   └── ViewController.swift
├── project.xcodeproj
│   ├── project.pbxproj
│   ├── project.xcworkspace
│   │   ├── contents.xcworkspacedata
│   │   └── xcuserdata
│   │       └── Kishimoto.xcuserdatad
│   │           └── UserInterfaceState.xcuserstate
│   └── xcuserdata
│       └── Kishimoto.xcuserdatad
│           └── xcschemes
│               ├── project.xcscheme
│               └── xcschememanagement.plist
├── projectTests
│   ├── Info.plist
│   └── projectTests.swift
└── script
    └── acknowledgement_generator.swift

こんな感じのディレクトリ構造でしたら、この構造のroot directoryにて、

$ cd /path/to/project
$ ./script/acknowledgement_generator.swift . ./project/Resources/Acknowledgements.plist

とすると、 ./project/Resources 内にplistが生成されます。

ちょっとした解説

実行時の引数を取得する

実行時の引数は、 CommandLine.arguments で簡単に取得することができます。
ただ、0番目にscriptの実行pathが渡ってくるので、不要なら以下のようにしてdropさせると、純粋に引数だけ取得できます。

var args = Array(CommandLine.arguments.dropFirst())

また、今回は --help--verboseといったオプションも試しに搭載してみたので、こんな感じにargsに該当するオプションがあるか判定してそれぞれ必要な処理を実行しています。

enum Options: String {
    case help = "--help"
    case verbose = "--verbose"
    case version = "--version"
}

var args = Array(CommandLine.arguments.dropFirst())

if let _ = args.index(of: Options.version.rawValue) {
    print("Version: \(version)")
    exit(ExitCode.success.rawValue)
}

if let _ = args.index(of: Options.help.rawValue) {
    usage()
    exit(ExitCode.success.rawValue)
}

if let index = args.index(of: Options.verbose.rawValue) {
    print("debug enabled.")
    debug = true
    args.remove(at: index)
}

--verboseだった場合には、debugのflagをtrueにして、デバッグ用のログを有効にします。
その場合には、argsから--verboseを取り除いています。
その後に、必要な引数が2つ、渡ってきているかチェックしています。

if args.count != 2 {
    print("Need 2 arguments. ", separator: "", terminator: "")
    usage()
    exit(ExitCode.failure.rawValue)
}

終了ステータスを渡す

通常何もせず、エラーなく終了すればscriptの実行結果として終了ステータスは0が渡りますが、
状況によって変えたい場合には、 exit() 関数に任意の数値を渡してあげれば終了ステータスを変えることができます。
これも、enum用意してあげると便利だったりします。

enum ExitCode: Int32 {
    case success = 0
    case failure
    case licenseNotFound
    case invalidPlist
    case generateError
}

// 終了ステータスとして1を返す
exit(ExitCode.failure.rawValue)

Terminalでのcd コマンドをどうやるか

今回FIleManagerを使ってファイルの探索や保存を行うのですが、探す時の基準を、 スクリプト実行時に渡すプロジェクトのルートディレクト に確実に指定しておきたいので、
以下のようにしてFileManagerの現在のディレクトリを変更してあげます。

let fileManager = FileManager.default
fileManager.changeCurrentDirectoryPath(rootPath)
debugLog("Root path: \(fileManager.currentDirectoryPath)")

これが、shellの

$ cd /path/to/project
$ pwd

に相当します。

ProjectのRun Scriptに仕込む

さて、ここまででTerminal上でswiftで書いたスクリプトを実行できるようになりました。
できればビルド時なんかに自動的にplistを作成したいですよね。そこで、こんな感じでBuild PhaseにRunScriptとして追加してあげます。

スクリーンショット 2016-11-16 20.19.24.png (107.0 kB)

./ といった相対path指定がうまく動かないので、${SRC_ROOT}とかうまく使ってあげると良いです。

実はこの時に、swift scriptの上部に書く

#!/usr/bin/env xcrun --sdk macosx swift

というshebang(シバン)が重要になってきます。
よく見かける

#!/usr/bin/swift

だと、スクリプト実行時に失敗してしまいます。なのでRunScriptで実行することを考慮するのであれば、シバンは #!/usr/bin/env xcrun --sdk macosx swift としておきましょう。
今回の例では作成したswift scriptファイルを呼び出すようにしていますが、ここで適切にシバンを設定して直にコードを書くこともできます。

まとめ

script書きたいけど、shellはあまり書き慣れてないし、それならswiftで書いてみようかな...という人は是非挑戦してみてください! また、今までCarthageのライブラリのライセンス管理どうしよう...と迷っていた人も是非、このスクリプト使ってみてください!

参考にさせていただいた記事

こちらの、Swift Scripting By Example: Generating Acknowledgements For CocoaPods & Carthage Dependenciesという記事を参考にさせて頂きました。
こちらは、最終的に単一のHTMLの形式として出力するような形だったので、参考にしつつ、plist形式で出力ができるように作った形になります。

積極採用中!!

子育て家族アプリFamm、カップル専用アプリPairyを運営するTimers inc. では、現在エンジニアを積極採用中! 急成長中のサービスの技術の話を少しでも聞いてみたい方、スタートアップで働きたい方など、是非お気軽にご連絡ください! 採用HP : http://timers-inc.com/engineerings