こんにちは、hachi8833です。
Rails関連の記事でbinstubについてちょくちょく見かける割に、ちゃんとした理解がつい後回しになっていたので、rbenvのwikiにあるUnderstanding binstubs をこの機会にさっと訳してみました。原文の最終更新は2015年11月です。なお、訳文では原則「binstub」と単数形で表記しています。
さらに、訳文の技術チェックを行っていただいた弊社Webチーム部長のmorimorihogeさんによる解説を翻訳の後に追加いたしましたので、合わせてご覧ください。
binstubを理解する(翻訳)
binstubとは、実行可能ファイルのラッパースクリプトです。ここで言う実行可能ファイルは「バイナリ」を指すこともありますが、コンパイルされたバイナリでなくても構いません(訳注:シェルスクリプト等も対象です)。binstubの目的は、その実行可能ファイルを呼び出す前に環境を整えることです。
Rubyでよく使われるのは、何らかの実行可能ファイルを含むgemをインストールしたときに、RubyGemsによって生成されるbinstubです。しかし、binstubの記述にはどんな言語でも使えるので、多くの開発者がしばしばbinstubを自作しています。
RubyGems
gem install rspec-core
を実行したときの動作を詳しく見てみましょう。RSpecの最終的な実行可能ファイルは、gemの中にある./exe/rspec
です 。gemをインストールすると、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はこのbinstubをruby-prefix>/bin
に配置します。このディレクトリは$PATH
に設定済みであることが前提です(設定はrvmやrbenvなどのRubyバージョンマネージャの仕事です)。
番目のファイル(本来の実行可能ファイル)がRubyGemsによってインストールされるディレクトリは、$PATH
には含まれていません。仮に本来の実行可能ファイルが$PATH
にインストールされたとしても、Rubyプロジェクト内の実行可能ファイルは、たいてい適切なセットアップ処理なしに直接実行されることを意図していないため、安全に実行できないと考えておく方がよいでしょう。少なくとも、実行可能ファイルのプロジェクトのソースファイルをrequireするために、$RUBYOPT
を設定する必要があるでしょう。
こうして生成されたbinstubファイル<ruby-prefix>/bin/rspec
は、次のような短いRubyスクリプトです(やや簡略化してあります)。
#!/usr/bin/env ruby
require 'rubygems'
# Prepares the $LOAD_PATH by adding to it lib directories of the gem and
# its dependencies:
gem 'rspec-core'
# Loads the original executable
load Gem.bin_path('rspec-core', 'rspec')
RubyGemsのbinstubの目的は、gemの種類を問わず、本来の実行可能ファイルを呼び出す前に$LOAD_PATH
を準備することです。
rbenv
rbenv は、$PATH
変数に独自の「shim」ディレクトリを追加します。Rubyに関連する全実行可能ファイルのbinstubは、このshimディレクトリの下に配置されます。ruby
のbinstubコマンドやgem
のbinstubコマンドはもちろん、システムにインストールされているRubyのバージョンごとに、RubyGems binstubがこのshimディレクトリに置かれます。
コマンドラインで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
(本来の実行可能ファイル)
rbenvのshimファイルは、次のような短いRubyスクリプトです(やや簡略化してあります)。
#!/usr/bin/env bash
export RBENV_ROOT="$HOME/.rbenv"
exec rbenv exec "$(basename "$0")" "$@"
rbenvのshimファイルの目的は、Ruby実行可能ファイルへのすべての呼び出しがrbenv exec
を経由するようにし、指定したバージョンのRubyで正しく実行されるようにすることです。
プロジェクト固有のbinstub
rspec
を(訳注: bundle exec <コマンド>
を付けずに)プロジェクトディレクトリ内で実行すると、rbenvが適切なバージョンのRubyをプロジェクトの設定どおりに選択してくれます。しかしここで注意しないといけないのは、プロジェクト内でrspec
を実行した場合、正しいバージョンのRSpecが有効になるかどうかまでは保証されないという点です。実際、システムが古いバージョンのRSpecに依存していても、RubyGemsは単に最新バージョンのRSpecを有効にします。プロジェクトの取り扱いという観点から見ると、望ましくない動作です。
この問題を解決するために、bundle exec <コマンド>
が必要なのです。このコマンドを使うことで、正しい依存関係が有効になり、一貫したRuby実行環境を利用できるようになります。とは言うものの、いちいちbundle exec
を入力するのがだるいのも確かです。
Bundlerによって生成されるbinstub
Bundlerを使って、プロジェクトにbundleされた実行可能ファイルのbinstubをインストールできます。
bundleされたすべてのgemのbinstubを一括生成するには、次のコマンドを実行します。
bundle install --binstubs
gemをひとつ指定してbinstubを生成するには、次のコマンドを実行します(この方法をおすすめします)。
bundle binstubs rake
bundle binstubs rspec-core
プロジェクトのバージョンコントロールにあるbinstubを一度チェックしてみてください。プロジェクトの他のメンバーにとってbinstubがどのように役に立っているかがわかるでしょう。
たとえば、Bundlerがプロジェクト用に生成した./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')
これにより、プロジェクトディレクトリでbin/rspec
と入力するだけでRSpecを実行できます。
※プロジェクト自体がgemである場合(つまりgemを書いている場合)、bundle install --binstubs exe
のようなコマンドを実行して、bin/
以外のディレクトリを使うようにする必要があります。gemのリポジトリにうっかりbin/rspec
を登録したりすると、開発者がgemをインストールしたときにrspec
コマンドを上書きして壊してしまいます。
プロジェクト固有のbinstubをPATHに追加する
プロジェクトのbinstubは、慣例としてプロジェクトローカルのbin/
ディレクトリに配置されるので、このディレクトリへの相対パスをシェルの$PATH
に追加しておけば、bin/
を付けずにプロジェクト固有のrspec
を実行できるようになります。
export PATH="./bin:$PATH"
ただしこのシェル設定は、共有ホストのように他のユーザーが書き込み権限を持つシステムに設定するとセキュリティ上のリスク を生じます。セキュリティを高めるために、次のようにカレントプロジェクトのbin/
ディレクトリだけを$PATH
に追加することができます。
export PATH="$PWD/bin:$PATH"
hash -r 2>/dev/null || true
こちらのほうがよりセキュアですが、その代わりグローバルに一度だけ設定すればよいというわけではなく、プロジェクトを切り替えるたびにこのコマンドを実行する必要があります。
direnv もご覧ください。
binstubを手動で作成する
ここまで読んでいただければ、binstubの目的はもちろんのこと、binstubが単なるスクリプトであることと、どんな言語でも記述できることもご理解いただけたと思います。それでは、プロジェクトや自分のローカル開発環境に合わせてbinstubを自作できるかどうか、検討してみましょう。
たとえば、Railsアプリケーションのコンテキストで、次のようなbinstubを手書きして./bin/unicorn
に置くことで、Unicornを実行できるようになります。
#!/usr/bin/env ruby
require_relative '../config/boot'
load Gem.bin_path('unicorn', 'unicorn')
bin/unicorn
を使うことで、アプリケーションで使うRubyのバージョンやGemfileの依存関係と完全に同じ環境でUnicornを実行できます。
これは、/<パス>/app/current/bin/unicorn
のように、プロジェクトディレクトリの外からbinstubを呼び出す場合でも同様です。
「Understanding binstubs」の解説(morimorihoge )
エンジニア視点での翻訳内容確認を担当しましたmorimorihoge です。本記事に関するコメントです。
仕事でRails開発をしていると複数バージョンのRubyやgemを渡り歩くことが多く、binstubの仕組みを知ることは思わぬ地雷を踏まないためには有用かと思います。それなりにRails開発の経験がある人からも「コマンド実行してみたんだけどうまくいかない」といった相談を受けて、よくよく聞いてみるとbundle exec
の付与漏れであることがあるので、binstubまわりをきちんと理解している人は実は意外に少ないのでは?と思いました。
また、良くやられている開発者向けhackとしては、.bashrcや.zshrcあたりにalias be='bundle exec'
という記述をしておくと、bundle exec rails
をbe rails
と省略して書けて便利です。bundle exec
はあまりにもしょっちゅう叩くのでalias化すると良いですね。
元記事では、最後にオレオレbinstubを./bin以下に作る話が出ていますが、本番環境ではbinstubをどこに置くのか?という問題といえば、以前Capistrano の仕様変更にハマったことがありました。それまではリリースディレクトリ以下のbinはshared/binにシンボリックリンクされていたのが、特定のバージョンからシンボリックリンクではなくリポジトリのbinディレクトリを配置するのがデフォルトになっていたのでした。
また、以前には./binをgitignoreしていたプロジェクトもあったので、rails runnerがsshログインして実行しようとしても動かない問題などが現場で発生したこともありましたね。
さらに、Rails 4.1以降ではコマンド実行にSpringも絡んでくるので、もしbinstub周りでおかしいことがあった場合にはSpring周りも原因として疑ってみる方が何とかできる可能性が高まると思います。
概要
Mislav Marohnić氏の許諾を得て翻訳・公開します。