Carthageのちょっと面倒な作業をどうにかする

初投稿です。[Sumally](https://sumally.com)のエンジニアです。主に[Sumally for iOS](https://itunes.apple.com/jp/app/id481540526)と[Sumally Pocket for iOS](https://itunes.apple.com/jp/app/id1026885096)の開発をしてます。

さて、Sumally Pocket for iOSはSwift 1.2で開発しましたが、iOS7対応をしていたため、ライブラリはgit submoduleとCocoaPodsで入れていました。
ということで今更になってしまったのですが、Swift 2.0移行及びiOS7対応を切る作業に取りかかりはじめたので、ビルドが遅くなってきていることもあり、これを機にCarthageを導入することにしました。現段階ではすべてを移行することはできないのでCarthageとCocoaPodsとの両方を使っています。

導入してみて、ビルドのスピードは大改善され大満足なのですが、ライブラリを追加する手間がCarthageは結構かかるなと思いました。

ということで、`carthage update`からプロジェクトへの追加やその他の設定までをするスクリプトを書きました。

# スクリプトの流れ

1. （スクリプト内で指定可能 / 後々コマンドからのオプションで指定できるようにするかもです）`carthage update --platform iOS`の実行。
2. （1が実行された場合のみ）ビルドされたframeworkをプロジェクトに追加。
3. Build Phasesの`carthage copy-frameworks`のスクリプトのInput Filesの更新（まだ追加だけの対応です）。
4. （Podfileがある場合・スクリプト内で指定可能 / 後々コマンドからのオプションで指定できるようにするかもです）pod installの実行。
5. （スクリプト内で指定があれば）CocoaPodsのPods-Acknowledgements.plistをベースにCocoaPodsとCarthageのAcknowledgementsファイルの生成。

# プロジェクトファイルの下準備

実装を端折ったので、下準備をお願いします。

1. プロジェクトからこれまでのCarthage/Build以下へのReferenceをすべて消す。
1. Xcodeプロジェクトの直下にFile > New > Groupを選択し`Carthage`という名前をつける。（今後Carthage/Build/*.frameworkはこの直下に入っていきます）
2. 作成した`Carthage`グループのLocation（Xcode画面右側から指定）をRelative To Groupにする（デフォルトでなってます）。また、その右下あたりのフォルダアイコンをクリックしパネルからプロジェクト直下にある（なければ作ってください）Carthageフォルダを選択する。
3. XcodeプロジェクトのターゲットのBuild Phasesのタブを選択する。
4. すでにCarthageの`copy-frameworks`のスクリプトのRun ScriptのPhaseがある方は、そのPhase名を`Run Carthage Script`に変更する。ない人は新しく追加する。手順は次の通り。
    1. 左上の＋ボタンから`New Run Script Phase`を選択する。
    2. スクリプトに`/usr/local/bin/carthage copy-frameworks`を指定する。
    3. Phase名をRun Scriptから`Run Carthage Script`に変更する。

手順3と4はそれぞれのターゲット（例えばステージング版と本番版など）で必要です。

# 実行方法

[こちら](https://gist.githubusercontent.com/hoppenichu/d88d56e9b402153d687a/raw/fcf1c2e1f692f313c14ff84a7b37c6ba3c8022ce/gistfile1.rb)からダウンロードして好きなところに配置してください。僕はプロジェクトルートの直下にある`tools`というフォルダの中に入れてます。

実行には`CocoaPods`、`colorize`が必要なので`gem`で入れましょう。

```bash
sudo gem install cocoapods
sudo gem install colorize
```

あとはうえでダウンロード＆配置したスクリプトを実行するだけです。

```bash
./{スクリプト名}.rb
```

配置した場所によってはエラーが出るかもなので、エラーが出た人は次の「スクリプトの説明」を読んでください。

# スクリプトの説明

コメント書いてます。カスタマイズしようと思ったらこの辺をいじっていただけたらと思います。

```rb
#!/usr/bin/env ruby

require 'xcodeproj'
require 'colorize'

Dir.chdir "#{__dir__}/../" # プロジェクトのルートへのパスを指定してください（__dir__はこのRubyファイルのあるディレクトリの絶対パスです）

class LibraryUpdateManager
  attr_accessor :update_carthage, :update_cocoapods, :acknowledgements_path
  def initialize
    @update_carthage = true
    @update_cocoapods = File.exists?('Podfile')
  end

  def perform_update
    begin
      if @update_carthage
        puts 'update carthage🏃'.bold
        _update_carthage
        puts 'finish updating carthage🍺'.green
      end
      if @update_cocoapods
        puts 'update cocoapods🏃'.bold
        _update_cocoapods
        puts 'finish updating cocoapods🍺'.green
      end
      if !@acknowledgements_path.nil?
        puts 'update acknowledgements🏃'.bold
        _update_acknowledgements
        puts 'finish updating acknowledgements🍺'.green
      end
      puts 'complete update libraries🎉'.green
    rescue => error
      puts "#{error.message}😱".red
    end
  end

  private
  def _update_carthage
    raise 'carthage update failure' if !system('carthage update --platform iOS')
    _update_projects
  end

  def _update_projects
    project = Xcodeproj::Project.open(*Dir.glob('*.xcodeproj'))
    carthage_group = project.groups.find do |group|
      group.path == 'Carthage'
    end

    if !carthage_group
      raise 'carthage group not found'
    end

    existing_file_names = carthage_group.files.map do |file|
      file.name
    end

    new_framework_paths = Dir.glob('Carthage/Build/iOS/*.framework').select do |framework|
      !existing_file_names.include?(File.basename(framework))
    end

    if new_framework_paths.empty?
      return
    end

    framework_references = new_framework_paths.map do |framework_path|
      path = File.basename(framework_path)
      carthage_group.new_file("Build/iOS/#{path}")
    end

    project.targets.each do |target|
      next if target.product_type == 'com.apple.product-type.bundle.unit-test'
      run_carthage_script_phase = target.build_phases.find do |phase|
        phase.display_name == 'Run Carthage Script'
      end
      next if run_carthage_script_phase.nil?

      framework_references.each do |reference|
        target.frameworks_build_phase.add_file_reference(reference)
      end
      new_input_paths = new_framework_paths.map do |path|
        "$(SRCROOT)/#{path}"
      end
      input_paths_set = run_carthage_script_phase.input_paths.to_set
      run_carthage_script_phase.input_paths = input_paths_set.merge(new_input_paths).to_a
    end
    project.save
  end

  def _update_cocoapods
    raise 'pod install failure' if !system('pod install')
  end

  def _update_acknowledgements
    cocoapods_acknowledgement = Xcodeproj::PlistHelper.read('Pods/Target Support Files/Pods/Pods-Acknowledgements.plist')
    carthage_preference_specifiers = Dir.glob('Carthage/Checkouts/**/LICENSE').map do |license|
      {
        :FooterText => IO.read(license, :encoding => 'utf-8'),
        :Title => File.basename(File.dirname(license)),
        :Type => 'PSGroupSpecifier',
      }
    end
    key = 'PreferenceSpecifiers'
    cocoapods_acknowledgement[key].insert(cocoapods_acknowledgement[key].length - 1, *carthage_preference_specifiers)
    Xcodeproj::PlistHelper.write(cocoapods_acknowledgement, @acknowledgements_path)
  end
end

if __FILE__ == $PROGRAM_NAME
  manager = LibraryUpdateManager.new
  manager.update_carthage = true # carthage updateを実行するかどうか（default: true）
  manager.update_cocoapods = true # pod installを実行するかどうか（default: Podfileがあればtrue, なければfalse）
  manager.acknowledgements_path = 'Sumally/Resources/Settings.bundle/Acknowledgements.plist' # default: nil / Acknowledgements.plistへのパス / nilの時は更新しない

  manager.perform_update
end
```