bundlerでgemバージョンを束ねる

Rails1.1.6の環境でどうにかscaffoldはできるようになった。その時の実行環境は以下の状態だった。

$ rbenv version
1.8.7-p375 (set by /Users/zari/.rbenv/version)

$ gem list

 *** LOCAL GEMS ***

actionmailer (1.2.5)
actionpack (1.12.5)
actionwebservice (1.1.6)
activerecord (1.14.4)
activesupport (1.3.1)
rails (1.1.6)
rake (0.7.1)
sqlite3 (1.3.10)
  • この実行環境というのは非常にデリケートである。
  • 例えば、今の時代にRails-1.1.6をgemに従うままにインストールすると、最新のRake-10.3.2がインストールされる。
  • しかし、Rake-10.3.2ではRails-1.1.6はうまく動作しない。
    • rake db:migrateでエラーが出てしまう...。
  • このエラーに散々悩んで、ようやくrakeのバージョンを下げれば良いことに気付いた。
  • そこで、わざわざgem uninstall rakeで一旦rakeを削除して、その後、Rake-0.7.1を再インストールしている。面倒くさい。
  • しかも、動かしたい古いRails環境のバージョンは1.1.6だけではない。
  • バージョン1.2.6や2.0.5などのRails環境も動かしたいのだ。
  • それらのバージョンをインストールした瞬間に、Rails-1.1.6のプロジェクトは正常に動かなくなってしまう。
  • gemコマンドの操作は手軽さが売りなのに、これでは気楽にインストールできない。
    • gem updateなんて、もってのほかである。
  • プロジェクトはgemのバージョンと密接な関係があるはずなのに、インストールされた最新バージョンしか使えない所に問題がある。
  • 必要なgemとバージョンを自由に選択して、実行環境を素早く作り上げる仕組みが必要なのだ。


それを実現してくれるのがbundlerなのだった。

基本

bundlerをインストール
  • bundlerもまたgemなので、gemコマンドでインストールするのだ。
$ gem install bundle
Fetching: bundler-1.7.4.gem (100%)
Fetching: bundle-0.0.1.gem (100%)
Successfully installed bundler-1.7.4
Successfully installed bundle-0.0.1
2 gems installed
Installing ri documentation for bundler-1.7.4...
Installing ri documentation for bundle-0.0.1...
File not found: lib
ERROR:  While generating documentation for bundle-0.0.1
... MESSAGE:   exit
... RDOC args: --ri --op /Users/zari/.rbenv/versions/1.8.7-p375/lib/ruby/gems/1.8/doc/bundle-0.0.1/ri lib --title bundle-0.0.1 Documentation --quiet
  • ドキュメントの生成でエラーが出ているけど、気にしないことにした。
    • Rubyのバージョンが古いためのエラーか?
gemとバージョンの指定
  • Gemfileを初期化する。
$ cd ~/Desktop/rails116
$ bundle init
$ cat Gemfile
# A sample Gemfile
source "https://rubygems.org"

# gem "rails"
  • Gemfileに必要なgemとバージョンを書き込む。
$ cat <<EOS >> Gemfile
gem "rails", "1.1.6"
gem "rake", "0.7.1"
gem "sqlite3"
EOS
指定したgemバージョンをインストール
  • Gemfileで指定したgemバージョンをインストールする。
$ bundle install
  • installは省略可能なので、bundleのみでもOK。
$ bundle
Fetching gem metadata from https://rubygems.org/...........
Resolving dependencies...
Installing rake 0.7.1 (was 10.3.2)
Installing activesupport 1.3.1 (was 4.1.7)
Installing actionpack 1.12.5 (was 4.1.7)
Installing actionmailer 1.2.5 (was 4.1.7)
Installing activerecord 1.14.4 (was 4.1.7)
Installing actionwebservice 1.1.6
Installing rails 1.1.6 (was 4.1.7)
Installing sqlite3 1.3.10
Using bundler 1.7.4
Your bundle is complete!
Use `bundle show [gemname]` to see where a bundled gem is installed.
  • Gemfileで指定したgemバージョンとそれに依存するgemがインストールされた。
  • bundle install済のgemバージョンのセットは、bundle listで確認できる。
$ bundle list
Gems included by the bundle:
  * actionmailer (1.2.5)
  * actionpack (1.12.5)
  * actionwebservice (1.1.6)
  * activerecord (1.14.4)
  * activesupport (1.3.1)
  * bundler (1.7.4)
  * rails (1.1.6)
  * rake (0.7.1)
  * sqlite3 (1.3.10)
  • と同時に、このリストはbundlerの管理下で利用されるgemバージョンでもある。
    • たとえRails-4.1.7やRake-10.3.2が追加インストールされたとしても、
    • bundlerの管理下で実行する限り、上記gemの利用が保証されるのだ。
  • ちなみに、bundle install(オプション指定無し)は、gemデフォルトの場所にインストールされる。
$ gem env home
/Users/zari/.rbenv/versions/1.8.7-p374/lib/ruby/gems/1.8

$ bundle show rails
/Users/zari/.rbenv/versions/1.8.7-p374/lib/ruby/gems/1.8/gems/rails-1.1.6

$ bundle show rake
/Users/zari/.rbenv/versions/1.8.7-p374/lib/ruby/gems/1.8/gems/rake-0.7.1

$ bundle show sqlite3
/Users/zari/.rbenv/versions/1.8.7-p374/lib/ruby/gems/1.8/gems/sqlite3-1.3.10
bundlerの管理下で実行
  • bundlerの管理下で実行するためには、bundle execを付加してコマンド実行する必要がある。
    • bundle execありなら、たとえRails-4.1.7やRake-10.3.2がインストールされていても、Rails-1.1.6とRake-0.7.1が利用されるのだ。
    • bundle exec無しでは、今まで同様、インストールされているgemの中で最新のバージョンが利用されてしまう...。
  • rails-1.1.6のscaffoldなら、以下のようなコマンド操作になる。
      • -d sqlite3 = データベースにsqlite3を指定
$ bundle exec rails todo -d sqlite3
$ cd todo
# config/boot.rbを修正する
$ bundle exec script/generate model todo
# db/migrate/001_create_todos.rbを修正する
$ bundle exec rake db:migrate
$ bundle exec script/generate scaffold todo
$ bundle exec script/server
  • 上記コマンド操作の途中で修正するファイルは、以下のとおり。
# config/boot.rbを修正する
@@ -25,7 +25,7 @@
       rails_gem = Gem.cache.search('rails', "=#{version}").first
 
       if rails_gem
-        require_gem "rails", "=#{version}"
+        gem "rails", "=#{version}"
         require rails_gem.full_gem_path + '/lib/initializer'
       else
         STDERR.puts %(Cannot find gem for Rails =#{version}:
@@ -35,7 +35,7 @@
         exit 1
       end
     else
-      require_gem "rails"
+      require "rails"
       require 'initializer'
     end
   end
# db/migrate/001_create_todos.rbを修正する
class CreateTodos < ActiveRecord::Migration
  def self.up
    create_table :todos do |t|
      t.column :body,       :string
      t.column :due,        :date
      t.column :done,       :boolean
    end
  end

  def self.down
    drop_table :todos
  end
end
  • ブラウザでアクセスしてみると...


bundlerとたった4行のGemfileで、確実に動作するRails-1.1.6環境を素早く作れた!

# Gemfile
source "https://rubygems.org"
gem "rails", "1.1.6"
gem "rake", "0.7.1"
gem "sqlite3"

デフォルトのgem

  • 以上のように、利用するgemのバージョンまで指定できるbundlerは非常に便利。
  • gemコマンド同様、常にbundleコマンドも使えるようにしておきたい。
    • 今後は、gem installに代わって、bundle installを使いたいくらい。
  • ところでbundlerもgemなので、最初にgem install bundlerが必ず必要になる。
  • OSX標準のRubyしか使っていない時なら、gem install bundlerは1回で済む。問題ない。
  • ところが、今やrbenvで必要なRuby環境をいくつでも、素早くインストール可能になった。
  • 場合によっては、実験的にインストールとアンインストールを何度も繰り返すこともある。
  • 今のままではその度にgem install bundlerを繰り返すことになってしまう...。面倒くさい。

その面倒を解決してくれるのが、rbenv-default-gemsなのだ。

  • 前回、rbenvをインストールする時にrbenv-default-gemsもインストールしておいた。
    • というより、rbenv-default-gemsの依存関係によって、rbenvもインストールしたのだった。
  • よって、~/.rbenv/default-gemsファイルに、"bundler"と書き込んでおくだけで幸せになれる。
$ echo bundler >> ~/.rbenv/default-gems
$ cat ~/.rbenv/default-gems
bundler
  • ~/.rbenv/default-gemsに書かれたgemは、rbenv installする時に、同時にインストールされる。
  • 現在のRuby-1.8.7-p375を一旦削除して、試してみた。
$ rbenv uninstall 1.8.7-p375
rbenv: remove /Users/zari/.rbenv/versions/1.8.7-p375? y
  • すかさず、再インストールしてみると...
$ rbenv install 1.8.7-p375
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/usr/include/c++/4.2.1
Checking out http://svn.ruby-lang.org/repos/ruby/branches/ruby_1_8_7...
Installing ruby-1.8.7-p375...
Installed ruby-1.8.7-p375 to /Users/zari/.rbenv/versions/1.8.7-p375

Downloading rubygems-1.6.2.tgz...
Installing rubygems-1.6.2...
Installed rubygems-1.6.2 to /Users/zari/.rbenv/versions/1.8.7-p375

Fetching: bundler-1.7.4.gem (100%)
Successfully installed bundler-1.7.4
1 gem installed
Installing ri documentation for bundler-1.7.4...
Installing RDoc documentation for bundler-1.7.4...
  • bundlerもインストールされた!
$ rbenv global 1.8.7-p375 
$ gem list

 *** LOCAL GEMS ***

bundler (1.7.4)

素晴らしい!

  • ~/.rbenv/default-gemsには、bundlerに限らず、あらゆるgemを指定できる。
  • Ruby標準になって欲しいと思うgemがあれば、それも書いておくと良さそう。
  • 以前のRuby-1.8.7-p375環境も素早く復元できた。
$ cd ~/Desktop/rails116
$ bundle
Fetching gem metadata from https://rubygems.org/..........
Installing rake 0.7.1
Installing activesupport 1.3.1
Installing actionpack 1.12.5
Installing actionmailer 1.2.5
Installing activerecord 1.14.4
Installing actionwebservice 1.1.6
Installing rails 1.1.6
Installing sqlite3 1.3.10
Using bundler 1.7.4
Your bundle is complete!
Use `bundle show [gemname]` to see where a bundled gem is installed.

$ gem list

 *** LOCAL GEMS ***

actionmailer (1.2.5)
actionpack (1.12.5)
actionwebservice (1.1.6)
activerecord (1.14.4)
activesupport (1.3.1)
bundler (1.7.4)
rails (1.1.6)
rake (0.7.1)
sqlite3 (1.3.10)

$ cd todo
$ bundle exec script/server
./script/../config/boot.rb:25:Warning: Gem::cache is deprecated and will be removed on or after August 2011.  Use Gem::source_index.
./script/../config/boot.rb:25:Warning: Gem::SourceIndex#search support for String patterns is deprecated, use #find_name
=> Booting WEBrick...
=> Rails application started on http://0.0.0.0:3000
=> Ctrl-C to shutdown server; call with --help for options
[2014-11-05 09:58:00] INFO  WEBrick 1.3.1
[2014-11-05 09:58:00] INFO  ruby 1.8.7 (2013-12-22) [i686-darwin13.4.0]
[2014-11-05 09:58:00] INFO  WEBrick::HTTPServer#start: pid=41225 port=3000
  • 何の問題もなく、さきほど作ったtodoプロジェクトが起動した。


Gemfileの書き方

バージョン指定
  • Gemfileのバージョンは、もう少し柔軟に指定できる。
# rails 1.1.6を利用する
gem "rails", "1.1.6"

# rails 1.1.2以上を利用する
gem "rails", ">=1.1.2"

# rails 1.1.2以上 && 1.1系で最新のものを利用する
gem "rails", "~>1.1.2"
:source =>
  • ソース(gemをダウンロードするサーバー)を個別に指定する。
gem "rails", "1.1.6", :source => https://gems.example.com
:git =>
# masterの最新コミットをインストール
gem 'jpdate', :git => 'https://github.com/zarigani/jcal.git'

# :tag, :branch, :refなどを指定してインストール
gem 'jpdate', :git => 'https://github.com/tenderlove/nokogiri.git', :branch => '1.4'
  • ちなみに、gemコマンドのデフォルトとは異なる場所にインストールされた。
$ bundle show jpdate
/Users/zari/.rbenv/versions/1.8.7-p375/lib/ruby/gems/1.8/bundler/gems/jcal-cf7b86d88e08
:path =>
  • インストール済のgemをbundlerの管理に含める。
# ダウンロードフォルダに展開されたmecab-ruby-0.994をbundlerの管理下におく
gem "mecab-ruby", :path => '~/Downloads/ruby/1.8/gems/mecab-ruby-0.994'
詳細

プロジェクト配下にgemをインストール

  • 今まではbundle installの時、一切のオプションは指定せず、実行してきた。
  • その場合、bundlerはgemコマンドデフォルトの場所(gem env home)にインストールする。
  • 例えば、rbenvが管理するRuby-1.8.7-p375のgem env homeは、以下のように設定されている。
$ rbenv version
1.8.7-p375 (set by /Users/zari/.rbenv/version)

$ gem env home
/Users/zari/.rbenv/versions/1.8.7-p375/lib/ruby/gems/1.8
  • 様々なプロジェクトでbundle installを繰り返していると、当然ながらgem env homeの中はgemとバージョンで溢れ返る。
  • 溢れ返ったとしても、bundlerが必要なgemとバージョンを取りまとめているので、問題はないはず。
  • 但し、溢れ返って混沌とした状態のgem env homeは、何が必要で、何が不要か、もはや理解不能な状態になってくる。
  • あるプロジェクトで不要なgemも、他のプロジェクトでは必要かもしれない。また、その逆もあり得る。
  • 実験的にインストールして、もはや使わなくなったgemもあるはずなのに、削除するのが怖い...。
  • gem env homeにすべてのgemを詰め込んでしまうのは、あまり良い状態とは言えないのではないか?
  • 特に実験的なプロジェクトでgemを試用する場合は、gem env homeにはインストールしたくない。

そう言った気持ちを察してか、bundlerにはプロジェクト配下にインストールするオプションがある!

  • さっそく、todoプロジェクト配下にgemをインストールしてみる。
  • まずは以前と同様にGemfileを作成しておく。
$ cd ~/Desktop/rails116/todo
$ bundle init
Writing new Gemfile to /Users/zari/Desktop/rails116/todo/Gemfile
$ cat <<EOS >> Gemfile
gem "rails", "1.1.6"
gem "rake", "0.7.1"
gem "sqlite3"
EOS
  • そして、bundle installの時に--pathオプションを指定する。
    • オプションの値には、インストール先のパスを指定するのだ。
    • 通常は、--path=vendor/bundleが推奨されている。
$ bundle install --path=vendor/bundle
Fetching gem metadata from https://rubygems.org/...........
Resolving dependencies...
Installing rake 0.7.1
Installing activesupport 1.3.1
Installing actionpack 1.12.5
Installing actionmailer 1.2.5
Installing activerecord 1.14.4
Installing actionwebservice 1.1.6
Installing rails 1.1.6
Installing sqlite3 1.3.10
Using bundler 1.7.4
Your bundle is complete!
It was installed into ./vendor/bundle
  • todoプロジェクトのvendor/bundle以下に、gemがインストールされた!
$ find vendor/bundle/ruby/1.8/gems/* -d 0
vendor/bundle/ruby/1.8/gems/actionmailer-1.2.5
vendor/bundle/ruby/1.8/gems/actionpack-1.12.5
vendor/bundle/ruby/1.8/gems/actionwebservice-1.1.6
vendor/bundle/ruby/1.8/gems/activerecord-1.14.4
vendor/bundle/ruby/1.8/gems/activesupport-1.3.1
vendor/bundle/ruby/1.8/gems/rails-1.1.6
vendor/bundle/ruby/1.8/gems/rake-0.7.1
vendor/bundle/ruby/1.8/gems/sqlite3-1.3.10
  • 試しに、gem env homeのgemをbundler以外、すべて削除してみる。
$ gem list --no-version|grep ^[a-z]|grep -v bundler|xargs gem uninstall -aIx
Successfully uninstalled actionmailer-1.2.5
Successfully uninstalled actionpack-1.12.5
Successfully uninstalled actionwebservice-1.1.6
Successfully uninstalled activerecord-1.14.4
Successfully uninstalled activesupport-1.3.1
Removing rails
Successfully uninstalled rails-1.1.6
Removing rake
Successfully uninstalled rake-0.7.1
Successfully uninstalled sqlite3-1.3.10

$ gem list

 *** LOCAL GEMS ***

bundler (1.7.4)
  • これで、gem env homeにはbundler以外、もう何も残っていない。
  • サーバーを起動してみると...
$ bundle exec script/server
./script/../config/boot.rb:25:Warning: Gem::cache is deprecated and will be removed on or after August 2011.  Use Gem::source_index.
./script/../config/boot.rb:25:Warning: Gem::SourceIndex#search support for String patterns is deprecated, use #find_name
=> Booting WEBrick...
=> Rails application started on http://0.0.0.0:3000
=> Ctrl-C to shutdown server; call with --help for options
[2014-11-05 14:52:56] INFO  WEBrick 1.3.1
[2014-11-05 14:52:56] INFO  ruby 1.8.7 (2013-12-22) [i686-darwin13.4.0]
[2014-11-05 14:52:56] INFO  WEBrick::HTTPServer#start: pid=45168 port=3000
  • gem env homeの中は空っぽだけど、
  • それでもtodoプロジェクトは動いた!


bundle execによって、vendor/bundleのgemが利用されているのだ!

--path=vendor/bundleを保持する仕組み
  • 一旦--path=vendor/bundleオプションを指定すると、
  • 作業ディレクトリには、.bundle/configが作成される。
$ cat .bundle/config
    • -
BUNDLE_PATH: vendor/bundle BUNDLE_DISABLE_SHARED_GEMS: '1'
      • BUNDLE_PATH: vendor/bundleは、--path=vendor/bundleオプションの保存。
      • BUNDLE_DISABLE_SHARED_GEMS: '1'は、既存のgemが存在しても共有せず、vendor/bundleへコピーする設定。
  • .bundle/configの内容を確認するbundle configコマンドもある。
$ bundle config
Settings are listed in order of priority. The top value will be used.
path
Set for your local app (/Users/zari/Desktop/rails116/todo/.bundle/config): "vendor/bundle"

disable_shared_gems
Set for your local app (/Users/zari/Desktop/rails116/todo/.bundle/config): "1"
  • 上記のように、--pathオプションを指定すると.bundle/configに保持されるので、
  • その後はbundle installのみでも、--path=vendor/bundleへのインストールとなる。
$ bundle install
Using rake 0.7.1
Using activesupport 1.3.1
Using actionpack 1.12.5
Using actionmailer 1.2.5
Using activerecord 1.14.4
Using actionwebservice 1.1.6
Using rails 1.1.6
Using sqlite3 1.3.10
Using bundler 1.7.4
Your bundle is complete!
It was installed into ./vendor/bundle
--pathオプションを解除する
  • では、プロジェクトごとのbundle管理をやめて、再びgem env homeでbundle管理するには?
  • --systemオプションを指定すると、gem env homeにインストールされるのだ。
$ bundle install --system
Fetching gem metadata from https://rubygems.org/..........
Installing rake 0.7.1
Installing activesupport 1.3.1
Installing actionpack 1.12.5
Installing actionmailer 1.2.5
Installing activerecord 1.14.4
Installing actionwebservice 1.1.6
Installing rails 1.1.6
Installing sqlite3 1.3.10
Using bundler 1.7.4
Your bundle is complete!
Use `bundle show [gemname]` to see where a bundled gem is installed.

$ gem list

 *** LOCAL GEMS ***

actionmailer (1.2.5)
actionpack (1.12.5)
actionwebservice (1.1.6)
activerecord (1.14.4)
activesupport (1.3.1)
bundler (1.7.4)
rails (1.1.6)
rake (0.7.1)
sqlite3 (1.3.10)
  • そして、.bundle/configの内容は削除された。
$ cat .bundle/config
 --- {}

$ bundle config
Settings are listed in order of priority. The top value will be used.
  • 以降は、--pathオプションを指定しない限り、常にgem env homeへのインストールとなるのだ。

Railsプロジェクトの遅延作成

遅延というキーワードは崇高な気がするが、とっても単純な話。

  • 例えば、rails todoコマンドでプロジェクトを始めようとする時の悩み。
    • 実験的なプロジェクトなので、todoプロジェクトフォルダ内でbundleを管理したい。
    • それにはtodoフォルダ内でbundle install --path=vendor/bundleする必要がある。
    • そこでrails todoによって、todoプロジェクトフォルダを生成しようとすると、railsコマンドがないよ、ってエラーで警告されてしまう...。
  • Railsを、自身が生成するプロジェクト内に含めてbundle管理することはできないのだろうか?


以下の手順でRails自身のプロジェクト内でbundle管理できた!

$ mkdir todo
$ cd todo
$ bundle init
Writing new Gemfile to /Users/zari/Desktop/rails116/todo/Gemfile

$ cat <> Gemfile
gem "rails", "1.1.6"
gem "rake", "0.7.1"
gem "sqlite3"
EOS

$ bundle install --path=vendor/bundle
Fetching gem metadata from https://rubygems.org/...........
Resolving dependencies...
Installing rake 0.7.1
Installing activesupport 1.3.1
Installing actionpack 1.12.5
Installing actionmailer 1.2.5
Installing activerecord 1.14.4
Installing actionwebservice 1.1.6
Installing rails 1.1.6
Installing sqlite3 1.3.10
Using bundler 1.7.4
Your bundle is complete!
It was installed into ./vendor/bundle

$ bundle exec rails . -d sqlite3
  • 先にmkdir todoして、bundle環境を整えてから、railsコマンドを使うのだ!
  • railsコマンドの引数には、todoフォルダ内で . 指定するのがポイント。
  • 以降の手順は今までどおり。
# config/boot.rbを修正する
$ bundle exec script/generate model todo
# db/migrate/001_create_todos.rbを修正する
$ bundle exec rake db:migrate
$ bundle exec script/generate scaffold todo
$ bundle exec script/server