Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

binstubをしっかり理解する: RubyGems、rbenv、bundlerの挙動(翻訳+解説)

こんにちは、hachi8833です。

Rails関連の記事でbinstubについてちょくちょく見かける割に、ちゃんとした理解がつい後回しになっていたので、rbenvのwikiにあるUnderstanding binstubs をこの機会にさっと訳してみました。原文の最終更新は2015年11月です。なお、訳文では原則「binstub」と単数形で表記しています。

さらに、訳文の技術チェックを行っていただいた弊社Webチーム部長のmorimorihogeさんによる解説を翻訳の後に追加いたしましたので、合わせてご覧ください。

binstubを理解する(翻訳)

概要

Mislav Marohnić氏の許諾を得て翻訳・公開します。


  • 2016/08/24: 初版公開
  • 2021/11/22: 更新

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を呼び出すと、次の順に呼び出しが発生します。

  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 (本来の実行可能ファイル)

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 railsbe railsと省略して書けて便利です。bundle execはあまりにもしょっちゅう叩くのでalias化すると良いですね。

元記事では、最後にオレオレbinstubを./bin以下に作る話が出ていますが、本番環境ではbinstubをどこに置くのか?という問題といえば、以前Capistrano の仕様変更にハマったことがありました。それまではリリースディレクトリ以下のbinはshared/binにシンボリックリンクされていたのが、特定のバージョンからシンボリックリンクではなくリポジトリのbinディレクトリを配置するのがデフォルトになっていたのでした。

また、以前には./binをgitignoreしていたプロジェクトもあったので、rails runnerがsshログインして実行しようとしても動かない問題などが現場で発生したこともありましたね。

さらに、Rails 4.1以降ではコマンド実行にSpringも絡んでくるので、もしbinstub周りでおかしいことがあった場合にはSpring周りも原因として疑ってみる方が何とかできる可能性が高まると思います。

関連記事

chefからansibleに乗り換えた5つの理由


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。