Mac の開発環境構築を自動化する (2015 年初旬編)

背景

愛用していた MBP15" が一ヶ月ほど前に突然亡くなり、急遽 MBP13" を買って環境構築を行ったので記録しておく。

(その後噂の薄くて軽くて新しい Macbook が出ただけでなく MBP13" までマイナーアップデートされたりしたが、悔しくはない。悔しくはないぞ!!)

Brewfile オワコン問題

開発環境の構築は HomebrewHomebrew Cask を入れて Brewfile を書き、 brew bundle すれば終わりかと思いきや、もう Brewfile はオワコンになってしまったらしい。

(3/25 追記) Brewfile がオワコンなのではなく Homebrew 本体から bundle コマンドが外されただけで、 元となった brewdle コマンドは健在で、もっと便利な brew-file もあるとのことです。 参考: Brewfileはオワコンではない

Ansible でできる?

しかし開発環境の構築は可能な限り手作業を減らしたい。Brewfile 相当のシェルスクリプトを書いても良いが、少し調べてみると Ansible に homebrew モジュール があり、 Ansibleでhomebrewを管理する ことができるらしい。実際に Macの環境構築をAnsibleでやることにした 方もいるようだ。

そして hnakamur さんが AnsibleでHomebrew, Cask, Atomエディターのパッケージを管理する 自動化タスクを再利用可能な形で Ansible Galaxy に公開されていることもわかった。 他にも osxc - simple configuration tool for os x というツールがあるらしい (これも Ansible ベース)。これらを踏まえて、自動化に取り組んでみる。

自動化の前に手で入れた(入れてしまった)もの

なお、下記のアプリは仕事上すぐに必要だったので、後述の自動化の仕組みに入れずに手で入れてしまった。

  • dropbox
  • 1Password

自動化準備

XCode

Homebrew を入れるためにまず Mac App Store から XCode をインストール。長い時間待ってダウンロードが終わったら一度立ち上げ、ライセンスに同意しておく。後にわかったことだが Mac Yosemite Rails 最新環境 詳解 構築手順 によると、立ち上げずとも下記コマンドでライセンス同意できるらしい (未確認)。

sudo xcodebuild -license

Homebrew

Xcode の Command Line Tool を入れ、その後から homebrew をワンライナーで入れる。

xcode-select --install
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

Homebrew インストール後に brew doctor コマンドを叩き、古いと言われた場合は brew update する。

brew doctor
brew update

Ansible

Ansible でプロビジョニングを行うため python と ansible を入れる。二つとも Homebrew で入る。

brew install python
brew install ansible

自動化開始

プロビジョニング用のディレクトリを作る。名前は何でも良いが .macbook-provisioning としておく。

mkdir .macbook-provisioning
cd .macbook-provisioning/

以降の作業は .macbook-provisioning ディレクトリで行う。

inventory ファイル作成

ansible 用の inventory ファイル (実行対象ホスト指定ファイル) を作る。今回は手元マシンのプロビジョニングなので localhost だけあれば良い。ファイル名は hosts とする。

echo 'localhost' > hosts

playbook ファイル作成

次に playbook (プロビジョニングの内容) を書く。ファイル名は localhost.yml とする。(このファイルを含め github に置いておきました twada/macbook-provisioning)

- hosts: localhost
  connection: local
  gather_facts: no           
  sudo: no
  vars:
    homebrew_taps:
      - homebrew/binary
      - homebrew/dupes
      - caskroom/cask
      - railwaycat/emacsmacport
      - sanemat/font
    homebrew_packages:
      - { name: readline }
      - { name: openssl }
      - { name: openssl, state: linked, install_options: force }
      - { name: python }
      - { name: ansible }
      - { name: coreutils }
      - { name: git }
      - { name: zsh, install_options: disable-etcdir }
      - { name: wget }
      - { name: curl }
      - { name: cmake }
      - { name: autoconf }
      - { name: automake }
      - { name: pkg-config }
      - { name: ctags }
      - { name: tree }
      - { name: lv }
      - { name: nkf }
      - { name: jq }
      - { name: go }
      - { name: direnv }
      - { name: peco }
      - { name: hub }
      - { name: tig }
      - { name: fish }
      - { name: rbenv }
      - { name: ruby-build }
      - { name: tofrodos }
      - { name: lha }
      - { name: flow }
      - { name: mysql }
      - { name: sqlite }
      - { name: redis }
      - { name: imagemagick }
      - { name: mercurial }
      - { name: packer }
      - { name: xz }
      - { name: socat }
      - { name: rlwrap }
      - { name: w3m }
      - { name: tmux }
      - { name: reattach-to-user-namespace }
      - { name: phantomjs }
      - { name: graphviz }
      - { name: autojump }
      - { name: gibo }
      - { name: source-highlight }
    homebrew_cask_packages:
      - { name: emacs-mac }
      - { name: iterm2 }
      - { name: firefox }
      - { name: google-chrome }
      - { name: adobe-reader }
      - { name: java }
      - { name: skype }
      - { name: slack }
      - { name: sourcetree }
      - { name: gitx }
      - { name: karabiner }
      - { name: seil }
      - { name: flux }
      - { name: dash }
      - { name: skitch }
      - { name: seashore }
      - { name: atom }
      - { name: kobito }
      - { name: webstorm }
      - { name: phpstorm }
      - { name: intellij-idea }
      - { name: vagrant }
      - { name: virtualbox }

  tasks:
    - name: homebrew の tap リポジトリを追加
      homebrew_tap: tap={{ item }} state=present
      with_items: homebrew_taps

    - name: homebrew をアップデート
      homebrew: update_homebrew=yes

    # brew
    - name: brew パッケージをインストール
      homebrew: >
        name={{ item.name }}
        state={{ item.state | default('latest') }}
        install_options={{
          item.install_options | default() | join(',')
          if item.install_options is not string
          else item.install_options
        }}
      with_items: homebrew_packages
      register: brew_result
    - name: brew パッケージの情報保存先ディレクトリを作成
      file: path=brew_info state=directory
    - name: brew パッケージの情報を保存
      shell: brew info {{ item }} > brew_info/{{ item }}
      with_items: brew_result.results | selectattr('changed') | map(attribute='item') | map(attribute='name') | list

    # cask
    - name: homebrew-cask のインストール
      homebrew: name=brew-cask state=latest
    - name: cask パッケージをインストール
      homebrew_cask: name={{ item.name }} state={{ item.state|default('installed') }}
      with_items: homebrew_cask_packages
      register: cask_result
    - name: cask パッケージの情報保存先ディレクトリを作成
      file: path=cask_info state=directory
    - name: cask パッケージの情報を保存
      shell: brew cask info {{ item }} > cask_info/{{ item }}
      with_items: cask_result.results | selectattr('changed') | map(attribute='item') | map(attribute='name') | list

    # oh-my-zsh
    - name: oh-my-zsh のインストール
      shell: curl -L https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh | sh
      args:
        creates: ~/.oh-my-zsh/

    # Ricty
    - name: xquartz のインストール (for Ricty)
      homebrew_cask: name=xquartz
    - name: fontforge のインストール (for Ricty)
      homebrew: name=fontforge
    - name: Ricty のインストール
      homebrew: name=ricty
    - name: 生成されたフォントファイルをコピー
      shell: cp -f $(brew --cellar ricty)/*/share/fonts/Ricty*.ttf ~/Library/Fonts/
      args:
        creates: ~/Library/Fonts/Ricty-Bold.ttf
      notify: run fc-cache

  handlers:
    - name: run fc-cache
      shell: fc-cache -vf

実行

あとは ansible-playbook コマンドを叩けば、 localhost.yml の内容が次々に実行され、アプリが大量にインストールされる。出力の量は -vv オプションくらいがちょうど良い気がしている。

HOMEBREW_CASK_OPTS="--appdir=/Applications" ansible-playbook -i hosts -vv localhost.yml

ただ、実際には cask パッケージのいくつかでパスワードを聞かれるので、全自動とまではいかない。

(追記) HomebrewとAnsibleでMacの開発環境構築を自動化する | mawatari.jp によると、コマンド実行時に HOMEBREW_CASK_OPTS="--appdir=/Applications" を指定した方がよさそう。オプションを指定しないと、アプリケーションによって /Applications だったり、 ~/Applications だったりにシンボリックリンクリンクが作られてしまうとのこと。

localhost.yml を軽く説明

  • 構造や内容は hnakamur さんの Qiita エントリ をとてもとても参考にしつつ、何が起こるかを自分で把握したいので、またインストール後の処理や cask のオプションも後々明示的に指定できるようにしたかったので、結局ほぼ同じ内容を1ファイルで書いた (hnakamur さんすみません)。同様の理由で osxc も結局使わなかった。
  • Ansible の homebrew モジュールのオプションに何が指定できるかは 本家ドキュメントAnsibleでhomebrewを管理する - 理系学生日記 が参考になる。
  • 各パッケージのデフォルトの statelatest にしているので、既にインストールされているパッケージでも新しいバージョンがある場合はアップデートされる。
  • Ricty フォントのインストールは xquartz (cask), fontforge (brew), ricty (brew) の順に入れなければならないので、一発で入らず試行錯誤することになった (ここに書いた定義には、やっとたどり着いた)。
  • oh-my-zsh や Ricty のインストール定義のところはファイルの存在チェックを行う creates オプション を使用しているので、一度インストールが終わったら次の回からはスキップされる。
  • ATOK 派なので google-japanese-ime を入れていない
  • Emacs 派なので vim (ry
  • Emacs は railwaycat/emacsmacport を tap に加えて homebrew-cask で emacs-mac-port をインストールしている。
  • ansible で実行すると、 homebrew を使っている人にはおなじみの brew install 後の出力がないので不安を覚える。この出力内容は brew info で見れるので、ファイルにダンプしておく(本当はインストール時の標準出力をファイルに出しておきたい。詳しい人教えてください)。
  • (3/24 追記) mawatari さんに プルリクエストいただいて いくつかの問題点を修正しました。 mawatari さんありがとうございます! 参考: HomebrewとAnsibleでMacの開発環境構築を自動化する | mawatari.jp

homebrew-cask の不安な点について

今回 homebrew だけでなく homebrew-cask も使いアプリケーションのインストールまで自動化したが、うまく動かないものや違和感を覚えるものもあることを記しておく。たとえば Chrome の info には次のように出てくる。

$ brew cask info google-chrome
google-chrome: latest
google-chrome
https://www.google.com/chrome/
/opt/homebrew-cask/Caskroom/google-chrome/latest (389 files, 367M)
https://github.com/caskroom/homebrew-cask/blob/master/Casks/google-chrome.rb
==> Contents
  Google Chrome.app (app)
==> Caveats
The Mac App Store version of 1Password won't work with a Homebrew-Cask-linked Google Chrome. To bypass this limitation, you need to either:

  + Move Google Chrome to your /Applications directory (the app itself, not a symlink).
  + Install 1Password from outside the Mac App Store (licenses should transfer automatically, but you should contact AgileBits about it).

$ 

しかし実際に使ってみると Max App Store で入れた 1Password と組み合わせても期待通り動いたりしているので、実際にやってみないと何ともいえないところがあるのかもしれない。他にも調べてみると homebrew-cask でうまく動かずに使うのをやめたり、ハックして運用で回避する等はよくあるようなので、下記リンクは参考になった(私の場合入れてしまった後なのだが)

入れるアプリにもよるだろうが、まだまだうまくいかないところがあるのだろう。このあたりは自己責任となりそうだ。

ghq + peco

仕事でも個人でも大量のソースコードの読み書きを手元で統一的に扱いたいので ghq を入れる。ディレクトリ構成も、この機会に lestrrat / antipop / miyagawa 方式にした。

まず .zshrc に以下の設定を追加

export GOPATH=$HOME
export PATH=$PATH:$GOPATH/bin

ghq をインストール

go get github.com/motemen/ghq

以下のコマンドで .gitconfig に設定を追加

git config --global user.name "ユーザー名"
git config --global user.email "メールアドレス"
git config --global ghq.root "~/src"

これで

cd $(ghq list -p | peco)

したり、

ghq get twada/power-assert

したりできるようになった。これは捗る!!

おわりに

あとは秘伝のタレ系の dotfiles を持ってきたりすれば終わり。ただそれら dotfiles もこの際に断捨離した方が良いかなと思っている。

これまで書いてきたオレオレ shell スクリプト等に比べると、やはり冪等性のあるプロビジョニングツールは二周目以降に強いと感じた。冪等性があると、設定に書かれた状態に向かって収束するように動作する。この習性がローカルの開発環境構築にも使えるというのは盲点だった。今回作成した仕組みは何回でも走らせられるので、追加したいパッケージがあるときは追記して実行すればいい。そうでなくとも定期的に実行しておけば環境を新しくしておけるので、かなり便利だ。

ということで、

Mac の開発環境構築自動化における定番である Brewfile がオワコンになっていたが Ansible を使ってまあまあ自動化できた話

でした。