Edited at

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

More than 3 years have passed since last update.


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