HomebrewでPythonアプリのFormulaを作る時に気をつけること

# やったこと

最近[FlatCAM](http://flatcam.org/)というオープンソース基板CAM（ガーバーデータを読んでGコードとかを出力してくれるやつ）ソフト用の野良Homebrew Formulaを作りました。

<https://github.com/tomoyanonymous/homebrew-flatcam>

というのも、LinuxとWindowsにはビルドされたバイナリがあるのにMacには無く、自力で依存パッケージを`brew`でインストールして、Python依存パッケージは`pip`でインストールして自力で立ち上げてね、とのことだったので、流石にちょっとめんどくさいな…と思ったからです。

しかもFlatCAMは現在まだGUIに使っているのPyqtのバージョンが4と一つ古いバージョンで、公式のHomebrewではすでに提供されていません。こういうものは本家リポジトリにFormula
リクエストしても承認されないので野良リポジトリとして作るのが懸命です。

# 参考にしたもの

インストールステップ自体は概ねこの手順に従いました。

Installing FlatCam on macOS Sierra
https://wolfgang.reutz.at/2017/04/25/installing-flatcam-on-macos-sierra/

あとFormula作る時は本家のFormula Cookbookを穴が空くほど読みましょう。野良リポジトリにしても有用な話がたくさんあります。

https://github.com/Homebrew/brew/blob/master/docs/Formula-Cookbook.md

それから今回の本題であるPythonアプリに関してのTipsはここに情報がまとまっています。

https://github.com/Homebrew/brew/blob/master/docs/Python-for-Formula-Authors.md

# 手順

まずformulaのファイルを作りましょう。

```sh:bash
brew create flatcam
```

こうすると`/usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/Formula/flatcam.rb`というファイルが出来上がると思います。後で移動しますがこの場所で作業していたほうが楽なのでとりあえずここに置いて直接編集します。ある程度雛形は作ってくれているので、必要なところを埋めていきます。

## 基本情報

```ruby:flatcam.rb
class Flatcam < Formula
  version "8.5.0"
  desc "FlatCAM: 2D Computer-Aided PCB Manufacturing"
  homepage "http://flatcam.org/"
```
このあたりの基本情報は野良リポジトリならシビアになる必要はないですが何も入れないと警告が出たりするので（特にバージョン名はフォルダ名につながるので）入れましょう。

## 本体のURL

```ruby:flatcam.rb
  url "https://bitbucket.org/jpcgt/flatcam.git"
```
これはビルドされたバイナリが配布されていたりすればその.tar.gzファイルと、そのSHA256キーを書く事が推奨されますが、特にない場合はGitリポジトリのURLそのまんまでも大丈夫です。ブランチは標準でmasterですが、特定のブランチやタグ、コミットハッシュを指定することも出来ます[^version]。

[^version]: 今回は最終リリース版の`8p5`というブランチがあったのですが、`setup.py`がそれ以降に整備されていたようなのでheadバージョンを使っています。ブランチ指定は本来はいつ変更があるかわからないのでコミット指定の方がいいとは思います…。

## Homebrew依存パッケージ

```ruby:flatcam.rb
  depends_on "python" => "with-tcl-tk"
  depends_on "cartr/qt4/qt@4"
  depends_on "cartr/qt4/pyqt@4"
  depends_on "cartr/qt4/pyside"
  depends_on "geos"
  depends_on "spatialindex"
  #for matplotlib
  depends_on "pkg-config"
  depends_on "freetype"
  #for scipy(fortran compiler)
  depends_on "gcc"
```
ここからが本題。tapしなければいけない非公式リポジトリは例えば、

```sh:bash
brew tap cartr/qt4 # Github上のユーザーcartrのhomebrew-qt4というリポジトリを指定
brew install qt@4
```
というものは`  depends_on cartr/qt4/qt@4`として書いておけばいいです。バージョン古いぞとwarningは出ますがそういう目的なので仕方ない。
geosとspatialindexは普通に最新版でいいのでそのようにしています。あと参考記事では書いてなくて引っかかりましたが、python上でmatplotlibを動かすためにはpkg-configとfreetypeが、scipyを動かすためにはgfortranが、つまりそれが入っているgccが（何かのfftライブラリが依存してるらしいです…）必要なので書いておきます。

仮にHomebrewで自分のマシンにgccとかがインストール済みになっていても、明示的に依存を指定しておかないとパスが通りません。余談ながらgccはビルドに1時間ぐらいかかるので初回インストールでは罠になります。

# HomebrewにおけるPythonのバージョン達

本題中の本題です。
まず前提として、Homebrewを使っている人のMac上には大体3種類のPythonがインストールされていると思ってください。これらはごっちゃにならないようにそれぞれのコマンド名が分けられています。パッケージマネージャpipも同様に、それぞれインストールされるパッケージのフォルダも異なります。

|インストール元|系列|コマンド名|pipコマンド|pipインストールディレクトリ|
|:---|:---|:---|:---|:---:|
|システム標準|2.7|`python`|`(標準では付属せず)`|
|Homebrew `python`|2.7|`python2`|`pip/pip2`|`/usr/local/lib/python2.7/site-packages`
|Homebrew `python3`|3.x|`python3`|`pip3`|`/usr/local/lib/python3.x/site-packages`

pyenvやvirtualenvを使っている人は更にこれに加えて幾つかのバージョンがインストールされていたり、エイリアスが変更されていたりします。ここは突っ込み始めると泥沼なのでノータッチです。
HomebrewでPythonアプリを扱う場合、特別なことがなければシステムのpythonを使うようにやってくれるのですが、今回のアプリではtcl-tkのインタプリタが入っていないとダメで、Homebrewの2.7に--with-tcl-tkオプションを付けてインストールすることになります。
というわけで`depends_on "python" => "with-tcl-tk"`という物を書いておく必要があります。

# Homebrewでpipパッケージの管理

さてFlatCAMが依存していたpipでインストールすべきパッケージは以下です。

`numpy matplotlib rtree scipy shapely simplejson svg.path`

例えばここでインストールスクリプトに`pip install <packages>`と直打ちしてしまうのも一つの手ですが、**HomebrewではHomebrew以外の何かをDLするツールを使用しない事が原則**となっています。それに、Homebrewでインストールしていたら知らない間に`/usr/local/lib/python2.7/site-packages`の中に知らないライブラリが山のように溜まっている…という状況も嫌なものです。

そこでHomebrewでは`virtualenv`を用いて`python`/`python2`/`python3`のどれかをベースにしたPythonの仮想実行環境を作り、パッケージはその仮想環境内にそれぞれインストールする形を取っています。
![スクリーンショット 2018-02-10 0.22.13.png](https://qiita-image-store.s3.amazonaws.com/0/56461/5a8d9639-ddda-a850-2c33-800fcb391756.png)

`/usr/local/Cellar/flatcam/8.5.0/libexec/lib`内にPythonの仮想環境のが構築され、画像のような感じでsite-packagesが作られ、その中にpipでインストールしたものが入るというわけです。

# homebrew-pypi-poetを使ってpipパッケージをURLに
では具体的にpipコマンドを使わずにこのフォルダにパッケージをDLするために、pipがインストールする時に取ってくる`pypi`のリポジトリから.tar.xzファイルとそのSHA256ハッシュを拾い集めなければなりません。それを手動で…？と思ったら`homebrew-pypi-poet`という便利スクリプトがあるようです。使い方を説明したほうが速いと思うのでそうすると、

```sh:bash
# virtualenvwrapperという仮想環境作成ユーティリティを入れる（これだけはグローバルで入れてしまって下さい）
brew install python
python -m pip install virtualenvwrapper #システムのpythonからpython2のpipを呼び出している
source $(brew --prefix)/bin/virtualenvwrapper.sh

# 一時的な仮想環境を作ります
mkdir ~/hoge && cd ~/hoge
mktmpenv

# homebrew-pypi-poetと、Formulaに必要なpackage達を一時的にインストール
pip install homebrew-pypi-poet numpy matplotlib rtree scipy shapely simplejson svg.path

# poetコマンドでFormulaに必要な文言を出力（2個以上のパッケージに依存する場合は-aと共に列挙します）
poet numpy -a matplotlib -a rtree -a scipy -a shapely -a simplejson -a svg.path

# 全部終わったら一時的な仮想環境を消去
deactivate
cd ~ && rm -rf ~/hoge
```
poetコマンドを実行した結果こんな文が帰ってきます。

```ruby:flatcam.rb
resource "backports.functools_lru_cache" do
    url "https://files.pythonhosted.org/packages/57/d4/156eb5fbb08d2e85ab0a632e2bebdad355798dece07d4752f66a8d02d1ea/backports.functools_lru_cache-1.5.tar.gz"
    sha256 "9d98697f088eb1b0fa451391f91afb5e3ebde16bbdb272819fd091151fda4f1a"
  end

  resource "Cycler" do
    url "https://files.pythonhosted.org/packages/c2/4b/137dea450d6e1e3d474e1d873cd1d4f7d3beed7e0dc973b06e8e10d32488/cycler-0.10.0.tar.gz"
    sha256 "cd7b2d1018258d7247a71425e9f26463dfb444d411c39569972f4ce586b0c9d8"
  end

  resource "matplotlib" do
    url "https://files.pythonhosted.org/packages/6d/bd/3e8cec37bcf71cfd81fe798cf733c046b1ceb123e7dddf6d3435cf03b506/matplotlib-2.1.2.tar.gz"
    sha256 "725a3f12739d133adfa381e1b33bd70c6f64db453bfc536e148824816e568894"
  end

  resource "numpy" do
    url "https://files.pythonhosted.org/packages/ee/66/7c2690141c520db08b6a6f852fa768f421b0b50683b7bbcd88ef51f33170/numpy-1.14.0.zip"
    sha256 "3de643935b212307b420248018323a44ec51987a336d1d747c1322afc3c099fb"
  end

  resource "pyparsing" do
    url "https://files.pythonhosted.org/packages/3c/ec/a94f8cf7274ea60b5413df054f82a8980523efd712ec55a59e7c3357cf7c/pyparsing-2.2.0.tar.gz"
    sha256 "0832bcf47acd283788593e7a0f542407bd9550a55a8a8435214a1960e04bcb04"
  end

  resource "python-dateutil" do
    url "https://files.pythonhosted.org/packages/54/bb/f1db86504f7a49e1d9b9301531181b00a1c7325dc85a29160ee3eaa73a54/python-dateutil-2.6.1.tar.gz"
    sha256 "891c38b2a02f5bb1be3e4793866c8df49c7d19baabf9c1bad62547e0b4866aca"
  end

  resource "pytz" do
    url "https://files.pythonhosted.org/packages/60/88/d3152c234da4b2a1f7a989f89609ea488225eaea015bc16fbde2b3fdfefa/pytz-2017.3.zip"
    sha256 "fae4cffc040921b8a2d60c6cf0b5d662c1190fe54d718271db4eb17d44a185b7"
  end

  resource "Rtree" do
    url "https://files.pythonhosted.org/packages/b0/6c/6cc8d738f14d5efa0c38ec29403bbd9c75e64b3fe84b53290178dda0dbd9/Rtree-0.8.3.tar.gz"
    sha256 "6cb9cf3000963ea6a3db777a597baee2bc55c4fc891e4f1967f262cc96148649"
  end

  resource "scipy" do
    url "https://files.pythonhosted.org/packages/d0/73/76fc6ea21818eed0de8dd38e1e9586725578864169a2b31acdeffb9131c8/scipy-1.0.0.tar.gz"
    sha256 "87ea1f11a0e9ec08c264dc64551d501fa307289460705f6fccd84cbfc7926d10"
  end

  resource "Shapely" do
    url "https://files.pythonhosted.org/packages/7d/3c/0f09841db07aabf9cc387662be646f181d07ed196e6f60ce8be5f4a8f0bd/Shapely-1.6.4.post1.tar.gz"
    sha256 "30df7572d311514802df8dc0e229d1660bc4cbdcf027a8281e79c5fc2fcf02f2"
  end

  resource "simplejson" do
    url "https://files.pythonhosted.org/packages/0d/3f/3a16847fe5c010110a8f54dd8fe7b091b4e22922def374fe1cce9c1cb7e9/simplejson-3.13.2.tar.gz"
    sha256 "4c4ecf20e054716cc1e5a81cadc44d3f4027108d8dd0861d8b1e3bd7a32d4f0a"
  end

  resource "six" do
    url "https://files.pythonhosted.org/packages/16/d8/bc6316cf98419719bd59c91742194c111b6f2e85abac88e496adefaf7afe/six-1.11.0.tar.gz"
    sha256 "70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9"
  end

  resource "subprocess32" do
    url "https://files.pythonhosted.org/packages/b8/2f/49e53b0d0e94611a2dc624a1ad24d41b6d94d0f1b0a078443407ea2214c2/subprocess32-3.2.7.tar.gz"
    sha256 "1e450a4a4c53bf197ad6402c564b9f7a53539385918ef8f12bdf430a61036590"
  end

  resource "svg.path" do
    url "https://files.pythonhosted.org/packages/1d/6c/cf484a95b895a7acd3989082501c67c8f43b6f91181f2a0b7aa634d1df6f/svg.path-2.2.tar.gz"
    sha256 "bc7b75606e76f910bf0045d09a5f8415aaafff3cedd0a3ce9f0d474fe2007722"
  end
```

この文章を`depends_on`の後に続けて書いておけば良いだけです。後はインストールステップで

```ruby:flatcam.rb
  include Language::Python::Virtualenv #実際にはclass先頭に書いてます
  def install
    virtualenv_install_with_resources
  end
```

とやるとHomebrewがよしなにやってくれます。この`virtualenv_install_with_resources`は

```ruby:flatcam.rb
venv = virtualenv_create(libexec)
venv.pip_install resources
venv.pip_install_and_link buildpath
```
のエイリアスになっていて、例えば今回はFlatCAM自体がsetup.pyというファイルを持っているのでpipがそれに従ってリソースファイルの移動などをやってくれるのですが、./configureやmakeなどをしなければいけない場合はよくあるhomebrew formulaのように`system "make", "arg1"`や`bin.install("file1")`などと組み合わせていく必要があります。

# テストと公開

実際にインストール出来るかテストするのに便利なコマンド達を紹介しておきます。

```sh:bash
brew info flatcam
brew install --debug flatcam
brew install --interactive flatcam
brew audit --strict flatcam
```

まずシンタックスエラーがあれば`brew info`の時点でエラーが出ます。幾つかWarningもここで出してくれます。`brew install --debug`はインストール中の情報を全部表示してくれたり、エラーで止まった時にシェルに入れたりします。`brew install --interactive`は必要なリソースをDLし展開終わったところでtmpフォルダに移動してシェルに入れます（その後は全部手動です）。`brew audit`はFormulaのフォーマットとしておかしいところがないかチェックしてくれます。--strictは公式リポジトリにプルリクする時は付けたほうがいいですが野良リポジトリならそこまで気にする必要ない部分まできっちりチェックしてくれます。

で、大丈夫だったら元々flatcam.rbが作られた場所はhomebrew-coreという公式のGitリポジトリそのものなのでこれを置いているとbrew updateなどに支障がでます。適当にフォルダを作って逃がしてやりましょう。

```sh:bash
mkdir ~/homebrew-flatcam #適当です
mv /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/Formula/flatcam.rb ~/homebrew-flatcam
```

出来上がったらGithub上にhomebrew-xxというリポジトリ名で公開しておきましょう。
`brew tap username/xx`でtapしてインストールできるようになります。

# 希望

Formulaの中に`pip install hogehoge`って書いたらサーバサイドとかでhomebrew-pypi-poetが自動で全部解決して欲しい…