Ruby 1.9.2とRubyGems 1.3.7とGem.pathの消失

Debianのバグレポート(Bug#588125)によると、こんな具合いになることがわかった。

$ gem1.9.1 list
/usr/lib/ruby/1.9.1/rubygems/source_index.rb:68:in `installed_spec_directories': undefined method `path' for Gem:Module (NoMethodError)
Ruby 1.9.2とRubyGems 1.3.7の組み合わせで、どちらもパッケージを利用している。RubyGemsのコードは、この場合、Ruby 1.9.2に含まれているものではなく、RubyGems 1.3.7からのものが使われている。Gem.pathがないっていうのはなかなか興味深いことだなあとちょっと調べてみた。

現象の確認

$ ruby1.9.1 -ve 'p Gem.path'
ruby 1.9.2dev (2010-07-30) [i486-linux]
["/home/akira/.gem/ruby/1.9.1", "/usr/lib/ruby/gems/1.9.1"]

特に問題ない。次。

$ ruby1.9.1 -e 'require "rubygems"; p Gem.path'
/usr/lib/ruby/1.9.1/rubygems/source_index.rb:68:in `installed_spec_directories': undefined method `path' for Gem:Module (NoMethodError)
[...]
おや。
$ ruby1.9.1 -e 'require "rubygems"'
/usr/lib/ruby/1.9.1/rubygems/source_index.rb:68:in `installed_spec_directories': undefined method `path' for Gem:Module (NoMethodError)
[...]
    from /usr/lib/ruby/1.9.1/rubygems.rb:839:in `searcher'
    from /usr/lib/ruby/1.9.1/rubygems.rb:478:in `find_files'
    from /usr/lib/ruby/1.9.1/rubygems.rb:982:in `load_plugins'
    from /usr/lib/ruby/1.9.1/rubygems.rb:1138:in `<top>'
    from <internal:lib/rubygems/custom_require>:29:in `require'
    from <internal:lib/rubygems/custom_require>:29:in `require'
    from -e:1:in `<main>'

おやおや。

$ ruby1.9.1 -e 'p Gem.path; begin; require "not exist"; rescue LoadError; end; require "rubygems"; p Gem.path'
["/home/akira/.gem/ruby/1.9.1", "/usr/lib/ruby/gems/1.9.1"]
["/home/akira/.gem/ruby/1.9.1", "/var/lib/gems/1.9.1"]
おやおやおや。 ある状況でGem.pathが消えてしまうらしいことがわかった。このあたりでRuby 1.9.2に入っているRubyGemsと単体で配布されているものとのdiffをとったりもして、どうやらこういうことらしいとわかった。
$ ruby1.9.1 -e 'p Gem.path; Gem::QuickLoader.remove; require "rubygems"; p Gem.path'
["/home/akira/.gem/ruby/1.9.1", "/usr/lib/ruby/gems/1.9.1"]
["/home/akira/.gem/ruby/1.9.1", "/var/lib/gems/1.9.1"]
回避策としてはとりあえずはこれでいい。ただ、どうしてこういう結果になるのかを把握するまでには、ここからさらにけっこうな時間がかかった。-d付きで実行したりデバッガ使ったり。printfデバッグしているつもりが表示内容がまずかったせいで別の例外を起こしているのにしばらく気付かなかったり。

からくり

ようやくわかったところによると、こういう流れがあるようだ。 まず「require 'rubygems'」がどこかで(たとえばgemコマンドの中で)行われてrubygems.rbが読み込まれる。このrubygems.rbはRubyGems 1.3.7由来のものである。 rubygems.rbは内部で「require 'rubygems/defaults/operating_system'」としているのだが、このoperating_systemというのはシステム上にない(*)。ここでRuby 1.9.2の「require」はRubyGemsが置き換えた後のものであることを思い出す。したがってrubygems/defaults/operating_system.rb(や.so)がなければ、RubyGemsの機構を使ってこれを救済できないか試みられる。 ところで、今問題にしている環境では、RubyGemsはRubyGems 1.3.7由来であり、Ruby 1.9.2に含まれるものではなかった。しかしながら、この時点で動作している「require」はRuby 1.9.2に含まれるRubyGemsに由来するものなのである。これはRubyインタプリタが内部に持っているgem_preludeの中にあって、$LOAD_PATH上にあるrubygems/custom_require.rb(=RubyGems 1.3.7由来)のそれとは動作が異なる。 ではそれによって何が起こるのかというとGem.try_activateが呼び出される。その実体はGem::QuickLoader.try_activateであり、これもRuby 1.9.2に由来する。このコードはGem::QuickLoader.load_full_rubygems_libraryを呼び出し、その先でGem::QuickLoader.removeが呼び出される。そうしてgem_preludeに由来するGem関係の何もかもを忘れ去ろうとする。
module QuickLoader
  @loaded_full_rubygems_library = false

  def self.remove
    return if @loaded_full_rubygems_library

    @loaded_full_rubygems_library = true

    class << Gem
      undef_method(*Gem::GEM_PRELUDE_METHODS)
    end

    remove_method :const_missing
    remove_method :method_missing

    Kernel.module_eval do
      undef_method :gem if method_defined? :gem
    end
  end
[...]
end

さて、このremove、そもそもどうして呼び出されたのかというと「require "rubygems"」がきっかけとなって内部的に行われた「require "rubygems"」による。removeは、本来のRubyGemsがロードされるべき場面で使用されるはずのものであるが、この状況ではすでにrubygems.rb(RubyGems 1.3.7由来)が読み込まれてしまっている。したがって、removeした後で行われるに違いないと思われていたGem以下の再定義はなされない。

そうしてGem.pathがなくなってしまう。

組み合わせが問題

この問題はRuby 1.9.2だけで構成されていれば起きることはない。というのも、Ruby 1.9.2に含まれるrubygems.rbは、その先頭で次のようなことをしている。

gem_disabled = !defined? Gem

unless gem_disabled
  # Nuke the Quickloader stuff
  Gem::QuickLoader.remove
end

この記述はRubyGems 1.3.7に含まれるrubygems.rbにはない。

Ruby 1.9.2だけから構成されていれば、上のストーリーの最初の「require 'rubygems'」の時点でgempreludeに由来するコードは消し去さられ、フルセットのRubyGemsが読み込まれことになる。また、RubyGems 1.3.7に由来するrubygems.rbの先頭に同じ記述を加えた場合、同じように最初の「require 'rubygems'」の時点でgempreludeに由来するコードは消し去られる。

いずれのケースでも、問題の発端となる「require 'rubygems/defaults/operatingsystem'」の時点で動作するのはgempreludeに由来する「require」であるのに変わりはないが、removeの効果は一度だけしか現れない(@loadedfullrubygems_library)ためGem.path消失問題が起きることはない。

(*) なお、operating_systemがシステム上にないのは特に問題ない。以下のようにLoadErrorをrescueしていて、そのファイルがなければなくても構わないというコードになっている。

begin
  # Defaults the operating system (or packager) wants to provide for RubyGems.
  require 'rubygems/defaults/operating_system'
rescue LoadError
end

-dオプション

この話とは直接的には関係ないけれど、今回の調査をしている中でRuby 1.9.2の-dオプションが地味に便利になっていることに気付いた。
$ ruby1.9.1 -d -e 'require "rubygems"'
[...]
/usr/lib/ruby/1.9.1/rubygems.rb:630: warning: method redefined; discarding old path
<internal:gem_prelude>:43: warning: previous definition of path was here
[...]
<internal:lib/rubygems/custom_require>:29: warning: loading in progress, circular require considered harmful - /usr/lib/ruby/1.9.1/rubygems.rb
[...]

追記(2010-09-03)

どこで何が起きているのかを問題としていたので長々と書いたが、とりあえず今だけなんとか動けばよいというならRubyに--disable-gemsを指定して実行することで回避できなくもない。どの程度、どんな場合に有効かはわからないが。

$ ruby1.9.1 /usr/bin/gem1.9.1 list
/usr/lib/ruby/1.9.1/rubygems/source_index.rb:68:in `installed_spec_directories': undefined method `path' for Gem:Module (NoMethodError)
[...]
$ ruby1.9.1 --disable-gems /usr/bin/gem1.9.1 list

*** LOCAL GEMS ***