https://github.com/sstephenson/rbenv/wiki/Understanding-binstubs の翻訳です。かなり適当です。誤訳等あったらご指摘いただければと思います。
rvm seppku がすごく時間かかって暇なので、その間に rbenv について調べてたら発見したのでした。
binstub を学ぼう!
binstub は実行可能ファイル(いわゆるバイナリー、コンパイルされているとは限らない)をラップしているスクリプトで、その目的は元の実行可能ファイルを呼び出す前に環境を整えることである。
Ruby の世界でいうと、RubyGems は実行可能ファイルを含んだ gem をインストールした後に、そいつの binstub を生成していたりする。binstub はどんな言語でも書けるから、自分自身で書いてみても良いんじゃないかな。
Ruby Gems
gem install rspec-core
を叩いた時に何が起こるか見てみよう。RSpec は実行可能ファイルをもっていて、gem 内の ./exe/rspec
に配置されている。インストール後、RubyGems は実行可能ファイルと共に以下のものを用意してくれる:
-
<ruby-prefix>/bin/rspec
(RubyGems によって作られた binstub) -
<ruby-prefix>/lib/ruby/gems/1.9.1/gems/rspec-core-XX.YY/exe/rspec
(オリジナル)
最初のファイルは、2番目のファイルをラップする為につくられた binstub である。RubyGems が何故 <ruby-prefix>/bin
にこの binstub を配置するかと言うと、そいつが既に $PATH
に含まれているからだ。(それが Ruby version manager の仕事だ)
RubyGems がインストールした2番目のファイルのディレクトリ(オリジナルの方)は $PATH
に含まれていない。だからと言って、直接実行しようとしてはいけない。なぜなら、Ruby のプロジェクトは基本的にはセットアップ無しで呼ばれる事を想定していないからだ。最低限、プロジェクトのファイルを読み込めるように $RUBYOPT
がセットされている必要がある。
この生成された binstub <ruby-prefix>/bin/rspec
は、ちっちゃい Ruby のスクリプトだ。簡潔で簡素な全貌をお見せしよう。
#!/usr/bin/env ruby
require 'rubygems'
# gem の lib ディレクトリと gem の依存先を $LOAD_PATH に追加する
gem 'rspec-core'
# Loads the original executable
load Gem.bin_path('rspec-core', 'rspec')
RubyGems の要は、オリジナルの実行可能ファイルを呼び出す前に $LOAD_PATH
を追加する所にある。
rbenv
rbenv は自分自身の "shims" ディレクトリを $PATH
に通している。この "shims" には、全ての Ruby に関連した実行可能ファイルの binstub が含まれている。ruby
や gem
、そして Ruby のバージョン毎にインストールされた RubyGems の binstub がある。
rspec
を叩いた時、こんな調子で実行される。
-
$RBENV_ROOT/shims/rspec
(rbenv shim) -
$RBENV_ROOT/versions/1.9.3-pXXX/bin/rspec
(RubyGems binstub) -
$RBENV_ROOT/versions/1.9.3-pXXX/lib/ruby/gems/1.9.1/gems/rspec-core-XX.YY/exe/rspec
(original)
rbenv の shim も、簡潔で簡素な短いシェルスクリプトだ。
#!/usr/bin/env bash
export RBENV_ROOT="$HOME/.rbenv"
exec rbenv exec "$(basename "$0")" "$@"
rbenv の shims の要は、全ての ruby の呼び出しを rbenv exec
を通すことで、これによって正しい Ruby のバージョンを選択する事ができる。
プロジェクト専用の binstub
rspec
コマンドを叩いた時、rbenv はそのプロジェクトに設定された Ruby のバージョンをきちんと選択する事ができる。ただし、RSpec のバージョンは正しく選択されない。RubyGems は、そのプロジェクトが古いバージョンの RSpec に依存しているのではない限り、シンプルに最新の RSpec を起動させるのが原因である。プロジェクトというコンテキストにおいては、これは期待した振る舞いではない。
なぜ bundle exec <command>
が必ず必要になるのだろうか?それは、依存関係から正しいバージョンを解決して、Ruby の実行環境の矛盾を解決する為だ。しかしながら、毎回毎回 bundle exec
と書くのは、辛ぽよ。
Bundler が生成する binstub
Bundler によって、プロジェクトに必要な実行可能ファイル群の為の binstub をインストールすることができる。
# generates binstubs for all gems in the bundle
bundle install --binstubs
# generate binstubs for a single gem (requires Bundler v1.3)
bundle binstubs rspec
これは、./bin/rspec
にこんな感じで生成される。(次に示すのは、簡単なバージョンだ)
#!/usr/bin/env ruby
require 'rubygems'
# Prepares the $LOAD_PATH by adding to it lib directories of all gems in the
# project's bundle:
require 'bundler/setup'
load Gem.bin_path('rspec-core', 'rspec')
RSpec は今や単純に bin/rspec
を叩くだけで起動できる。
プロジェクト専用の binstub に PATH を通す
プロジェクト固有の binstub が bin/
にあると仮定して、それをシェルの $PATH
に追加すれば、rspec
コマンドは bin/
無しで起動させる事ができる。
export PATH="./bin:$PATH"
しかしながら、この設定すると、このシステムに他の人が書き込み権限を持っている場合には(共有のホストなど)、セキュリティリスクが発生する。このセキュリティの為に、現在のプロジェクトの bin/
ディレクトリだけを $PATH
に加えるためのシェルスクリプトを書くこともできます。
export PATH="$PWD/bin:$PATH"
hash -r 2>/dev/null || true
このより安全なアプローチの残念なところは、グローバルに一度だけ実行する代わりに、各プロジェクト毎に毎回これを実行しなければならない所だ。
binstub を自作する
binstub が様々な言語で書けるシンプルなスクリプトであることと、その役割が分かった今、自分のプロジェクトや開発環境に合わせて自分自身で binstub が書けるようになっていると思う。
例えば、Rails アプリケーション を作っているとして、Unicorn の起動の為の binstub を自作する事ができる。
#!/usr/bin/env ruby
require_relative '../config/boot'
load Gem.bin_path('unicorn', 'unicorn')
./bin/unicorn
で、アプリケーションとまったく同じ環境(Ruby のバージョン、Gem の依存関係)で Unicorn を起動する事ができる。もしこの binstub がアプリケーションの外側から呼ばれても、正しく動作する。 こんな感じで起動したとしても、だ。/pass/to/app/current/bin/unicorn