binstub について翻訳してみた


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 は実行可能ファイルをもっていて](https://github.com/rspec/rspec-core/blob/v2.12.2/exe/rspec "rspec-core/exe/rspec at v2.12.2 · rspec/rspec-core")、gem 内の `./exe/rspec` に配置されている。インストール後、RubyGems は実行可能ファイルと共に以下のものを用意してくれる:

1. `<ruby-prefix>/bin/rspec` (RubyGems によって作られた binstub)
2. `<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 のスクリプトだ。簡潔で簡素な全貌をお見せしよう。

```rb
#!/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` を叩いた時、こんな調子で実行される。

1. `$RBENV_ROOT/shims/rspec` (rbenv shim)
2. `$RBENV_ROOT/versions/1.9.3-pXXX/bin/rspec` (RubyGems binstub)
3. `$RBENV_ROOT/versions/1.9.3-pXXX/lib/ruby/gems/1.9.1/gems/rspec-core-XX.YY/exe/rspec` (original)

rbenv の shim も、簡潔で簡素な短いシェルスクリプトだ。

```rb
#!/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 をインストールすることができる。

```rb
# 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` にこんな感じで生成される。(次に示すのは、簡単なバージョンだ)

```rb
#!/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/` 無しで起動させる事ができる。

```sh
export PATH="./bin:$PATH"
```

しかしながら、この設定すると、このシステムに他の人が書き込み権限を持っている場合には（共有のホストなど）、[セキュリティリスク](https://github.com/sstephenson/rbenv/issues/309)が発生する。このセキュリティの為に、現在のプロジェクトの `bin/` ディレクトリだけを `$PATH` に加えるためのシェルスクリプトを書くこともできます。

```sh
export PATH="$PWD/bin:$PATH"
hash -r 2>/dev/null || true
```

このより安全なアプローチの残念なところは、グローバルに一度だけ実行する代わりに、各プロジェクト毎に毎回これを実行しなければならない所だ。

### binstub を自作する

binstub が様々な言語で書けるシンプルなスクリプトであることと、その役割が分かった今、自分のプロジェクトや開発環境に合わせて自分自身で binstub が書けるようになっていると思う。

例えば、Rails アプリケーション を作っているとして、Unicorn の起動の為の binstub を自作する事ができる。

```rb:./bin/unicorn
#!/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`