はじめに
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コマンドの挙動の理解がかなり怪しいです。
追々理解を深めていきたいです。