gemライブラリの依存はGemfileではなくgemspecに記述する理由

  • 76
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

Conclusion

bundle gemrails plugin newなどでgemひな形を作るときGemfileと.gemspecファイルができる.
どちらを使っても依存関係を記述できるように見えてしまう.
結論を言うと他の様々なサイトでも指摘されている通り、gemライブラリの依存関係はgemspecに記述し、Gemfileはgemspec1行書くにとどめておく.

以下のようにgemに依存するgemライブラリを作成し、Gemfile, .gemspecそれぞれに依存関係を記述して挙動をたしかめた.

スクリーンショット 2015-05-01 12.27.22.png

■今回テストするコード

dependent1/lib/dependent1.rb
require "dependent1/version"

module Dependent1
  def self.version
    VERSION
  end
end
dependent2/lib/dependent2.rb
require "dependent1"
require "dependent2/version"

module Dependent2
  def self.dependent1_version
    ::Dependent1.version
  end
  def self.version
    ::Dependent2::VERSION
  end
end

dependent2(gemに依存するgem)にdependent1のバージョンを出力するdependent1_versionメソッドを定義し、それぞれのパターンでどのdependent1バージョンが返されるか確認していく.

Gemfileに依存記述する場合

:exclamation: Gemfileはgemライブラリの依存を定義するものとしては使えません!!

dependent2/Gemfile
source 'https://rubygems.org'
# Specify your gem's dependencies in dependent2.gemspec
gemspec
gem "dependent1", "0.0.1"

dependent2下でbundle install

スクリーンショット 2015-05-01 12.36.43.png

bundle installすれば正常にdependent1を使えるようになる

% bundle exec irb
irb(main):001:0> require "dependent2"
=> true
irb(main):002:0> Dependent2.version
=> "0.1.1"
irb(main):003:0> Dependent2.dependent1_version
=> "0.0.1"

ruby app下でbundle install

スクリーンショット 2015-05-01 12.41.29.png

しかし、ruby app側でdependent2をbundle installすると、dependent1は読み込まれない.
これはつまり、再帰的にgemの依存解決するときにgemが持っているGemfileの情報は使われないということ.

rubyapp/Gemfile
source 'https://rubygems.org'
gem "dependent2", "0.1.1"

dependent2内でdependent1を使おうとしているとLoadErrorになる.

% bundle exec irb
irb(main):001:0> require "dependent2"
LoadError: cannot load such file -- dependent1
    from /usr/local/ruby-2.1.1/lib/ruby/gems/2.1.0/gems/dependent2-0.1.1/lib/dependent2.rb:1:in `require'
    from /usr/local/ruby-2.1.1/lib/ruby/gems/2.1.0/gems/dependent2-0.1.1/lib/dependent2.rb:1:in `<top (required)>'
    from (irb):1:in `require'
    from (irb):1
    from /usr/local/ruby/bin/irb:11:in `<main>'

ruby app下でdependent1,dependent2を両方記述してbundle install

スクリーンショット 2015-05-01 12.43.31.png

ruby app側でdependent1もGemfileに記述すれば、dependent2内で使おうとしているdependent1も使えるようになるが、当然ruby app側で指定したバージョンが有効になる.
dependent2がdependent1のバージョン整合性を担保できないということになる.

rubyapp/Gemfile
source 'https://rubygems.org'
gem "dependent2", "0.1.1"
gem "dependent1", "0.0.2"
% bundle exec irb
irb(main):001:0> require "dependent2"
=> true
irb(main):002:0> Dependent2.dependent1_version
=> "0.0.2" # <- ruby appのGemfileで指定したバージョン!!

gemspecに依存記述する場合

dependent2/dependent2.gemspec
# 省略
Gem::Specification.new do |s|
  # 省略
  s.add_dependency "dependent1", "0.0.1"
end

dependent2下でbundle install

特に問題なし.

% bundle exec irb
irb(main):001:0> require "dependent2"
=> true
irb(main):002:0> Dependent2.dependent1_version
=> "0.0.1"

ruby app下でbundle install

スクリーンショット 2015-05-01 14.11.31.png

gemspecに依存が記述されていればrubyappがbundle installされたとき依存するgemが再帰的に取得される.
結果dependent2の中でdependent1が使えるようになり、dependent2がバージョンをコントロールできるようになる.

rubyapp/Gemfile
source 'https://rubygems.org'
gem "dependent2", "0.0.1"
% bundle exec irb
irb(main):001:0> require "dependent2"
=> true
irb(main):002:0> Dependent2.dependent1_version
=> "0.0.1"

ruby app下でdependent1,dependent2を両方記述してbundle install

スクリーンショット 2015-05-01 14.11.44.png

ruby app側にもdependent2が依存するgemを明示的に記述すると、きちんと競合が検知されてbundle installがエラーになる.

rubyapp/Gemfile
source 'https://rubygems.org'
gem "dependent2", "0.0.1"
gem "dependent1", "0.0.2"
% bundle install
Fetching gem metadata from https://rubygems.org/..
Resolving dependencies...
You have requested:
  dependent1 = 0.0.1

The bundle currently has dependent1 locked at 0.0.2.
Try running `bundle update dependent1`
% bundle update dependent1
Fetching gem metadata from https://rubygems.org/..
Resolving dependencies...
Bundler could not find compatible versions for gem "dependent1":
  In Gemfile:
    dependent2 (= 0.0.2) ruby depends on
      dependent1 (= 0.0.2) ruby

    dependent1 (0.0.1)

その他メモ

Gemfileにはgitから直接コードを取得するようにgitリポジトリパスを指定することができるが、この書き方はgemspecではできないらしい. なので、ちゃんとgem化してrubygemsにアップロードするか、gemのプライベートホスティングなどする必要がある.

## この書き方はGemfileでしかできない
gem 'dependent2', '0.0.1', git: 'git@github.com:metheglin/dependent2.git', branch: 'master'

また、gemspecでバージョンを固定すると、ruby app側で使いたいライブラリのバージョンが制限されるためgemを作る場合バージョン制約はなるべく緩いほうがよさそう.

Environment

% gem -v
2.2.2