Mac 環境構築の自動化 2015年末版 ( homebrew + homebrew-cask + homebrew-brewdle + mackup + crontab + mas-cli )

この記事は [クラウドワークスAdvent Calendar 2015](http://qiita.com/advent-calendar/2015/crowdworks) 14日目の記事になります。（風邪をひいて一日遅れましたごめんなさい＞＜）


# 今までの Mac 環境構築の歴史

最初は、インストールしたアプリケーションの手書きメモとホームディレクトリの dotfiles を dropbox 配下に移動しシンボリックリンクへ張替えを行う自前シェルスクリプトがあっただけでした。

アプリをインストールする度に、手元のメモを更新するという温かみのある日々（もちろん頻繁に更新を忘れる）、そんな日常に颯爽と現れた [Github Boxen](http://boxen.github.com/) [^1] !  
Puppetを使ったプロビジョニングをローカルに適用するとか胸熱すぎる！さよなら僕の `*.txt` `*.sh` たち！

[^1]: [Mac - Boxen使わなくても許されるのは2012年までだよね - Qiita](http://qiita.com/yuku_t/items/c6f20de0e4f4c352046c)



こうして冪等性と共に楽しい日々を送っていたのですが、半年ほどで異変が起き始めます。

まずboxen適用の実行時間がひたすら遅くなりはじめ、rubyの新バージョンをインストールしたいのに boxen 本家のアップデートを待つ羽目になり（仕方ないので手で入れようものならboxenに削除されてしまいます。冪等性の名のもとに）、いざアップデートしたら非互換の変更により僕が育てた boxen はエラーが発生し、ついに適用すらできなくなりました。[^2]  ~~マジFuxdajfkjfa~~

[^2]: [Mac - Boxen使ってて許されるのは2013年だけだった - Qiita](http://qiita.com/yuku_t/items/50b2b376604c2fefd8cd)

こうしてもはやスクラップ状態と化した Mavericks とはファイルシステムごとおさらばし、乗り換えた先が [Ansible + homebrew_module](http://docs.ansible.com/ansible/homebrew_module.html) [^3] !

[^3]: [AnsibleでHomebrew, Cask, Atomエディターのパッケージを管理する - Qiita](http://qiita.com/hnakamur/items/1c27cf0df19fe57ec624)


YAMLによる簡潔な記述で動作も軽快な Ansible とはパートナーとして仲良くやっていける！そう確信して意気揚々と playbook を育てていきました。

そんな日々にやってきた OS X Yosemite アップグレード！
いつものように `ansible-playbook` コマンドを叩けばエラーの嵐！エラーを潰すために必要になる様々なコマンドを playbook に叩き込んでいくうちに、簡潔だった yaml がインデントとコマンドだらけになっていきました。。
そしてある日、 Ansible 自体のアップデートをしたら複雑化した僕の playbook は呆気無く実行できなくなってしまいました。  ~~マジFufjdsk%^@(*&^@&!##!!!jljfldsakjkljxda!!!~~


## 思う所

環境構築を自動化したくてツールを使ってきたのですが、振り返ってみればツールの変化についていくことが精一杯で（むしろ振り切られて...）、自動化したいタイミングでいつもメンテが必要になっており、僕は自動化の恩恵を全く預かっていないことに気づきました....!!

mac の環境構築を行う頻度(新しいmac買った時、macが壊れた時、OSアップグレード時..etc) は短くても半年に一回程度ですが、現代のプロビジョニングツールの変遷はそれよりも早いようです。。


# 現在の構成

色々と考えた末、今（2015年末）の僕の mac の環境構築は下記のツールを組み合わせて、それらをインタラクティブに呼び出すシェルスクリプトで構成しています。

<dl>
  <dt>homebrew + homebrew-cask + homebrew-brewdle</dt>
  <dd>パッケージとアプリの管理</dd>
  <dt>mackup</dt>
  <dd>dotfiles, ディレクトリ, アプリ設定の管理</dd>
  <dt>crontab </dt>
  <dd>バックアップ</dd>
</dl>
[^5] [^6] 

[^5]: この記事を書いている最中に調べたら、homebrew-brewdle は homebrew-bundle に変更された(正確には戻った)ようです。[Rename to `brew bundle`. · Homebrew/homebrew-bundle@2c59ed1](https://github.com/Homebrew/homebrew-bundle/commit/2c59ed1752a069ab423441f53dfc686670406d64) ですが、僕は brew-bundle で試していない+今もhomebrew-brewdleで動くので、記事中では homebrew-brewdle で統一します。

[^6]: mac では [cron は非推奨で launchd を使うべき](http://furudate.hatenablog.com/entry/2014/11/09/155017)なようですが、launchd の記法を覚えるのが面倒でぼくは crontab 使ってます。><


ポイントは `crontab` [^6] を用いてバックアップを自動化することで、プロビジョニングツール使用時に発生する `構成ファイルを編集(playbook, Brewfileなど)` -> `構成を適用` という手順を省略しています。 
つまり、普段はいつも通り `brew install hoge` や `brew uninstall fuga` 等で管理するだけでよく、初回の環境構築の時だけ専用のシェルスクリプトを呼べば良いようになっています。


## [homebrew](http://brew.sh/) + [homebrew-cask] (https://github.com/caskroom/homebrew-cask) + [homebrew-brewdle](https://github.com/Homebrew/homebrew-brewdle) [^5]


[homebrew + homebrew-cask](http://www.task-notes.com/entry/20150129/1422459342) でパッケージとアプリケーションをほぼカバーでき、インストールするパッケージ/アプリのリストは [homebrew-brewdle の Brewfile](https://github.com/Homebrew/homebrew-bundle#usage)  という形式で管理できます。
参考用の僕の `Brewfile` は長いので、記事の一番下に記述しておきます。

~~※ MacAppStore からインストールするアプリはカバーできていないのですが、だれか良い方法があったら教えて下さい。~~

#### 2016/04/23 追記

`homebrew-brewdle` に MacAppStore をCLIで扱える [mas-cli](https://github.com/argon/mas) のサポートが追加されました。
[Add Mac App Store support using mas-cli. by mikemcquaid · Pull Request #172 · Homebrew/homebrew-bundle](https://github.com/Homebrew/homebrew-bundle/pull/172)

現時点では `master` ブランチにしか入っていないので、明示的にリポジトリを設定すると使えるようになります。

```bash
$ brew install argon/mas/mas # mas-cli のインストール
$ brew tap homebrew/brewdler git@github.com:Homebrew/homebrew-bundle.git
$ brew update
```

### バックアップ
homebrew-brewdle には [`dump`](https://github.com/Homebrew/homebrew-bundle#dump) コマンドで Brewfile にインストールしているパッケージ/アプリの一覧をファイルに書き出す機能があるので、これを `crontab` [^6] に設定し現在の構成を自動的にバックアップするようにしています。

```bash:crontab
@daily  cd /your/brewfile/directory && brew brewdle dump --force && git add Brewfile && git commit -m "auto commit of Brewfile dump `date "+\%Y-\%m-\%d \%H:\%M:\%S"`" && git push
```


僕は git 管理下にしていますが、単に Dropbox 等の配下でもいいと思います。

```bash:crontab
@daily  cd ~/Dropbox && brew brewdle dump --force
```


### アップデート

パッケージ(homebrew) とアプリケーション(homebrew-cask) は `crontab` で週次アップデートするようにしています。

```bash:crontab
@weekly time nice -n19 brew update && time nice -n19 brew upgrade && time nice -n19 brew cleanup && for c in `brew cask list`; do ! brew cask info $c | grep -qF "Not installed" || time nice -n19 brew cask install $c; done && time nice -n19 brew cask cleanup && for c in /opt/homebrew-cask/Caskroom/*; do vl=(`ls -t $c`) && for v in "${vl[@]:1}"; do \rm -rf "$c/$v"; done; done

```

#### 参考リンク

* [homebrew-cask - brew caskのappどもをupgradeする - Qiita](http://qiita.com/2k0ri/items/9fe8d33a72dbfb15fe6b)

* [Homebrew caskの古いバージョンを削除するワンライナー · Hori Blog](http://hori-ryota.com/blog/remove-outdated-files-of-brew-cask/)





## [mackup](https://github.com/lra/mackup)

`mackup` は dotfiles や アプリケーションの設定を dropbox 等に同期してくれるアプリです。
といってもやっていることは単純で、実ファイルをコピーしてシンボリックリンクに置き換えているだけで、特に dotfiles については同様のことを自前で書いている人も多いのではないでしょうか？

使い方は `mackup backup` コマンドを実行すれば終わりです。
この mackup は dotfiles に加えて[かなりの数のアプリケーションの設定ファイルをサポートしている](https://github.com/lra/mackup#supported-applications)のでオススメです。


デフォルトに加えてバックアップしたいファイル/ディレクトリがある場合の[設定ファイルの書き方も簡単](https://github.com/lra/mackup/blob/master/doc/README.md#add-support-for-an-application-or-any-file-or-directory)ですし、[Dropbox以外にも多数のストレージをサポート](https://github.com/lra/mackup/blob/master/doc/README.md#storage)しています。参考までに僕のmackup設定ファイルはこちらです。

```~/.mackup/ctokoro.cfg
[application]
name = My personal synced files and dirs

[configuration_files]
.gitignore_global
.config
.bundle
.chef
.z
.zsh_history
Gemfile
Gemfile.lock
Documents
Movies
Music
Pictures
```

### バックアップ

この mackup のコマンドを crontab に設定しておくことで新しいアプリの設定も自動的にバックアップされるようにしています。

```bash:crontab
@daily  yes | mackup backup
```


## 環境構築用シェルスクリプト

あらかじめ Dropbox だけは同期しておき、このスクリプトを呼んで `y` を連打するだけで環境構築を終えられるようになっています。
インタラクティブにしている理由は、OSアップグレード時などですんなり入らず途中で終了することもよくあるので、途中から再開しやすくするためです。


```bash:setup_osx.sh
#!/bin/bash
BREWFILE_DIR=/your/brewfile/directory

ask() {
  printf "$* [y/n] "
  local answer
  read answer

  case $answer in
    "yes" ) return 0 ;;
    "y" )   return 0 ;;
    * )     return 1 ;;
  esac
}

set -e

if ask 'xcode install?'; then
  xcode-select --install
fi

if ask 'Homebrew install?'; then
  ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

  brew doctor
  brew install caskroom/cask/brew-cask
fi

if ask 'execute brew brewdler(Brewfile)?'; then
  brew tap Homebrew/brewdler
  pushd $BREWFILE_DIR
  brew brewdler
  popd
fi

if ask 'restore setting from mackup? (need Dropbox directory)'; then
  mackup restore
fi

if ask "Do you want to install ruby by rbenv-rubybuild?"; then
  INSTALL_RUBY_VERSION="$( rbenv install -l | peco)"
  brew link readline --force
  MAKE_OPTS="-j 4" RUBY_CONFIGURE_OPTS="--with-readline-dir=$(brew --prefix readline)" rbenv install $INSTALL_RUBY_VERSION
fi
```

## OSXの設定
OSXの設定でコマンドで設定できるものはシェルスクリプトでやるようにしています。ですが、カバー率が低くGUIでポチポチと設定が必要なものも多いので、なにかいい方法はないですかね。。


```bash:setup_osx.sh
## mac setting
if ask "set 'locate' command?"; then
  sudo launchctl load -w /System/Library/LaunchDaemons/com.apple.locate.plist
fi

if ask 'set visible dotfiles in finder?'; then
  defaults write com.apple.finder AppleShowAllFiles TRUE
  killall Finder
fi

if ask 'set fullpath title at finder?'; then
  defaults write com.apple.finder _FXShowPosixPathInTitle -bool yes
  killall Finder
fi

if ask 'set always expand save dialog?'; then
  defaults write -g NSNavPanelExpandedStateForSaveMode -bool yes
fi

if ask 'set mute in mac booting sound?'; then
  sudo nvram SystemAudioVolume=%80
fi

# @see https://discussionsjapan.apple.com/thread/10153604
if ask 'set clamshell mode off? (for multi display sleep)'; then
  sudo nvram boot-args="niog=1"
fi
```


# 最後に

他にもここでは紹介していないですが、作業ディレクトリ圧縮+バックアップスクリプトなどを駆使して、いつ何時いまこの瞬間にmacが突然死しても、半日くらいで元の状態のマシンを構築できる体制を整えています。
ツールの移り変わりもあるので、来年の今頃はどんなツールを使っているかわかりませんが、新しいmacを買った時の時間の節約や最悪の事態の回避のために、常にブラッシュアップを続けていきたいと思います。
より良い方法やツッコミ、お待ちしております！



### P.S. `Brewfile`

```ruby:Brewfile
tap 'caskroom/cask'
tap 'homebrew/brewdler'
tap 'homebrew/dupes'
tap 'homebrew/services'
tap 'homebrew/versions'
cask 'java'
cask 'xquartz'
brew 'autoconf'
brew 'autossh'
brew 'awscli'
brew 'icu4c'
brew 'boost'
brew 'bsdmake'
brew 'cmake'
brew 'colordiff'
brew 'csshx'
brew 'go'
brew 'direnv'
brew 'xz'
brew 'gettext'
brew 'dwdiff'
brew 'elasticsearch'
brew 'exiftool'
brew 'pkg-config'
brew 'x264'
brew 'lame'
brew 'libvo-aacenc'
brew 'xvid'
brew 'libpng'
brew 'freetype'
brew 'openssl'
brew 'ffmpeg'
brew 'fortune'
brew 'gdbm'
brew 'ghi'
brew 'jpeg'
brew 'libtiff'
brew 'little-cms2'
brew 'ghostscript'
brew 'ghq'
brew 'gnu-getopt'
brew 'git-now'
brew 'heroku-toolbelt'
brew 'hub'
brew 'libtool'
brew 'imagemagick'
brew 'jbig2dec'
brew 'oniguruma'
brew 'jq'
brew 'libevent'
brew 'libffi'
brew 'libyaml'
brew 'mackup'
brew 'mecab'
brew 'mecab-ipadic'
brew 'memcached'
brew 'pcre'
brew 'nginx'
brew 'packer'
brew 'peco'
brew 'readline'
brew 'postgresql'
brew 'proctools'
brew 'q'
brew 'ruby-build'
brew 'rbenv'
brew 'rbenv-binstubs'
brew 'reattach-to-user-namespace'
brew 'redis'
brew 'rmtrash'
brew 'source-highlight'
brew 'spark'
brew 'terminal-notifier'
brew 'terraform'
brew 'the_silver_searcher'
brew 'tig'
brew 'tree'
brew 'watch'
brew 'wget'
brew 'youtube-dl'
brew 'z'
brew 'zsh'
brew 'caskroom/cask/brew-cask'
brew 'homebrew/dupes/apple-gcc42'
brew 'homebrew/versions/mysql56'
brew 'homebrew/versions/node012'
cask '1password'
cask 'adobe-creative-cloud'
cask 'alfred'
cask 'atom'
cask 'bartender'
cask 'burn'
cask 'caffeine'
cask 'cheatsheet'
cask 'chefdk'
cask 'clipy'
cask 'cyberduck'
cask 'disk-inventory-x'
cask 'dockertoolbox'
cask 'dropbox'
cask 'evernote'
cask 'flickr-uploadr'
cask 'flux'
cask 'github-desktop'
cask 'google-chrome'
cask 'google-drive'
cask 'google-japanese-ime'
cask 'google-photos-backup'
cask 'handbrake'
cask 'hosts'
cask 'imageoptim'
cask 'istat-menus'
cask 'iterm2'
cask 'karabiner'
cask 'licecap'
cask 'maczip4win'
cask 'mou'
cask 'rubymine'
cask 'sequel-pro'
cask 'shiftit'
cask 'skitch'
cask 'skype'
cask 'slack'
cask 'sublime-text'
cask 'vagrant'
cask 'virtualbox'
cask 'vlc'
cask 'zoomus'
```