はじめに
Everyday RailsでRSpecの学習をしていたところ、以下のようなコマンドと説明が出てきました。
bundle exec rspec
Rails アプリケーションは Bundler の使用が必須であるため、RSpec を実行する際は bundle exec rspec のように毎回 bundle exec を付ける必要があります。
binstub を作成すると bin/rspec のように少しタイプ量を減らすことができます。
- Railsはbundlerを使うため、
bunle exec
が必要。 - binstubによってショートカットを作成できる。
ということはわかったのですが、もう少し詳しく知りたくなったので調べてみました。
ざっくり結論
-
bundle exec
は、現在のプロジェクトのコンテキストでコマンドを実行するために必要。- プロジェクトのコンテキスト =
gemfile.lock
に基づいた環境。
- プロジェクトのコンテキスト =
- binstubは元の実行可能ファイルを実行する前に、環境を整えることを目的としたラッパー。
-
bundle binstubs
はショートカット作成を目的に、実行ファイルのラッパーを作成する。- 作成されたラッパーは
/bin
ディレクトリ下に置かれる。 - これにより毎回
bundle exec
と打たなくても良くなる。
- 作成されたラッパーは
bundle exec
参考
bundle exec | Bundler日本語ドキュメント | Ruby STUDIO
bundle execって必要なの? - Qiita
現在のbundleのコンテキストで、スクリプトを実行します。
bundle exec
を付けると、現在のプロジェクトでインストールされたgemを使用してコマンドを実行することが出来ます。
bundle exec
無しだと、システム共通の場所でコマンドを実行することになります。
binstub
参考
Bundler: bundle binstubs
Understanding binstubs · rbenv/rbenv Wiki · GitHub
主にUnderstanding binstubs · rbenv/rbenv Wiki · GitHubを参考に、「binstubとは?」を見ていきます。
binstubとは
binstubは実行可能ファイルのラッパー。
その目的は、元の実行可能ファイルを実行する前に環境を整えること。
Gem installによるbinstub
gem install
でインストール後、RubyGemsは次の実行可能ファイルを提供します。
/bin/rspec (binstub generated by RubyGems)
/lib/ruby/gems/1.9.1/gems/rspec-core-XX.YY/exe/rspec (original)
既にrbenv等によって $PATH
に含まれている <ruby-prefix>/bin
に、binstubを配置します。
また、RubyGems がインストールした2番目のディレクトリ(オリジナルの方)は $PATH に含まれていませんが、直接実行してはいけません。
なぜなら、Ruby のプロジェクトは基本的にはセットアップ無しで呼ばれる事を想定していないからです。
最低でもプロジェクトのファイルを読み込めるように $RUBYOPT
がセットされている必要があります。
The generated binstub /bin/rspec is a short Ruby script, presented in a slightly simplified form here:
#!/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')
The purpose of every RubyGems binstub is to use RubyGems to prepare the $LOAD_PATH before calling the original executable.
生成されたbinstubを簡略化したものを上に示します。
ここでは、gemのlibディレクトリとその依存関係を追加して$LOAD_PATH
を準備します(?)
全てのRubyGemによるbinstubの目的は、元の実行可能ファイルを呼ぶ前に $LOAD_PATH
を準備することにあります。
$LOAD_PATH
参考
$-I (Ruby 3.1 リファレンスマニュアル)
instance method Kernel#require (Ruby 3.2.0)
$LOAD_PATH
はrequire
でgemをロードするときに使う検索パスらしいです。
require
実行時、$LOAD_PATH
ではなくGemファイルの中から見つかった場合は、そのgemを$LOAD_PATH
に登録します。
rbenvによるbinstub
rbenvは、独自の shims
ディレクトリを$PATH
に追加します。
このディレクトリはRubyに関連するすべての実行ファイルのbinstubを含みます。
具体的には、ruby、gem、およびインストールされた各RubyバージョンにまたがるすべてのRubyGemsのbinstubsです。
コマンドラインで 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)
An rbenv shim, presented here in a slightly simplified form, is a short shell script:
#!/usr/bin/env bash export RBENV_ROOT="$HOME/.rbenv" exec rbenv exec "$(basename "$0")" "$@"
まずshims/rspec
のbinstub。
shims
にいるbinstubの目的は、全てのrubyの呼び出しをrbenv exec
を通すようにすること。
このようにすることで、正しいRubyのバージョンが実行されるようになる。
次にbin/rspec
のbinstub。
先述の通り、$LOAD_PATH
の準備を目的とします。
最後に元の実行可能ファイル。
shims
のbinstubが何をしているのか
#!/usr/bin/env bash
export RBENV_ROOT="$HOME/.rbenv"
exec rbenv exec "$(basename "$0")" "$@"
export
は環境変数RBENV_ROOT
を定義しています。
exec
についてはコマンド:exec: UNIX/Linuxの部屋がわかりやすかったです。
exec
以降の動作があまり良くわかっていませんが、
環境変数定義 → exec
とすることで、環境変数が登録された状態でrbenv
を実行出来る・・・ということで良いんでしょうか。
プロジェクト専用のbinstub
rspec コマンドを叩いた時、rbenv はそのプロジェクトに設定された Ruby のバージョンを正しく選択する事ができます。
ただし、RSpec のバージョンは正しく選択されない可能性があります。
これは、RubyGems がそのプロジェクトが古いバージョンの RSpec に依存していたとしても、シンプルに最新の RSpec を起動させることが原因です。
そのため、bundle exec
が重要です。
これは依存関係を正しく解決し、一貫したrubyの実行環境を保証してくれます。
しかし、常に bundle exec を書かなければならないのは面倒です。
そこで、bundle binstub
の出番です。
# generates binstubs for ALL gems in the bundle
bundle install --binstubs
# ...OR, generate binstubs for a SINGLE gem (recommended)
bundle binstubs rake
bundle binstubs rspec-core
bundle binstubs
(もしくは bundle install --binstubs
)を利用することで、指定したgemのbindtubを作成することが出来ます。
This creates, for example, ./bin/rspec (simplified version shown):
#!/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 can now be easily run with just bin/rspec.
生成されたbinstubによりbin/rspec
と叩くだけでサクッと実行出来るようになりました。
最後に
bundle exec
、bundle binstubs
の役割を理解することが出来ました。
サクッと調べて終わる予定だったのですが、rbenvによるbinstubなど若干脱線してしまい、思っていたよりも時間がかかってしまいました。
深掘る箇所と、ふーんで済ませる箇所の区別が難しいです。
今回はそこまで深入りしませんでしたが、シェル・コマンド周りの知識が浅く、exec
コマンドの挙動の理解がかなり怪しいです。
追々理解を深めていきたいです。