binstub について翻訳してみた

More than 5 years have passed since last update.

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 は実行可能ファイルをもっていて./exe/rspec に配置されている。インストール後、RubyGems は実行可能ファイルにひき続いてこんな事をする:



  1. <ruby-prefix>/bin/rspec (binstub が RubyGems によってつくられる)


  2. <ruby-prefix>/lib/ruby/gems/1.9.1/gems/rspec-core-XX.YY/exe/rspec (オリジナル)

最初のファイルが binstub で、2番目のファイルをラップする為につくられる。RubyGems が何故 <ruby-prefix>/bin に配置するかと言うと、そいつが既に $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 が含まれている。rubygem、そして 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 も、簡潔で簡素な短いシェルスクリプトだ。

#!/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 を自作する事ができる。


./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