[Ruby] mini_portile を使って依存ライブラリのコンパイルを簡略化する


はじめに
--------

mini_portile という拡張ライブラリ作者向けの gem がある。
拡張ライブラリが依存しているものを事前にコンパイルし、拡張ライブラリをコンパイルする際にそちらを参照するようにしてくれる gem だ。
nokogiri や sqlite3 でも使われているが、 mini_portile 自体の情報が日本語では見つけられなかったので紹介したい。

なお、筆者は本来想定されている使い方とは違う用途で利用してみたので、そちらについても軽く触れてみたいと思う。

mini_portile の使い方
---------------------

使い方は [README](https://github.com/flavorjones/mini_portile#readme) に詳しく載っているが、「そちらを見て欲しい」では紹介記事にならないので利用例を上げたいと思う。

### 基本的な使用方法

```ruby
require "mini_portile"
recipe = MiniPortile.new("libiconv", "1.13.1")
recipe.files = ["http://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.13.1.tar.gz"]
recipe.cook
recipe.activate
```

READMEに載っているものをそのまま持ってきた形だが、 iconv をコンパイルして拡張ライブラリのビルド時に使用するようにするには上記のコードのみでよい。
`MiniPortile#cook` でファイルの取得から展開、 `./configure` の実行、`make` までを自動で行ってくれる。
インストール先は `<BASEDIR>/port/<platform>/<ライブラリ名>/<バージョン>` となり、その後の `MiniPortile#activate` で環境変数にこのパスを追加するようになっている。
パッケージ管理システムの代替を目指している訳ではないためシステムワイドにインストールする機能は提供されていないが、拡張ライブラリをコンパイルする用途であれば問題ない。

### パッチを当てる

mini_portile にはダウンロードしてきたソースにパッチを当てるための機能が存在する。

```ruby
require "mini_portile"
recipe = MiniPortile.new("libiconv", "1.14")
recipe.files = ["http://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.14.tar.gz"]
recipe.patch_files = [File.expand_path("path/to/libiconv-1.14-ja-1.patch")]
recipe.cook
recipe.activate
```

[libiconv-1.14 日本語パッチ](http://apolloron.org/software/libiconv-1.14-ja/) にて公開されている日本語エンコーディングに関するパッチを当てる場合などは上記のようにすればよい。
ただし、 `MiniPortile#patch_files` は `MiniPortile#files` と違い自動でダウンロードしてくれないため事前にダウンロードしておく必要があることと、相対パスを指定した場合うまくいかなかったため `File.expand_path` で絶対パスにしてやる必要があった。
さらに内部的に `git-apply` を呼び出しているため、 git のインストールが必須となっている。

### mini_portile デフォルトでは対応できない場合

基本的には `./configure` が色々と吸収してくれるはずだが、 zlib のように Windows にインストールする場合は別の手順を行う必要があったりと、 mini_portile が想定している方法通りではうまく行かないケースが多々存在する。
ではどうするかというと、 [利用例のRakefile](https://github.com/flavorjones/mini_portile/blob/master/examples/Rakefile) を見る限り mini_portile を継承したクラスを作成し、その中にライブラリ固有の処理を書く必要があるようだ。

`MiniPortile#cook` は下記メソッドを呼ぶだけの処理のため、これらを適時オーバーライドしてやればよい。

* MiniPortile#download
* MiniPortile#extract
* MiniPortile#patch
* MiniPortile#configure
* MiniPortile#compile
* MiniPortile#install

サンプルは長くなりそうなので、上記の Rakefile を参照してほしい。

おまけ：mini_portileをあえて任意の場所にインストールする用途に使ってみる
-----------------------------------------------------------------------------

話は変わるが、筆者は eucJP-ms が適切に処理出来なければならない環境にいるため、 iconv に上記日本語パッチを当てたものを度々コンパイルする。
shell script で書いてもいいのだが、今回あえて mini_portile を使ってやってみようと思った。
mini_portile は任意の場所にインストールすることを想定していないため、そのあたりに手を入れてやる必要がある。
また、前述の通りパッチファイルは自動でダウンロードしてくれないため、自動でダウンロードしてくれるように手を入れた。

```ruby
require "mini_portile"

class PatchedPortile < MiniPortile
  attr_writer :port_path
  attr_accessor :patch_urls

  def initialize name, version
    super name, version
    @patch_urls = []
  end

  def download_patch
    @patch_urls.each do |url|
      filename = File.basename(url)
      download_file(url, File.join(patches_path, filename))
      @patch_files.push File.expand_path(File.join(patches_path, filename))
    end
  end

  def cook
    download_patch
    super
  end

  private

  def port_path
    @port_path.nil? ? super : @port_path
  end

  def patches_path
    "#{@target}/patches"
  end
end

class LibiconvRecipe < PatchedPortile
  def initialize version
    super "libiconv", version
    @files << "http://ftp.gnu.org/pub/gnu/#{@name}/#{@name}-#{@version}.tar.gz"
  end
end

recipe = LibiconvRecipe.new "1.14"
recipe.patch_urls << "http://apolloron.org/software/libiconv-1.14-ja/libiconv-1.14-ja-1.patch"
recipe.port_path = ENV['LIBICONV_INSTALL_DIR'] || "/usr/local/#{recipe.name}-#{recipe.version}"
recipe.configure_options = [
  "--host=#{recipe.host}",
  "--enable-shared"
]
recipe.cook
recipe.activate
```

これで `LIBICONV_INSTALL_DIR=/path/to/libiconv ruby script.rb` とすれば `/path/to/libiconv` にインストールされ、環境変数 `LIBICONV_INSTALL_DIR` を指定しなければ `/usr/local/libiconv-1.14` にインストールされる。
アンインストールコマンドが提供されていないため簡単に入れ替えが出来るようなデフォルトインストール先としているが、環境変数をごにょごにょ弄りたくないという場合であれば `LIBICONV_INSTALL_DIR=/usr/local` としてもよいのかもしれない。

実際に使ってから思ったのだが、やはりこういう用途で使用するならば Ruby で書くよりも shell scriptで書いたほうがよいようである。
筆者環境では Windows でコンパイルすることもあるため Ruby でやったほうが便利ではないか？とも思ったが、この方法では Cygwin や MinGW/MSYS、さらに Git for Windows をインストールする必要があるため、それならば bash も入っているから shell script で充分ではないか？という話であった...。
自動でパッチをダウンロードするぐらいは mini_portile で対応してくれたら嬉しそうなので、気が向いたら修正して PR するかもしれない。