bundle execとbinstubを理解する

# はじめに

[Everyday Rails](https://leanpub.com/everydayrailsrspec-jp)でRSpecの学習をしていたところ、以下のようなコマンドと説明が出てきました。

```shell
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](https://ruby.studio-kingdom.com/bundler/bundle_exec/)
[bundle execって必要なの? - Qiita](https://qiita.com/d0ne1s/items/fa2dafcee02e963fe997)

> 現在のbundleのコンテキストで、スクリプトを実行します。

`bundle exec`を付けると、現在のプロジェクトでインストールされたgemを使用してコマンドを実行することが出来ます。
`bundle exec`無しだと、システム共通の場所でコマンドを実行することになります。


# binstub

参考
[Bundler: bundle binstubs](https://bundler.io/man/bundle-binstubs.1.html)
[Understanding binstubs · rbenv/rbenv Wiki · GitHub](https://github.com/rbenv/rbenv/wiki/Understanding-binstubs)

主に[Understanding binstubs · rbenv/rbenv Wiki · GitHub](https://github.com/rbenv/rbenv/wiki/Understanding-binstubs)を参考に、「binstubとは？」を見ていきます。

## binstubとは

binstubは実行可能ファイルのラッパー。
その目的は、元の実行可能ファイルを実行する前に環境を整えること。

## Gem installによるbinstub

`gem install` でインストール後、RubyGemsは次の実行可能ファイルを提供します。

> <ruby-prefix>/bin/rspec (binstub generated by RubyGems)
> <ruby-prefix>/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 <ruby-prefix>/bin/rspec is a short Ruby script, presented in a slightly simplified form here:
>   ```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')
>   ```
> 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 リファレンスマニュアル)](https://docs.ruby-lang.org/ja/latest/method/Kernel/v/=2dI.html)
[instance method Kernel#require (Ruby 3.2.0)](https://rurema.clear-code.com/3.2.0/method/Kernel/i/require.html)

`$LOAD_PATH`は`require`でgemをロードするときに使う検索パスらしいです。
`require`実行時、`$LOAD_PATH`ではなくGemファイルの中から見つかった場合は、そのgemを`$LOAD_PATH`に登録します。

## rbenvによるbinstub

rbenvは、独自の `shims` ディレクトリを`$PATH`に追加します。
このディレクトリはRubyに関連するすべての実行ファイルのbinstubを含みます。
具体的には、ruby、gem、およびインストールされた各RubyバージョンにまたがるすべてのRubyGemsのbinstubsです。

コマンドラインで `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)
>
> An rbenv shim, presented here in a slightly simplified form, is a short shell script:
>   ```ruby
>   #!/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が何をしているのか

```ruby
#!/usr/bin/env bash
export RBENV_ROOT="$HOME/.rbenv"
exec rbenv exec "$(basename "$0")" "$@"
```

`export`は環境変数`RBENV_ROOT`を定義しています。

`exec`については[コマンド:exec: UNIX/Linuxの部屋](http://x68000.q-e-d.net/~68user/unix/pickup?exec)がわかりやすかったです。

`exec`以降の動作があまり良くわかっていませんが、
環境変数定義 → `exec` とすることで、環境変数が登録された状態で`rbenv`を実行出来る・・・ということで良いんでしょうか。

## プロジェクト専用のbinstub

rspec コマンドを叩いた時、rbenv はそのプロジェクトに設定された Ruby のバージョンを正しく選択する事ができます。
ただし、RSpec のバージョンは正しく選択されない可能性があります。
これは、RubyGems がそのプロジェクトが古いバージョンの RSpec に依存していたとしても、シンプルに最新の RSpec を起動させることが原因です。

そのため、`bundle exec`が重要です。
これは依存関係を正しく解決し、一貫したrubyの実行環境を保証してくれます。
しかし、常に bundle exec を書かなければならないのは面倒です。

そこで、`bundle binstub`の出番です。

```shell
# 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):
>   ```ruby
>   #!/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`コマンドの挙動の理解がかなり怪しいです。
追々理解を深めていきたいです。