Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rbenv・RubyGems・Bundlerの仕組みと連携を徹底解説

Rubyで開発を行う際、依存関係の管理は通常、プロジェクトが依存するRubyのバージョンやgemのバージョンを明示することから始まります。筆者の経験上、Rubyの依存関係のデバッグは最も苦労する作業のひとつでした。多くの処理は「そのまま動く」ため失敗は頻繁ではありませんが、一度問題が発生すると、原因の特定と修正が不必要に難しくなることが多いのです。本記事では、Rubyにおける依存関係管理に関わる各コンポーネントを整理して解説します。これにより、奇妙な問題に直面した際のデバッグが格段にしやすくなるでしょう。

Rbenv・RubyGems・Bundlerの仕組みと連携を徹底解説

Rubyのコード読み込みの仕組み

デフォルトで、Ruby言語には外部で定義されたコードを読み込むための主要な方法として loadrequire の2つが用意されています。

load 'json.rb'
require 'json.rb'
require_relative 'json.rb'

どちらの読み込み方法も、絶対パスと相対パスの両方を引数として受け付けます。しかし、両者には以下の2つの重要な違いがあります。

  1. load を複数回呼び出すとファイルが再実行されますが、require を複数回呼び出してもファイルは再実行されず、代わりに false が返されます。
  2. load は絶対パスと相対パスのみを解決します。一方 require は、パスが絶対パスとして解決できない場合に $LOAD_PATH を検索します。

3つ目のバリエーションとして require_relative があります。これは相対パスを使用しますが、Rubyプロセスのワーキングディレクトリではなく、現在のファイルの場所を基準にコードを読み込みます。

Rbenvとは

バージョンマネージャーとは、インタプリタ(ここではRuby)の複数バージョンを管理し、簡単に切り替えられるようにするツールであり、プロジェクトごとに対応するgemの場所を指定することもできます。バージョンマネージャーはほぼ言語非依存の概念であり、Node.jsにはNvmやn、Pythonにはpyenv、RubyにはRbenv、rvm、chrubyなど、各言語にそれぞれの実装が存在します。それでは、rbenv を実際に見ていきましょう。

Rubyバージョンのインストール

rbenv install コマンドを使えば、任意のバージョンのRubyをインストールできます。

# ruby 2.6.1をインストール
$ rbenv install 2.6.1
Downloading openssl-1.1.1i.tar.gz...
-> https://dqw8nmjcqpjn7.cloudfront.net/e8be6a35fe41d10603c3cc635e93289ed00bf34b79671a3a4de64fcee00d5242
Installing openssl-1.1.1i...
Installed openssl-1.1.1i to /home/directory/.rbenv/versions/2.6.1

Downloading ruby-2.6.1.tar.bz2...
-> https://cache.ruby-lang.org/pub/ruby/2.6/ruby-2.6.1.tar.bz2
Installing ruby-2.6.1...
ruby-build: using readline from homebrew
Installed ruby-2.6.1 to /home/directory/.rbenv/versions/2.6.1

# インストール確認
$ rbenv versions # インストール済みの全バージョンを表示
  system
  2.6.1

# インストール可能なバージョン一覧を取得
$ rbenv install -L
1.8.5-p52
1.8.5-p113
1.8.5-p114
...
2.7.0-rc1
2.7.0-rc2
2.7.0
...
truffleruby+graalvm-20.1.0
truffleruby+graalvm-20.2.0
truffleruby+graalvm-20.3.0

# 上記の一覧は約500バージョンにもなり、全部スクロールするのは大変です。
# 次のコマンドはfzfと組み合わせて目的のバージョンを素早く見つけるショートカットです。
$ rbenv install `rbenv install -L | fzf`

バージョンの切り替え

Rubyのバージョンを切り替える方法はいくつかありますが、rbenv は常に以下の順序で判断します。

  • 環境変数 RBENV_VERSION をチェックする。
  • 実行スクリプトのディレクトリおよびその親ディレクトリをルートディレクトリまで遡って .ruby-version ファイルを探す。
  • $PWD およびその親ディレクトリをルートディレクトリまで遡って .ruby-version ファイルを探す。
  • グローバル設定ファイル ~/.rbenv/version を使用する。

優先順位は上から下へと適用され、~/.rbenv/version は最終的なフォールバックとしてグローバルバージョン扱いになります。以下の例をご覧ください。

# プロジェクトのルートディレクトリ内

# プロジェクト用のRubyバージョンを選択
$ touch .ruby-version && echo "2.7.1" >> .ruby-version

# 選択されたバージョンを確認
$ ruby --version
ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-darwin20] # 結果

$ rbenv version
2.7.1 (set by /path/to/current/directory/.ruby-version) # 結果

# 選択バージョンを変更
$ : >> .ruby-version && echo "2.6.1" >> .ruby-version

# 変更を確認
$ ruby --version
ruby 2.6.1p33 (2019-01-30 revision 66950) [x86_64-darwin20] # 結果

$ rbenv version
2.6.1 (set by /path/to/current/directory/.ruby-version)

# .ruby-versionが存在する状態でRBENV_VERSIONによる切り替え
$ export RBENV_VERSION=2.5.1

# 変更を確認
# .ruby-versionは無視される
$ ruby --version
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin20] # 結果

$ rbenv version
2.5.1 (set by RBENV_VERSION environment variable) # 結果

# 未インストールのバージョンを指定し、RBENV_VERSIONを解除
$ unset RBENV_VERSION & : >> .ruby-version && echo "2.4.1" >> .ruby-version

# 変更を確認
$ ruby --version
rbenv: version `2.4.1' is not installed (set by full/path/to/current/directory/.ruby-version) # 結果

ShimとRehashの仕組み

この2つの概念を正しく理解しておくことは、rbenv を効果的にデバッグするために不可欠です。

Shim(シム)とは、PATH 上に存在する軽量なbashスクリプトで、コマンドを横取り(intercept)し、適切なバージョンへ振り分けて実行する役割を担います。大まかに言えば、すべてのコマンド(例:rspec)は rbenv exec rspec に変換されて実行されます。詳細は以下の通りです。

まず、rbenv はインストール済みの全Rubyバージョンにわたるすべてのコマンド(rspecbundle など)に対してshimを作成し、バージョンに関係なくCLIへの呼び出しを横取りできるようにします。これらのshimは ~/.rbenv/shims に配置されており、すべて同じbashスクリプトを含んでいます。

#!/usr/bin/env bash
set -e
[ -n "$RBENV_DEBUG" ] && set -x

program="${0##*/}"
if [ "$program" = "ruby" ]; then
   for arg; do
     case "$arg" in
     -e* | -- ) break ;;
     */* )
        if [ -f "$arg" ]; then
         export RBENV_DIR="${arg%/*}"
         break
       fi
       ;;
     esac
   done
 fi

 export RBENV_ROOT="/home/directory/.rbenv"
 exec "/usr/local/Cellar/rbenv/1.1.2/libexec/rbenv" exec "$program" "$@"

このスクリプトの動作は、おおよそ次のように要約できます。

  • プログラム名が ruby で引数に -e が含まれる場合:
    • rbenv exec ruby <args> に変換される。
  • プログラム名が ruby でスクリプトへのパスが渡された場合:
    • スクリプトのディレクトリを RBENV_DIR に設定する。これにより、rbenv$PWD より先にスクリプトのディレクトリ内の .ruby-version を検索できる。両方の場所に .ruby-version が存在する場合、rbenv はスクリプト側のディレクトリを優先する。
  • プログラム名がRuby以外の場合:
    • rbenv exec <program-name> <args> に変換される。

最後に、rbenv exec <command-name> <args>RBENV_VERSION 環境変数をチェックすることで、コマンドを渡すべき正しいバージョンを特定します。RBENV_VERSION は前述のアルゴリズムによって設定されることを思い出してください。

shimが PATH の先頭に配置されている必要があるのは、Rubyの実行可能ファイルへの最初の接点となり、確実に横取りできるようにするためです。PATH の設定状況を把握し、shimが正しく機能しているかを確認するのに最も有効な方法は以下の通りです。

$ which -a bundle

/path/to/home/.rbenv/shims/bundle
/usr/bin/bundle

which -a bundlePATH を単純に順番に検索し、bundle が見つかった場所を見つけた順に出力します。~/.rbenv/shims より前に何かが出力された場合は、shimの設定が正しくないことを意味します。rbenv which bundle ではこの問題は検出できません。このコマンドは PATH を検索せず、rbenv のコンテキスト内で動作するためです。

Rehash(リハッシュ)とは、shimを作成するプロセスのことです。rspec のように実行可能ファイルを提供するgemを新たにインストールした際は、rbenv rehash を実行してshimを作成する必要があります。これにより、以降の rspec 呼び出しが rbenv によって横取りされ、適切なRubyバージョンへ引き渡されるようになります。

RubyGemsとは

次はRubyGemsです。公式のRubyサイトから入手できます。RubyGemsは、ライブラリの作成・共有・インストールを容易にするために設計されたRubyのパッケージングシステムです。ある意味、apt-getのようなディストリビューションのパッケージ管理システムに似ていますが、対象がRubyソフトウェアである点が異なります。RubyGemsはgemを共有する事実上の標準手段であり、通常は ~/.rbenv/versions/{version-number}/lib/ruby/gems/{minor-version}/ または使用するバージョンマネージャーに応じた類似のパスにインストールされます。

Rubyのデフォルトの読み込みメソッド Kernel.require には、gemのインストールディレクトリからgemを読み込む仕組みがありません。そこでRubyGemsは Kernel.require をモンキーパッチし、以下のように動作を拡張しています。

  • まず $LOAD_PATH 内でgemを検索する。
  • 見つからなければ、GEM INSTALLATION DIRECTORY内でgemを検索する。
    • 見つかった場合、そのパスを $LOAD_PATH に追加する。

これが「ネイティブ」に動作するのは、Ruby 1.9以降にRubyGemsが標準搭載されているためです。それ以前のRubyバージョンではRubyGemsを手動でインストールする必要がありました。標準で動作するとはいえ、デバッグ時にはこの違いを知っておくことが重要です。

gemとは、特定の問題を解決するための関連コードの集まりです。gemをインストールし、gem環境の情報を確認するには以下のようにします。

$ gem install gemname
$ gem env

RubyGems Environment: 
    - RUBYGEMS VERSION: 3.1.2 
    - RUBY VERSION: 2.7.1 (2020-03-31 patchlevel 83) [x86_64-darwin20] 
    - INSTALLATION DIRECTORY: /path/to/home/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0 
    - USER INSTALLATION DIRECTORY: /path/to/home/.gem/ruby/2.7.0 
    - RUBY EXECUTABLE: /path/to/home/.rbenv/versions/2.7.1/bin/ruby 
    - GIT EXECUTABLE: /usr/bin/git 
    - EXECUTABLE DIRECTORY: /path/to/home/.rbenv/versions/2.7.1/bin 
    - SPEC CACHE DIRECTORY: /path/to/home/.gem/specs 
    - SYSTEM CONFIGURATION DIRECTORY: /path/to/home/.rbenv/versions/2.7.1/etc 
    - RUBYGEMS PLATFORMS:   
        - ruby   
        - x86_64-darwin-20 
    - GEM PATHS:    
        - /path/to/home/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0   
        - /path/to/home/.gem/ruby/2.7.0 
    - GEM CONFIGURATION:   
        ...
    - REMOTE SOURCES:   
        - https://rubygems.org/ 
    - SHELL PATH:   
        - /path/to/home/.rbenv/versions/2.7.1/bin

RubyGemsはどのようにこの問題を解決しているのでしょうか。それは、Kernel のrequireシステムを独自の require メソッドでモンキーパッチすることによります。この仕組みが整った状態で require honeybadger が呼び出されると、gemsフォルダ内から honeybadger.rb を検索し、見つかればそのgemをアクティベートします。

例えば、require 'honeybadger' を実行すると、内部的にはおおよそ以下のような処理が行われます。

  • spec = Gem::Specification.find_by_path('honeybadger')
  • spec.activate

gemのアクティベートとは、端的に言えばそのgemを $LOAD_PATH に追加することです。また、RubyGemsはgem本体をダウンロードする前に、そのgemが依存するすべての依存関係を先にダウンロードする役割も担っています。

さらに、RubyGemsには便利な機能があり、gem open <gem-name> コマンドで該当gemのディレクトリを直接開くことができます。

Rbenv・RubyGems・Bundlerの仕組みと連携を徹底解説

これにより、アプリが参照しているgemの特定バージョンを簡単に見つけたり追跡したりできます。

Bundlerとは

このレイヤーにおいて、Bundlerはプロジェクトの全依存関係を簡単に指定できるようにし、必要に応じて各gemのバージョンも指定できます。そして、gemの依存関係を解決し、gem本体とその依存関係をインストールします。Bundlerが登場する前は、実際のアプリケーション開発において以下のような多くの課題がありました。

  • アプリケーションは多数の依存関係を持ち、さらにそれらの依存関係もまた別の依存関係とそれぞれのバージョンを持っています。誤ったバージョンのgemをひとつでもインストールすればアプリは容易に壊れ、その修復には涙ものの労力が必要でした。
  • また、2つの依存関係が同じ第三階層の依存関係を参照することがあり、互換性のあるバージョンを見つけるのが課題でした。仮に互換性があったとしても、それを特定すること自体が問題でした。
  • 同一マシン上で複数のアプリケーションが様々な依存関係を持って稼働している場合、自分のアプリケーションがマシンにインストールされた任意のgemへアクセスできてしまいます。これは最小権限の原則に反し、悪意のあるものであろうとなかろうと、マシン上の全gemにアプリが晒されることになります。

Bundlerは以下の機能によって、これら3つの問題をすべて解決し、アプリの依存関係を健全に管理する手段を提供します。

Bundlerは依存関係を解決しロックファイルを生成する

# Gemfile
gem 'httparty'

bundle または bundle install を実行すると、ロックファイルが生成されます。

GEM
  specs:
    httparty (0.18.1)
      mime-types (~> 3.0)
      multi_xml (>= 0.5.2)
    mime-types (3.3.1)
      mime-types-data (~> 3.2015)
    mime-types-data (3.2020.1104)
    multi_xml (0.6.0)

PLATFORMS
  ruby

DEPENDENCIES
  httparty

BUNDLED WITH
   2.1.4

上記のように、Bundlerはインストールされる httparty のバージョンとその依存関係を Gemfile.lock に生成します。このファイルはアプリの依存関係の設計図であり、バージョン管理システムにコミットすべきものです。これにより、開発・ステージング・本番といった環境間でプロジェクトの依存関係の一貫性が保証されます。

Bundlerは依存関係間の互換性を解決する

Bundlerは httparty の依存関係について、適切なバージョンを見つけて指定することで解決します。さらに、gem同士の依存関係の解決も試みます。例えば、

# Gemfile
gem 'httparty' # gem 'mime-types', '>= 3.0.1, < 4.0.1' に依存
gem 'rest-client' # gem 'mime-types', '>= 2.0.1, < 3.0' に依存

上記の例は架空のものですが、実行すると以下のようなエラーが発生します。

Bundler could not find compatible versions for gem "mime-types":
In Gemfile:
    httparty was resolved to 0.18.1, which depends on
        mime-types ('>= 3.0.1, < 4.0.1')

    rest-client was resolved to 2.0.4, which depends on
        mime-types ('>= 2.0.1, < 3.0')

これは、2つのgemが互換性のない依存関係を持ち、自動的に解決できないためです。

BundlerはGemfileに未指定のgemへのアクセスを制限する

以下のようなサンプルGemfileの場合、

# Gemfile
gem 'httparty'

# irb
require 'rest-client'

# エラー発生
LoadError (cannot load such file -- rest-client)

Gemfileで指定された依存関係だけがプロジェクトからrequireできるよう、Bundlerがアクセスを制限してくれます。

Bundle exec

プロジェクトのディレクトリで rspec を直接実行した場合、Gemfileで指定されたバージョンとは異なるバージョンが実行される可能性があります。これは、Gemfileで指定されたバージョンではなく最新版が選択されて実行されるためです。bundle exec rspec を使えば、rspecがそのプロジェクトのコンテキスト(つまりGemfileで指定されたgem群)の中で確実に実行されます。

Bundle binstubs

./bin/rails のようなコマンドを実行する記事をよく目にするでしょう。このコマンドは bundle exec rails と同等です。Binstub(ビンスタブ)とは、Rubyの実行可能ファイルをラップし、bundle exec の利用を簡便化するものです。

binstubを生成するには bundle binstubs gem-name を実行します。これにより ./bin フォルダ内にbinstubが作成されますが、--path オプションでディレクトリを指定することも可能です。

参考文献

さらに詳しく学びたい方は、以下の資料をご参照ください。

  • How Do Gems Work?
  • Rbenv
  • RubyGems
  • Bundler

  1. Ruby 2.6のMJITとは?仕組みとパフォーマンスを徹底解説

    Rubyのパフォーマンスは、バージョンが上がるごとに大きく向上してきました。そしてRuby開発チームは、さらに速いRubyを実現するために全力を尽くしています。 その取り組みのひとつが「3×3プロジェクト」です。 その目標とは? Ruby 3.0を、Ruby 2.0の3倍速くすることです。 このプロジェクトの一環として誕生したのが、新しいMJITコンパイラです。これが本記事のテーマとなります。 MJITとは何か MJITは「Method Based Just-in-Time Compiler(メソッドベースのジャストインタイムコンパイラ)」の略称です。 これは具体的にどういう意味でしょうか?

  2. CortanaとAlexaを連携して使う方法|Windows 10とAmazon Echoで両アシスタントを活用

    Microsoftは、Build開発者向けカンファレンスでの発表をもとに、Windows 10 PCへのAlexa搭載と、Amazon EchoスピーカーへのCortana搭載を実現しました。現在、プレビュー版は米国限定で提供されていますが、この2大音声アシスタントの提携は今後さらに発展していくことが期待されます。 私たちが暮らす現代は多様化した世界であり、1台のデバイスに複数のアシスタントを搭載することには大きなメリットがあります。個別のOSとしてではなく、まるでアプリのようにアシスタントへアクセスできるのは非常に便利です。 現時点での連携機能はまだ基本的なものですが、近いうちにCortan