can't activate foo-2.0 already activated foo-1.0

bundleを使わず、gemを使っていると時々発生する「can't activate activesupport-5.1.2, already activated activesupport-4.2.9」のようなエラー。

不要なほうのgemを削除すれば回避できるのだが、どうしても気になって調べてみた。

環境はRuby 2.4.1、RubyGemsはRuby同梱のもの。実際のエラーは以下のようなもの。(middlemanでエラーが起きているが、middlemanに原因があるわけではない。)

$ middleman init --help
/Users/akira/.rbenv/versions/2.4.1/lib/ruby/2.4.0/rubygems/specification.rb:2281:in `check_version_conflict': can't activate activesupport-5.1.2, already activated activesupport-4.2.9 (Gem::LoadError)
    from /Users/akira/.rbenv/versions/2.4.1/lib/ruby/2.4.0/rubygems/specification.rb:1407:in `activate'
(略)

activesupport-4.2.9がすでにactivateされているところにactivesupport-5.1.2をさらにactivateしようとした、ということ。では誰が5.1.2をactivateしようとしたのか。そしてそれはなぜか。

というのを確認するためにいくつかのメソッドにデバッグプリントを入れてみた。(紆余曲折しつつ追加していったので過不足あるかもしれない。) 対象メソッドとその機能を簡単に説明すると次の通りとなる。

  • Kernel#require (rubygems/core_ext/kernel_require.rb)
    • requireのRubyGemsによる置き換え、というか現状では標準のrequire
    • 指定されたファイルがロードパスにあればそれを読み込む。なければ指定ファイルを含んでいるgemを検索してactivateした上で、指定ファイルを読み込む。(Kernel#gemGem::Specification#activateを呼び出す。)
  • Kernel#gem
    • 指定されたgemをactivateする。ただしバージョン指定がなければ、requireの内部で呼び出されるので明示的に使用する必要はない。
  • Gem::Specification#activate
    • activateする。ロードパスにgemのものを追加する。
    • そのgemが依存するgemがあればそれもactivateする。
  • Gem::Specification.find_active_stub_by_path
    • stubというのはGem::StubSpecificationのことで、インストールされたgemspec(specificationsディレクトリにあるもの)の# stub:行に対応するものらしい。ここでは本来のgemspecのサブセット程度に考えておいてよい、と思う。
    • 指定されたファイルを含んでいて、かつ、activate済みであるstubを検索して返す。
    • つまり、指定されたファイルを直接的に含むgemを検索する。
  • Gem::Specification.find_in_unresolved
    • unresolvedなgemの中から指定されたファイルを含むgemを検索する。
    • あるgemがactivateされる際、そのgemが依存しているgem群もactivateされる。ただし、activateの候補が複数ある場合にはactivateをいったん保留して、後でactivateする。unresolvedなgemというのは、このいったん保留したgem群のことで、先行するactivate処理の中で出てきたactivate候補gem群と考えられる。
  • Gem::Specification.find_in_unresolved_tree
    • unresolvedなgemが持つ依存関係のツリーに含まれるgem群の中から、指定されたファイルを含むgemを検索する。
  • Gem::Specification#traverse
    • gemの依存ツリーを探索するためのメソッド。Gem::Specification.find_in_unresolved_treeの内部で使用している。

これらのメソッドにデバッグプリントをいれたことで、以下のようなトレース情報を得られるようになった。

$ RUBYOPT=-r$(pwd)/gem_trace.rb middleman init --help
R rubygems
` rubygems
A middleman-cli-4.2.1
:  A thor-0.19.4
:  + thor-0.19.4
+ middleman-cli-4.2.1
(続く)

R#requireA#activateの呼び出しを示している。それぞれに対応する+はrequire/activateが成功したことを、`はすでにrequire/activateされていたことを、それぞれ表す。

ここまでのころは、rubygemsをrequireしようとしたがrequire済みであり、middleman-cli-4.2.1をactivateする中でthor-0.19.4もactivateされたことが分かる。

(続き)
R middleman-core/load_paths
|  S middleman-core-4.2.1
|  R pathname
|  |  T [rack-2.0.3, rack-2.0.1, tilt-2.0.7, tilt-2.0.6, parallel-1.11.2, parallel-1.10.0, servolux-0.13.0, servolux-0.12.0, dotenv-2.2.1, dotenv-2.2.0, activesupport-4.2.9, activesupport-4.2.7.1, padrino-helpers-0.13.3.4, padrino-helpers-0.13.3.3, addressable-2.5.1, addressable-2.5.0, memoist-0.16.0, memoist-0.15.0, rb-fsevent-0.10.2, rb-fsevent-0.9.8, rb-inotify-0.9.10, rb-inotify-0.9.8, fastimage-2.1.0, fastimage-2.0.1, sass-3.4.25, sass-3.4.24, sass-3.4.23, uglifier-3.2.0, uglifier-3.0.4, hashie-3.5.5, hashie-3.5.1, concurrent-ruby-1.0.5, concurrent-ruby-1.0.4, backports-3.8.0, backports-3.6.8]
|  |  :  t rack-2.0.3 {}
|  |  :  t rack-2.0.1 {}
|  |  :  t tilt-2.0.7 {}
(略)
|  |  ` []
|  |  R pathname.so
(略)
|  |  |  :  t backports-3.6.8 {}
|  |  + pathname.so
|  + pathname
+ middleman-core/load_paths
(続く)

S.find_active_stub_by_pathの呼び出し、T.find_in_unresolved_treeの呼び出し、ここには出てこないがU.find_in_unresolvedの呼び出しを示している。

Sに続くのはrequireにあたって使用するgem、T[〜]はrequireを完了するのために検査対象とするunresolvedなgemのリストである。

t#traverseの呼び出しを示してる。続く部分には探索の起点とするgem、そのgemの依存情報と対応するgemの候補を表示している。

抜粋した部分については以下のように解釈ききる。

  • R middleman-core/load_paths - require 'middleman-core/load_paths'が行われた
  • S middleman-core-4.2.1 - middleman-core 4.2.1がactivateされた
  • R pathname - require 'pathname'が行われた
  • T [rack-2.0.3, ...] - pathnameを読み込むために依存ツリーからの探索が行われたが、何も見付からなかった(のでgem_original_requirepathnameが読み込まれたと思われる)
  • R pathname.so - require 'pathname.so'が行われた
  • + pathname.so - pathname.soが読み込まれた
  • + pathname - pathnameが読み込まれた
  • + middleman-core/load_paths - middleman-core/load_pathsが読み込まれた

さて、問題の例外が発生したあたりではどうなっているかというと:

(続き)
|  |  R middleman-core/application
|  |  |  S middleman-core-4.2.1
|  |  |  R active_support/core_ext/integer/inflections
|  |  |  |  S activesupport-4.2.9
|  |  |  |  R active_support/inflector
|  |  |  |  |  S activesupport-4.2.9
|  |  |  |  |  R active_support/inflector/inflections
|  |  |  |  |  |  S activesupport-4.2.9
|  |  |  |  |  |  R thread_safe
|  |  |  |  |  |  |  T [rack-2.0.3, rack-2.0.1, tilt-2.0.7, tilt-2.0.6, parallel-1.11.2, parallel-1.10.0, servolux-0.13.0, servolux-0.12.0, padrino-helpers-0.13.3.4, padrino-helpers-0.13.3.3, addressable-2.5.1, addressable-2.5.0, memoist-0.16.0, memoist-0.15.0, rb-fsevent-0.10.2, rb-fsevent-0.9.8, rb-inotify-0.9.10, rb-inotify-0.9.8, fastimage-2.1.0, fastimage-2.0.1, sass-3.4.25, sass-3.4.24, sass-3.4.23, uglifier-3.2.0, uglifier-3.0.4, hashie-3.5.5, hashie-3.5.1, concurrent-ruby-1.0.5, concurrent-ruby-1.0.4, backports-3.8.0, backports-3.6.8, tzinfo-1.2.3, tzinfo-1.2.2, minitest-5.10.2, minitest-5.10.1]
|  |  |  |  |  |  |  :  t rack-2.0.3 {}
(略)
|  |  |  |  |  |  |  :  t padrino-helpers-0.13.3.4 {"padrino-support (= 0.13.3.4)"=>["padrino-support-0.13.3.4"], "tilt (< 3, >= 1.4.1)"=>["tilt-2.0.7", "tilt-2.0.6"], "i18n (>= 0.6.7, ~> 0.6)"=>["i18n-0.8.6", "i18n-0.8.4", "i18n-0.7.0"]}
|  |  |  |  |  |  |  :  :  t padrino-support-0.13.3.4 {"activesupport (>= 3.1)"=>["activesupport-5.1.2", "activesupport-4.2.9", "activesupport-4.2.7.1"]}
|  |  |  |  |  |  |  :  :  :  t activesupport-5.1.2 {"i18n (~> 0.7)"=>["i18n-0.8.6", "i18n-0.8.4", "i18n-0.7.0"], "tzinfo (~> 1.1)"=>["tzinfo-1.2.3", "tzinfo-1.2.2"], "minitest (~> 5.1)"=>["minitest-5.10.2", "minitest-5.10.1"], "concurrent-ruby (>= 1.0.2, ~> 1.0)"=>["concurrent-ruby-1.0.5", "concurrent-ruby-1.0.4"]}
|  |  |  |  |  |  |  :  :  :  :  t i18n-0.8.6 {}
|  |  |  |  |  |  |  :  :  :  :  t i18n-0.8.4 {}
|  |  |  |  |  |  |  :  :  :  :  t i18n-0.7.0 {}
|  |  |  |  |  |  |  :  :  :  :  t tzinfo-1.2.3 {"thread_safe (~> 0.1)"=>["thread_safe-0.3.6", "thread_safe-0.3.5"]}
|  |  |  |  |  |  |  ` [thread_safe-0.3.6, tzinfo-1.2.3, activesupport-5.1.2, padrino-support-0.13.3.4, padrino-helpers-0.13.3.4]
|  |  |  |  |  |  |  A thread_safe-0.3.6
|  |  |  |  |  |  |  + thread_safe-0.3.6
|  |  |  |  |  |  |  A tzinfo-1.2.3
|  |  |  |  |  |  |  + tzinfo-1.2.3
|  |  |  |  |  |  |  A activesupport-5.1.2
|  |  |  |  |  |  |  X activesupport-5.1.2 failed (can't activate activesupport-5.1.2, already activated activesupport-4.2.9)
|  |  |  |  |  |  X thread_safe failed
|  |  |  |  |  X active_support/inflector/inflections failed
|  |  |  |  X active_support/inflector failed
|  |  |  X active_support/core_ext/integer/inflections failed
|  |  X middleman-core/application failed
(以下略)

S activesupport-4.2.9active_support/inflector/inflectionsをrequireするにあたり、activesupport-4.2.9が使用されたのを示している。

続いてthread_safeをrequireするために探索(T)を行い、padrino-helpers 0.13.3.4→padrino-support 0.13.3.4→activesupport 5.1.2→tzinfo 1.2.3とたどってthread_safe 0.3.6または0.3.5を検出(t)している。

そしてthread_safe 0.3.6、tzinfo 1.2.3、とactivate(A)を行い、さらにactivesupport 5.1.2をactivateしようとする。しかしそれは失敗する。X activesupport-5.1.2 failedがそれを示しており、このときの例外メッセージがコマンド実行時のエラーメッセージに合致する。

ここで問題なのは、例外発生に直接的につながるpadrino-support 0.13.3.4が、activesupportの5.1.2など4.2.9より新しいバージョンのactivesupportを要求しているわけではないという点である。

t padrino-support-0.13.3.4 {"activesupport (>= 3.1)"=>["activesupport-5.1.2", "activesupport-4.2.9", "activesupport-4.2.7.1"]}

この通り、activesupport 3.1以上に依存していて、その候補には5.1.2、4.2.9、4.2.7.1の各バージョンが挙げられている。activate済みの4.2.9で満足するのではなく、あえてさらに5.1.2をactivateしようとして例外が起きていると考えられる。

ここからはきちんと追いかけたわけではないので、妥当かどうか分からないが、おそらく、この候補リストはバージョンの新しい順番になっていて、できるだけ新しいgemをactivateしようとしているのではないかと思う。

もしもそいうことであれば、候補の中のactivate済みのgemを優先させればよいかもしれない。あるいは、この場面だけに限っていえば、問題のactivesupport-5.1.2を無視するようにしても回避は可能だ。

実際に試してみたところ、以下により、今回問題となっているエラーを回避できることを確認できた。

module GemDepPatch_
  def to_specs
    super.sort_by {|spec| spec.activated? ? -1 : 1 } # activate済みのgemを優先
  end
end
Gem::Dependency.prepend(GemDepPatch_)
$ RUBYOPT=-r$(pwd)/gem_patch.rb middleman init --help
Usage:
  middleman middleman:cli:init [TARGET]

(略)

デバッグプリントのためのパッチはgitsに置いた。