bundle install を早くする唯四の方法

皆さん、bundler 使用していますか?(はーい!という声)そうですね、とても便利ですね。でも遅い。何より速さが足りない。そう思うことも時折あるのではないでしょうか。bundle install に数分間かかるのはよくある話、ときによっては10分以上も bundle install だけでかかってしまう。しかも遅いときに限って何も表示されない。壊れてる?…と思って放置してごはんを食べて戻ってくるといつの間にか終わってたりする。別に bundler 自体が悪いわけではないですが、不安になります。
「俺の bundler がこんなに遅いわけがない。もっともっと速くなればいいのに」、そう思ったあなたのためにこの記事です。bundle install 時間を短くするただ一つ、いや四つの方法をご紹介します。

rubygems のバージョンを適切にアップデートする

あまり知られてない気がしますが、gem のバージョンによっては bundle install がものすごく低速になることがあります。私が経験したところでは、 2.0.3 では 1.8.25 の 3 倍程度遅くなっていました。

$ gem --version
2.0.3
$ sudo gem update --system

バージョンを確認して 2.0.3 以下の 2.0 系だった場合、アップデートしておきましょう。2.0.7 は速度的には問題なさそうです。

https://rubygems.org ではなく http://rubygems.org を利用する

標準で利用される source 'https://rubygems.org' よりも、 source 'http://rubygems.org' としたほうが微妙に bundle install が早くなります。下の画像はさくらの VPS にて bundle install を試したときの時間を計測したものですが、 http のときは https と比較して1.5倍近く高速化されていますね。

http proxy がはさまれている環境の場合は、さらに早くなる可能性もあります。
ただ、http にすれば当然ながら公開鍵認証による信頼の恩恵は受けられませんし、本家 bundler にて推奨しているのは https://rubygems.org です。このあたりは自己責任でプリーズ。

並列 bundle install を利用する*1

1.4.0.pre.1 より、並列 bundle install が可能になっています。これは爆速です。ぜかましです。本家 bundler の issue によれば、特に RTT が長いときに効果を発揮するとのこと。ちなみに source 行の http/https の違いは並列化でかなり吸収されますので、並列 install を利用する場合はわざわざ source 行を http に書き換える必然性は薄いです。
雑な計測*2ですが、私の環境ではこんな結果が出ました。

$ gem install bundler --version='1.4.0.pre.2'
$ time bundle install --path=.bundle/gems --binstubs=.bundle/bin
real    4m32.956s
$ rm -rf .bundle/gems .bundle/bin
$ time bundle install -j 10 --path=.bundle/gems --binstubs=.bundle/bin
real    0m39.983s

この機能の原作者は @eagletmt 先生。利用する際はありがとうを三唱しましょう。
ただ、正式版ではなく pre の機能*3 ということはお忘れなく。メモリやCPUも使いますし。これも自己責任ですね。

インストール時間を表示させる

bundler は多くの gem を扱うわけなので、そのうちのひとつの gem の install が長くなると、全体としての bundle install 時間も一気に遅くなってしまいます。
具体的には、therubyracer 0.11.0 問題とか、Nokogiri 1.6.0 問題とかですね。
こういった問題に遭遇していたら、install の遅かったやつを探す必要があるわけですが、現状の bundler だとどこが遅いのか、よくわかりません。
ところで MOGOK では bundle install の時間が表示されるのが地味に便利です*4

Bundler installing..
2013-09-08T14:26:02+09:00 console[app2002.22]:   $ bundle install --path=.bundle/gems --binstubs=.bundle/bin --without=test development
2013-09-08T14:26:02+09:00 console[app2002.22]: Fetching gem metadata from http://rubygems.org/...........
2013-09-08T14:26:09+09:00 console[app2002.22]: Fetching gem metadata from http://rubygems.org/..
2013-09-08T14:26:10+09:00 console[app2002.22]: Installing rake (10.1.0)
2013-09-08T14:26:10+09:00 console[app2002.22]: Installing i18n (0.6.5)
...
2013-09-08T14:26:16+09:00 console[app2002.22]: Installing activeresource (3.2.14)
2013-09-08T14:26:16+09:00 console[app2002.22]: Using bundler (1.1.3)
2013-09-08T14:26:16+09:00 console[app2002.22]: Installing json (1.8.0) with native extensions
2013-09-08T14:26:18+09:00 console[app2002.22]: Installing libv8 (3.16.14.3)
2013-09-08T14:26:18+09:00 console[app2002.22]: Installing rack-ssl (1.3.3)
...

これをローカルでも実施できるよう、 bundle install に時刻表示をつけてみます。なんとなくこんな感じで Gemfile にモンキーパッチを書いておけばそれっぽく表示できます。

class << Bundler.ui
  def tell_me (msg, color = nil, newline = nil)
    msg = word_wrap(msg) if newline.is_a?(Hash) && newline[:wrap]
    msg = "[#{Time.now}] " + msg if msg.length > 3
    if newline.nil?
      @shell.say(msg, color)
    else
      @shell.say(msg, color, newline)
    end
  end
end

source 'http://rubygems.org'
gem 'libv8', '~>3.11.8.17'
gem 'therubyracer'

表示がこんな具合↓になるので、ボトルネック探しがはかどりますね。

$ bundle install --path=.bundle/gems --binstubs=.bundle/bin
[2013-09-08 14:38:26 +0900] Fetching gem metadata from http://rubygems.org/..
[2013-09-08 14:38:27 +0900] Installing libv8 (3.11.8.17)
[2013-09-08 14:38:28 +0900] Installing ref (1.0.5)
[2013-09-08 14:38:46 +0900] Installing therubyracer (0.11.4)
[2013-09-08 14:38:46 +0900] Using bundler (1.4.0.pre.2)
[2013-09-08 14:38:46 +0900] Your bundle is complete!
[2013-09-08 14:38:46 +0900] It was installed into ./.bundle/gems

ただ、これは bundler の内部構造に依存したモンキーパッチなので、バージョンによっては使用できなかったり、最悪の場合は機能を壊してしまう可能性があります。これも自己責任。

以上。

これ全部やれば、人によっては bundle install の時間が 1/10 くらいに縮むかもしれません。
システムワイドにインストールするとか、複数のプロジェクトで同じ BUNDLE_PATH を利用する方法もありますが、それらはあまりお勧めできないかなーと思います。bundle install は --path 指定をし、プロジェクトごとに別々の gem を使うようにしましょー。

Have a happy bundle life!

*1:1.4.0.pre.1 〜

*2:Gemfile.lock も .bundle/config も残ってる

*3:pre.2 の段階では並列のオプションが保存されないとか、pre1 では :github の互換性が崩れていたとか、まだ十分に叩かれていないかも知れないとか、画面表示が変わるとか、いろいろ

*4:ステマ

Nginx Proxy の下で GrowthForecast を動かした話

GrowthForecast を動かしたい欲がさきほど突然現れたので、さくらのVPSUbuntuをセットアップしてGrowthForecastを設定してみた。
インストール方法はさておいて、設定するときに注意すべき点が幾つかあったので、それらについて列挙してく。

目的

127.0.0.1:5125 で立てた GrowthForecast を、同ホスト上に立てた Nginx でリバースプロキシし、 http://example.com/growthforecast という URL で見られるようにすること。
サブドメインを使いたくないようなケースね。

想定してるのは、外部に GrowthForecast のグラフを見せ、データの挿入は 127.0.0.1 以外から許さないような用途。

Nginx での基本的な Proxy 設定

とりあえず GET/HEAD だけ許容して 127.0.0.1:5125 に Proxy しとこう。

location /growthforecast {
  if ($request_method !~ ^(GET|HEAD)$) {
    return 403;
  }
  proxy_pass http://127.0.0.1:5125;
}

って思うじゃん。

ダメなんです

ところが、これだとこんな画面になって失敗する。

原因は、ページ内のすべてのリンクが http://127.0.0.1:5125/ を向いてしまっていること。実にロケンロー。
ただ、こんな問題を本家が認識していないはずもなく、 https://github.com/kazeburo/GrowthForecast/issues/3 には対処法が書かれている。

GrowthForecast で行うべき設定

GrowthForecast では、前段に proxy が挟まる場合にはその設定を行う必要がある。と言っても、起動オプションを一つ追加するだけだ。

 $ growthforecast.pl --front-proxy=127.0.0.1

こっちはこれで問題ない。

Nginx で行うべき設定

Nginx 側としては、次の二点を行うことになる。

  • パスの書き換え
  • ホスト名の書き換え
パスの書き換え

Nginx で特に設定を行わない場合、 http://example.com/growthforecast/ にアクセスすると、GrowthForecast 側には /growthforecast/ というパスが伝わる。ここは当然 / であることを期待しているわけなので、 Rewrite が必要となる。

  rewrite ^/growthforecast/(.+) /$1 break;
  rewrite ^/growthforecast / break;

書き換えルールくらい一行で書けそうなものだけど、なぜかうまくいかなかったので二行で無理やり解決。

ホストの通知

GrowthForecast では、HTML中にある各種リンクがすべてフルパスで指定されている。で、プロキシの裏側にいる場合は 127.0.0.1:5125 で立っているもんだから、ページ内のリンクはすべて http://127.0.0.1:5125/ に対するリンクになってしまう。
先ほどの --front-proxy オプションをつけるのは、これを解決させるためだ。
ここで指定されたアドレスからの接続であった場合、先ほどの 127.0.0.1:5125 の代わりに指定されたホスト名を使用するようになる。
つまり、 Nginx 側でも渡すホスト名を指定してやらねばならない。

  proxy_set_header Host $host/growthforecast;

この設定を入れることで、このアドレス問題が解決されるわけ。

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html" charset="utf-8">
<link rel="stylesheet" href="http://example.com/growthforecast//css/bootstrap.min.css">
<style type='text/css'>
body {

そんな感じ。

何が起きているか?

Nginx からのメッセージを netcat とかで適当にキャプチャすればよくわかる。

$ nc -l 5125
GET /growthforecast HTTP/1.0
Host: 127.0.0.1:5125
...

適当に設定した Nginx では GrowthForecast に対してこういうメッセージが来ていたところ、

$ nc -l 5125
GET / HTTP/1.0
Host: example.com/growthforecast
...

こんな HTTP Request が来るようになるわけ。

まとめ

以上まとめると、次のような感じになる。

Nginx 設定
location /growthforecast {
  if ($request_method !~ ^(GET|HEAD)$) {
    return 403;
  }
  rewrite ^/growthforecast/(.+) /$1 break;
  rewrite ^/growthforecast / break;
  proxy_pass http://127.0.0.1:5125;
  proxy_set_header Host $host/growthforecast;
}
GrowthForecast 起動オプション
 $ growthforecast.pl --front-proxy=127.0.0.1


オッケー☆

知っていればなんてことのない話なのだけど、ブログにまとまっているところを見かけないので、ちょっと残してみることにした。

ただ今回の設定だと URL の途中にダブルスラッシュ入ってかっこわるい。リダイレクトルールの書き方とか、もっといい設定がありそう。

Ruby と exec と fd と

ruby で exec するときにソケットや fd のリークを起こさないために。主に自分用の調査結果のメモ。

結論

ruby 1.9.1 以上で exec するときは、何はなくとも :close_others をつける
ruby 2.0.0 以降ではこれを設定しなくても fd リークはしない
ruby 1.9.0 以前は人力で必死に頑張る

基本的な話

ruby の exec は基本的には POSIX execve のラッパ。execve 前後では多くのものが保持されないが、ファイルディスクリプタは基本的には残る。

デフォルトでは、ファイルディスクリプタは execve() を行った後でもオープンされたままである。 close-on-exec の印が付いているファイルディスクリプタはクローズされる。

http://linuxjm.sourceforge.jp/html/LDP_man-pages/man2/execve.2.html

ということで、fork-exec や exec を行いたい場合は fd の扱いに気を付ける必要がある。
また、exec を行う時点で open しっぱなしになっている可能性のある fd に関しては、 close-on-exec のフラグを付けておけば execve 内で close してもらえる。
ruby 1.9.3 以前では、この close-on-exec は自分で設定する必要がある。

f = File.open("/tmp/hoge")
f.close_on_exec = true

close-on-exec は ruby 2.0.0 以降ではデフォルトになっている*1。そのため、 exec による予期せぬ fd リークは起こりにくくなっている。

困るシーン

他の人が使用するフレームワーク系ライブラリの中で exec を使用する場合、そのライブラリユーザが開いた fd やソケットが開きっぱなしになってしまう可能性がある。やばい。宇宙やばい。マルチスレッドならさらにこのような問題が起こりやすくなる。

ユーザの知恵はそんなものを乗り越えられるかというとそんなことはなくて、ならば今すぐ ruby 1.9.3 ユーザ全員の IO::open に close_on_exec = true を授けて見せろと言われてもそうだよそれはできないから、ruby 2.0.0 になってからそうさせてもらうと言うことになった。

では 1.9.3 ではどうすればよいかというと、 Kernel#exec (及び #spawn) に :close_others というオプションがあり、stdin/stdout/stderr 以外の fd をすべて調べて閉じてくれる仕組みがある。しかしこれは単に ruby 処理系にて C で書かれているだけなので、どうやら標準ではなさそうだ。少なくとも execve にはそのようなオプションはない。

f = File.open("/tmp/hoge")
exec("ls /proc/$$/fd", :close_others = true)

1.9.2 1.9.0 以前は close_others の実装がない(1.9.1 より実装された)。そのため、close_others オプションによる fd リーク防止は効果を持たない。close_othersはつけておいても特に害はない。

2.0.0 以降は、 fd ごとに close_on_exec が設定され、exec 時の close を保証してくれる。したがって、意図的に fd を残したい場合を除いては自動的に fd は exec 時に close されることとなり、 :close_others の設定は不要である。とはいえ、close_others を付けていてもそう問題は起きないだろうし、つけておいても問題はない。むしろ、ユーザの不注意でつけられた close_on_exec = false な fd も閉じてくれるので、使い勝手はよいかもしれないくらいだ。

そういうわけで結論(再掲)は

ruby 1.9.1 以上で exec するときは、何はなくとも :close_others をつける
ruby 2.0.0 以降ではこれを設定しなくても fd リークはしない
ruby 1.9.0 以前は人力で必死に頑張る

ということになった。まあ 1.9.0 以前って普通は 1.8.7 以前のことだけど。

補足

むしろ、ユーザの不注意でつけられた close_on_exec = false な fd も閉じてくれるので、使い勝手はよいかもしれないくらいだ。

とさっき書いた。「fd を勝手に閉じられたら困る、という話があるのでは??」と思うかもしれないが…、

本来、Kernel#exec 前後で開いたままにするべき fd は、exec (spawn) のオプションとして明示的に渡しているはずなので、それ以外の fd はすべて閉じるべき。

# たとえば fd 7 番を残したいのであれば
exec("ls -ls", 7 => 7, :close_others => true)

また、close_on_exec は IO::open 時のオプションに設定できないので、ruby 1.9.3 以前では open と close_on_exec 設定とがアトミックな処理にならない。つまり、その間に exec が発生する可能性があり、fd 漏れの起こりうる箇所になる。
その点 :close_others => true なら exec 時に確実に設定できるので、fd 漏れのタイミングは生まれない。多分。

ただ、IO#close_on_exec は POSIX 標準に対するインターフェイスである一方で、:close_others => true は ruby の独自実装。なので、現実的には :close_others を付けるのが正しそうなんだけど、あんまり美しくない感はあって悔しいかもしれない。

そんな感じ。

therubyracer 0.11.0 問題まとめ

Rails 3.1 以降で良く使われるようになった therubyracer ですが、最近、これを含む bundle install がやたらと時間がかかったり、あるいはそもそも失敗してしまう、という事象が多数報告されています。

解決方法

以下の三つの方法が基本的な解決方法です。

その0 (※ 2013/1/11 追記)

Gemfile にて、libv8 3.11 系を使用することを明記します*1 *2

gem 'therubyracer', '0.11.1'
gem 'libv8', '~> 3.11.8.13'
その1

Gemfile にて、 therubyracer のバージョンを 0.11.0beta8 に戻します

gem 'therubyracer', '0.11.0beta8'
gem 'libv8'
その2

Gemfile にて、 therubyracer のバージョンを 0.10.2 に戻します

gem 'therubyracer', '0.10.2'
その3

Gemfile にて、 libv8 のバージョンを 3.11.8.3 に戻します

gem 'therubyracer'
gem 'libv8', '3.11.8.3'

これらのうちどれでも解決しない場合は、もともと therubyracer がうまく動かない環境ですので、新しいバージョンが出るのを待つか、あるいは自前で v8 のエンジンを導入してください。

なお、環境ごとの therubyracer および libv8 のバイナリ提供バージョンを確認されたい場合、 https://github.com/cowboyd/libv8/issues/62 こちらの表が便利です。

何が起きているの?

therubyracer は、JavaScript のエンジンである v8 *3Ruby から使えるようにする gem です。therubyracer では、v8 エンジンがシステム上で利用できる場合はそのエンジンを、また利用できない場合には libv8 という gem を取得して、各環境ごとに利用できるバイナリの v8 エンジンをこの libv8 gem から取得するようになっていました。賢いです。

ところがこの「バイナリの v8 エンジンを利用できる」という前提が、 0.11.0 に入って崩されてしまいました。therubyracer 0.11.0 が依存している最新の libv8 3.11.8.4 は、 v8 エンジンをソースコードからコンパイルしようとします。ソースコードからコンパイルすると、環境次第ですが、2分〜15分程度かかったという報告もありますし、そもそも環境が整っていないとコンパイル自体失敗します。

therubyracer 0.10 系では何も考えずに(ものの3秒で)インストールできていたものが、どうしてこんなことに…なってしまったのでしょーか。

どうしてこうなった

therubyracer の作者 @cowboyd は以前より、とある問題に頭を悩ませていました。
「therubyracer は特定のバージョンの libv8 のバイナリを入れることを強制してしまっている…。これは多くの環境では上手くいくけれど、このバイナリが動かないような環境では therubyracer を使う方法がなくなってしまう」
この問題への対応として彼は、0.11系でバージョン依存性を弱くしようとしました。また、新しいバージョンの libv8 へと依存するよう関係を変更しました。
ところがこの新しい libv8 が作成するのに失敗しており、バイナリが壊れている状態でリリースされてしまいました*4。この結果 therubyracer はあまりに多くの環境で動作しなくなってしまいました。
そして彼は、緊急でとある方策をとりました:
 
「とりあえずソースコードからコンパイルさせればいっか!」
 
い い わ け な い
 
先ほども言いましたが、v8のコンパイルは、要はひとつの言語処理系のコンパイルなのでそれなりに時間がかかります。Rails3.1 初期状態の bundle install はおおよそ1分くらいですが、libv8のコンパイルにはそれこそ10分とるわけなので、この影響は甚大でした。

@cowboyd にとっては、バージョン依存による環境縛りを外せるという意味で、若干いけてないけれどもまあ意味のあるワークアラウンドを出したなーと、この時点では思っていたように思います。
しかし、多くの利用者が欲しているのは「何も考えなくても入っていた therubyracer という gem」でした。
そのことを指摘した発言が出て、 @cowboyd はそのことの価値を明確に意識するようになります。

I'm very curious how 0.10.x versions of therubyracer managed to "just work" on nearly everyone's system (I never encountered or heard of anyone encountering a problem)(中略), and what changed in 0.11.x to make this no longer work. I am very familiar with "unix dependency hell"(中略) -- but therubyracer prior to 0.11.0 managed to avoid this and be an awesomely great install experience which just worked for everyone.

issues/215 @jrochkind

You make a valid point, but since this is the first positive feedback about the old system I have received , I guess I didn't really realize its value up until now. (and thank you by the way)

issues/215 @cowboyd

誰からのお礼やフィードバックがなかったがゆえ、彼は、自分の作った therubyracer が「簡単にインストールできる」ということがいかに大きな価値を持っていたか十分に認識していなかったのですね。応援とフィードバックは大事ですね。だれか私も応援してください。わぁい。

therubyracer の問題

以上の問題をまとめると、次の三つの要素が絡み合っていることになります。

  • gem 'therubyracer' としたら何も考えなくてもインストール成功してほしい
  • libv8 はバイナリで入ってほしい
  • バイナリバージョンが上手くいかない人のために、v8 エンジンを選択的に利用できてほしい
gem 'therubyracer' としたら何も考えなくてもインストール成功してほしい

今回はバージョンアップに伴って therubyracer の依存性が壊れたわけですが、ユーザは以前のバージョンを指定することで回避は可能でした。ただ、ライブラリ提供者としては、そういった回避手段をユーザ誰もに期待することはできません。特に therubyracer は、Rails 3.1 以降では標準的に使用され、多くの初心者が利用するライブラリですからなおさらです。
と言うわけで、gem 'therubyracer' その一行だけで全てが問題なく動くということは、とても大きな価値なのでした。

libv8 はバイナリで入ってほしい

これは言わずもがなですね。バイナリバージョンが入ってくれないと、数分〜十分以上の待ち時間が出てきます。しかも、このインストールはデプロイのたびに発生します。これはちょっとした恐怖ですよ。バイナリバージョンの libv8 は therubyracer では今や必須です。

バイナリバージョンが上手くいかない人のために、v8 エンジンを選択的に利用できてほしい

@cowboyd が大胆な変更をして実現しようとしていたのがこちらの機能です。似たようなケースとして、セキュリティリスクに敏感なため、その libv8 のバージョンは利用できない、という人がいるということもあるようです。ともあれ、ここで達成したい目的は、libv8 のバージョン指定をより自由にしたい、と言うことです。そしてそれは、一つ目の「gem 'therubyracer' が成功してほしい」という願いとわりと衝突しやすいものです。

この三つの問題をどうまとめて解決策を出すか、それを考えるのに @cowboyd は二週間悩みました。そして12/18にとうとう、今後の解決策を提案したのです。

解決策、その後

@cowboyd は、次の解決策を示しました。

  • libv8 と therubyracer とが互いに依存するようにする
  • libv8 は v8 への proxy としてふるまう
  • ソースバージョンとバイナリバージョン、両方をリリースする

前半二つは実装の話なので省略、最後の一つが面白いのでご紹介します。
今後の libv8 のリリースでは、「最新バージョンは常に binary 入り、そのひとつ前に source 版の libv8 を準備する」というリリース方針になるようです。

3.11.8.4 -> source
3.11.8.5 -> binary

こんな感じです。バイナリ版の libv8 がうまく動かないようだったら、バージョンを一つ戻してソース版をインストールすればいい、そういう設計になる予定のようです。
この変更はそれほど時間がかからない予定だそうですが、本家のコミットに動きが一切ないのでちょっと心配です。

ともあれ、今後も therubyracer は「インストールは簡単で、サクサク動くよ便利!」という方向は持ち続けたままメンテナンスされていくようなので、一安心です。私は次のリリースが来るまでは素直に therubyracer 0.10.2 を使い続けることにします。

ひとこと

問題は、 gem update とか bundle install で最新バージョンを無理やり取ってきてしまうとか、バージョン番号が四ケタになっているやつは '~>' でのバージョン指定でマイナーバージョンが上がってしまうとか、そういうところな気もしますけど、かといって私には、バージョン全指定以外に確実な方法なんて思い浮かばないので、この rubygems 式指定法とどう付き合っていくかなんでしょうね。

まとめ

がんばれ。
 
あとソフトウェア製作者への応援大事。
 

追記

あ、応援ブクマありがとうございます、普段がいかにもオメガブロガーなもので初の10ブクマ越えになりました。
わぁいブクマ suu-g ブクマ大好き

追記2 (2013/09/08)

therubyracer 0.12.0 でも同じ問題があるとのブクマコメを頂きました。
0.12.0 では、therubyracer は libv8 3.16.14 系に依存するように変更が加えられました。いまの最新の 3.16.14.3 は x86_64-darwin-12 および x86_64-linux に対するバイナリパッケージが提供されていますので、これらの環境では libv8 のビルドは行われずに済みそうです。言い方を変えると、それ以外の環境、たとえば mingwdarwin-10、32bit Linux といった環境では、バイナリパッケージが入らずに困る可能性があります。

*1:これを書かないと、未対応なのに投入された 3.15 系が入るかもしれません

*2:以前に何度か bundle install を試している場合は一度 bundle clean を行った方がよいです

*3:http://code.google.com/p/v8/ V8 JavaScript Engine

*4:issues/215 を読む限り。多分

yamlにRubyのクラス名を入れて設定ファイルとして利用する

Rubyで設定ファイルを作るとき、yamlファイルを利用することがある。そのyamlファイルの中に、ユーザ定義のクラス名を入れて、設定ファイルによって利用するクラスを変更するようなことをしたい。と思った時のためのやり方。

まあ、yamlクラスにあるんですけど。

yaml では、Ruby 向けに以下のローカルタグを扱えます。
・ !ruby/class: Class オブジェクト

http://doc.ruby-lang.org/ja/1.9.3/library/yaml.html

「この記法はRuby独自の方法だろうし、利用するエンジンによって実装が異なるだろうし使いたくないな!」と思っていたのだけど、調べてみるとこの !〜〜 という書き方はYAML1.1以降の正式なユーザ定義タグの仕様に則ってた。
http://www.yaml.org/spec/1.2/spec.html

ただ、この記法を使う場合、yamlファイルをパーズする時点でこのタグに相当するクラスが定義されていないとエラーが起こってしまうので、不便なときがある。

それと、Ruby以外で読めないyamlファイルになってしまう、という問題もないではない。

yamlから文字列として取得して、それをクラスにする

yamlファイルにはクラス名を文字列として格納しておいて、利用するときに改めてクラス名として展開する、というのが平和な解決法に思える。

文字列をクラスにする方法はググればそれなりに見つかる。大きく分けると二つ。

  • eval する
  • const_get する

evalは、与えられた文字列がクラス名だったら良いけれど、yamlファイルに "`sudo rm -rf ~/*`" とか書かれてるだけで死ぬので使えない。

const_getなら使えそうに見えるけど、階層が深くなると…

irb(main):001:0> Object.const_get("Hash")
=> Hash
irb(main):002:0> require 'net/http'
=> true
irb(main):003:0> Net::HTTP
=> Net::HTTP
irb(main):004:0> Object.const_get("Net::HTTP")
NameError: wrong constant name Net::HTTP
        from (irb):4:in `const_get'
        from (irb):4
        from :0

とまあ、こうなる。

Object.const_getが行っているのは文字通り、クラス名という定数(const)をObjectから取り出しているだけ。Objectの中には "Net::HTTP" という定数はないので、失敗する。というからくり。

でも、Object直下に "Net" 自体はあるので、

irb(main):005:0> Object.const_get("Net")
=> Net
irb(main):006:0> Object.const_get("Net").const_get("HTTP")
=> Net::HTTP

こうやって辿っていくことで、Net::HTTPを取得することができる。

したがって、任意のクラス名をクラスとして解釈するワンライナーは、例えばこう。

"Net::HTTP".split("::").reduce(Object, :const_get)
=> Net::HTTP

わりと常用しそうなテクニックなのに、誰も書いてない気がしたので記事化してみた。
reduceよりinjectが好きならそちらでどうぞ。

rbenvを全ユーザ用にインストールする

システムで利用するrubyのバージョン管理にaptやyumは使いたくないので、rbenvでやってしまいましょう。という話。

rbenv のインストール

インストール場所は /opt/ruby 以下。別に/usr/localでも問題ないけど、私の管理ポリシ的にそうしてる。

# mkdir -p /opt/ruby
# cd /opt/ruby
# git clone git://github.com/sstephenson/rbenv.git

/etc/profileに追記する、のは緊急時の影響が大きいので、rbenv環境変数系はまとめて1ファイルにしてユーザごとにsourceさせるように変更するとよい気がする。/etc/profile内でevalしたくはないよね。

# cat >> /etc/profile

RBENV_ROOT=/opt/ruby/rbenv
export RBENV_ROOT

PATH=/opt/ruby/rbenv/bin:${PATH}
eval "$(rbenv init -)"
^D

ここまでは、まあ、普通。

ruby-build のインストール

ruby-buildは便利なのだけど、rubyのバージョンごとに設定ファイルが作られていて、新しいバージョンのrubyを入れようと思ったらこのruby-buildごとインストールしなおさなくてはいけなくて、残念。

もういっそinstallなんてしないでも、git pull してくるだけで新バージョンの情報が手に入るようにしてみた。

# cd /opt/ruby
# git clone git://github.com/sstephenson/ruby-build.git
# find /opt/ruby/ruby-build/bin/* | xargs -I{} ln -s {} /opt/bin
# cat >> /etc/profile

PATH=${PATH}:/opt/bin
^D

インストール手順は以上で終わり。よくできているので、間違えなければやるべきことは少ない。

rubyをinstallする

うちの子の前準備。うちの子gcc入ってなかった。悲しかった。うちの子の名前何がいいですかね。いまのところ「はるだんじ」なんですけど。嘘ですけど。

# apt-get update; apt-get install -y build-essential automake autoconf cmake
# source /etc/profile

rubyを入れる。インストール可能なバージョンを表示するためには rbenv install -l と、知らないうちにオプションが必要になってた。

# rbenv install -l
...
# rbenv install 1.9.3-p327
# rbenv rehash
# rbenv global 1.9.3-p327

アップデートしたいとき

新しいrubyのバージョンが出て、アップデートしたいと思ったとき、通常であれば

$ rbenv install -l
...                                                           <== …あれ?新しいrubyバージョンがない
...                                                               rbenvをアップデートしなきゃ!
...                                                               rbenvじゃなくてruby-buildの方…?
$ cd /tmp
$ git clone https://github.com/sstephenson/ruby-build.git     <== このURLを知るためにググる
$ cd ruby-build
$ sudo ./install.sh                                           <== 一瞬で終わって不安…
$ rbenv install -l                                            <== あ、出たでた。良かった…

という感じの行動をとるので非常にやる気を削がれる。しかもインストールできてもそれまで入れたgemが引き継がれてなくて二度ガッカリする。*1

今回の記事で作った環境であれば、

$ cd /opt/ruby/ruby-build
$ sudo git pull
$ rbenv install -l

で新しいrubyバージョンの定義ファイルが入るので、時間とやる気を節約できる。


リンク。 http://yatmsu.hatenablog.com/entry/20120413/1334300058

*1:gem引継ぎがないことは利点にもなるので、この記事では対処してない

BundlerでFizzBuzz

今年もFizzBuzzの季節がやってまいりました。

FizzBuzzを書くことのできない言語を使えると口にすることはとても恥ずかしいことらしいので、きっとFizzBuzzを書くことのできないライブラリを使ってることも恥ずかしいことなのだろうと思います。

私も、ほとんどのライブラリはFizzBuzzしたことがありませんでした。このままではソーシャル転職をすることができません!将来のキャリアプランが台無しです。

そういえば最近、Ruby on Railsの世界ではBundlerというツールが流行っているらしいので、これを使ってFizzBuzzすることにしてみました。

続きを読む