Bitriseで gemとbrewのキャッシュ

# 追記
Gemのキャッシュについてはbitrise公式で手順が解説されていたので、[こちら](https://devcenter.bitrise.io/jp/builds/caching/caching-ruby-gems/)をご覧ください。

# はじめ
brewのcacheは他のCIでも使えるかも

[公式のサポート記事](https://discuss.bitrise.io/t/how-to-cache-ruby-gem-dependencies/196)があった。

[キャッシュについての記事](https://devcenter.bitrise.io/caching/about-caching/)も分かり易かった。

BitriseではRubyのGemがインストールされるディレクトリを `gem environment gemdir` で取得できる。そのディレクトリを環境変数で保存しておいて、最後にCache:pushする一覧に入れておく。

cache:pullではキャッシュされたディレクトリをダウンロードしてくるので、次回の `gem install` からはそれを見に行ってる。

数十秒かかっていた `gem install xxx` が6秒ぐらいになった:angel: 

とりあえず投稿して、フォーマットは後で整形します。

#1.Cache設定用スクリプトのステップを追加
ステップの場所: **Git cloneとCache:Pullの間**
名前は適当に`Gem cache settings`とかにした。

```bash:Gem_cache_settings.sh
#!/usr/bin/env bash
# fail if any commands fails
set -e
# debug log
set -x

# cacheのための設定
set -ev
envman add --key GEM_HOME --value "$(gem environment gemdir)"
```

#2.`gem install xxx` スクリプトのステップを追加
ステップの場所: **Cache:PullとCache:Pushの間**

```bash:gem_install.sh
#!/usr/bin/env bash
# fail if any commands fails
set -e
# debug log
set -x

# cacheのための設定
set -ev
gem install slather #例えば、iOSのカバレッジ出力
```

#3. Cache:Pushのパス一覧に$GEM_HOMEを追加

`Cache paths`というフィールドの最後の行を改行して、 `$GEM_HOME` を追加する。

# ステップの位置関係まとめ

1. Git Clone
2. Gem cache settings
3. Cache:Pull
4. gem install
5. Cache:Push

※各ステップの上下には別のステップが入っても問題ない

# brewのキャッシュ
記事がなかったのでHomebrewのソースコード読んだ。

どういう仕組みでinstall済みのfomulaを再installせずに済ませているのかを考えたら、

```bash:xxx.sh
xxx is already installed and up-to-date
To reinstall 1.8.3, run `brew reinstall xxx`
```

と表示される条件を見つければ良いと思ったので、[installコマンドの中身](https://github.com/Homebrew/brew/blob/master/Library/Homebrew/cmd/install.rb)を見に行って、この文章を見つけた。

```ruby:install.rb
#多分コマンドラインで引数に渡したformulaを見に行ってeachで取り出してる。
#--forceオプションがbrewにあって、このソースの途中に `unless ARGV.force?` っていう構文あるから多分あってる。

ARGV.formulae.each do |f|
（略
if f.keg_only? && f.any_version_installed? && f.optlinked? && !ARGV.force?
          # keg-only install is only possible when no other version is
          # linked to opt, because installing without any warnings can break
          # dependencies. Therefore before performing other checks we need to be
          # sure --force flag is passed.
          if f.outdated?
            optlinked_version = Keg.for(f.opt_prefix).version
            onoe <<~EOS
              #{f.full_name} #{optlinked_version} is already installed
              To upgrade to #{f.version}, run `brew upgrade #{f.name}`
            EOS
          elsif ARGV.only_deps?
            formulae << f
          else
            opoo <<~EOS
              #{f.full_name} #{f.pkg_version} is already installed and up-to-date
              To reinstall #{f.pkg_version}, run `brew reinstall #{f.name}`
            EOS
          end
（略
```

一番下のelseに到達できればキャッシュできたと言えそう。

###**条件判定メソッド**
|method | 方策、調査結果|
|---| ---|
| keg_only? |  /usr/local/Cellarには存在するが、/usr/local/bin, /usr/local/lib などにリンクされていないformula  |
| any_version_installed?   |  最低でも一つのバージョンのformulaがインストール済みかどうか。 |
| optlinked?  |  /usr/local/opt 以下にシンボリックリンクが存在するかどうか 。 |
| outdated?  |  古いパッケージが入ってる  |
| only?deps?  |  --only-dependenciesオプションをつけるとこのフラグがtrueになる  

Homebrewのkegとcellarの関係が分かりにくいからkeg_onlyが分かりずらい。

kegとは特定のformulaのインストール先Pathであり、cellarはkegのインストール先パス

つまり**/usr/local/Cellar/keg/**
という関係性。

なのでkeg_only?は、「Cellar以下にしかビルドが存在せず、/usr/local/binや/usr/local/libにシンボリックリンクが作成されていない」という状態を表す。

# 方策
/usr/local/Cellarをキャッシュすれば行けそう。
/usr/local/opt/をキャッシュすれば行けそう。

# 結果
できた。

70秒弱かかっていた `brew install xxx` が8秒弱になった:angel:

<img width="708" alt="スクリーンショット 2018-06-29 11.42.02.png" src="https://qiita-image-store.s3.amazonaws.com/0/114709/73bc2537-d1a3-4d7d-57d6-5554c7e2a171.png">

### 1: Cache:Pullの前に、brewがインストール/シンボリックリンク作成/ダウンロードされるパスを環境変数に追加しておく。

```
envman add --key BREW_LICENSE_PLIST --value "$(brew --cellar)/license-plist"
envman add --key BREW_OPT_LICENSE_PLIST --value "/usr/local/opt/license-plist"
```

ちなみに以下は追加しなくていい。

```
envman add --key BREW_CACHE_LICENSE_PLIST --value "$(brew --cache)/license-plist"
```

homebrewのキャッシュってことで関係あるっぽいけど、コマンドを叩いてみると中身がわかる。 `/Library/Caches/Homebrew/` 以下には、ダウンロードした圧縮ファイル(tar)が入るだけなので、CI上でのキャッシュ時には必要ない。(Cellarにビルドが入ってる前提なので)

```bash:ls.sh
$ ls ` brew --cache `
carthage-0.29.0.high_sierra.bottle.tar.gz
gettext-0.19.8.1.high_sierra.bottle.tar.gz
libidn2-2.0.5.high_sierra.bottle.tar.gz
license-plist-1.8.3.tar.gz
mint-0.9.1.high_sierra.bottle.tar.gz
openssl-1.0.2o_1.high_sierra.bottle.tar.gz
portable-ruby-2.3.3_2.leopard_64.bottle.tar.gz
swagger-codegen-2.3.1.high_sierra.bottle.tar.gz
swiftlint-0.25.1.high_sierra.bottle.tar.gz
wget-1.19.5.high_sierra.bottle.tar.gz
...
```

### 2: cache:pullとcache:pushの間でbrew install
`brew link xxx`をしておくと、/usr/local/bin/ にシンボリックリンクが作られるので、こうしておく。(再インストールを回避するための条件でないのでキャッシュはしてない)

```
brew tap mono0926/license-plist
brew install license-plist
brew link license-plist
```

### 3: cache:pullにパス追加
自分の場合はこうなった。一番上はデフォ。二番目はgem。それ以下はbrewの特定のfomula用。

```
$BITRISE_CACHE_DIR
$GEM_HOME
$BREW_LICENSE_PLIST
$BREW_OPT_LICENSE_PLIST
$BREW_CACHE_LICENSE_PLIST
```

# (おまけ)Bitriseの挙動
## Cache:Push
Cache:PushはデフォルトではPRでは走らない。
なのでPR時に走らせるように挙動を変えたければ以下のように `run_if: ".IsCI"
` を追加する必要がある。

```
- cache-push:
        run_if: ".IsCI"
        inputs:
        - cache_paths: |-
        ...
```

## キャッシュの意義
キャッシュはAWSのS3で保存されCloudFrontでキャッシュされている。毎回そこからキャッシュをダウンロードしている。

サイズが大きくなるようなキャッシュは、ダウンロード時間も増えるためメリットが少ない。
インストール時間 > ダウンロード時間 のようなパッケージはキャッシュするメリットがある。

/usr/local/Cellar/ のキャッシュ&ダウンロードはものすごく重たいので、このようにformulaを指定している。

```
envman add --key BREW_LICENSE_PLIST --value "$(brew --cellar)/license-plist"
```

Vagrantの立ち上げ時にすでに色々入っているので、 **/usr/local/Cellar/ のキャッシュを試みたら3GB弱のキャッシュになった** 普段は700MB弱。

# Homebrewの挙動
brew installが呼ばれた時、すでにインストール済みかどうかなど色々チェックした後に新規インストールが必要であれば、`FormulaInstaller` が初期化されてインストールが始まる。
1. kegにformulaがインストールされる
2. /usr/local/opt/へのシンボリックリンクが作成される
3. /usr/local/bin/ へのシンボリックリンクが作成される