Mac OS Xで動的ライブラリのバージョン違いの警告が出た
たまに、nokogiri.gem
を使っているときに、
WARNING: Nokogiri was built against LibXML version 2.7.8,
but has dynamically loaded 2.7.3
と言われて凹むことがあります、というか、先日ありました。原因は多分Mac OS X 10.7.3にしたことなんですがこういう時に何をすればいいのかという話です。
nokogiri.gem
はlibxml2
を使ったRubyのXML/HTMLパーサーなんですが、ビルド時に利用したlibxml2
のバージョンを覚えていて、実行時に違うバージョンを使うと文句を垂れます。というのも特定のlibxml2
はバグがアレすぎてnokogiri.gem
がまともに動かないのでそれを排除する目的でそういうことをしているんだと思います。
さて、こうなった時には誰が違うバージョンのlibxml2
をnokogiri.gem
より 先に ロードしているのを知る必要があります。普通は他のgemが明らかにlibxml2
を使っていたりして、あぁ、こいつが違うバージョンのlibxml2
をロードしてるのかー って気がつけることもあるのですが、まったく見当がつかない場合が問題です。
そこで、Mac OS Xで動的ライブラリのいろいろを司ってるのはdyld
ですが、その環境変数で便利なDYLD_PRINT_LIBRARIES
を使います。詳しくはman 1 dyld
。
DYLD_PRINT_LIBRARIES=1 ruby a_script_requires_many_gems.rb
とすると、ロードされたライブラリがずらずら出てきますので、どのタイミングで期待してないlibxml2
がロードされているのか眺めます。例えば、こんな感じ。
dyld: loaded: ... /gems/memcached-1.3.1.1/lib/rlibmemcached.bundle
dyld: loaded: /usr/lib/libsasl2.2.dylib
...
dyld: loaded: ... /Kerberos.framework/Versions/A/Kerberos
dyld: loaded: ... /Heimdal.framework/Versions/A/Heimdal
dyld: loaded: ... /Security.framework/Versions/A/Security
dyld: loaded: ... /CoreFoundation.framework/Versions/A/CoreFoundation
...
dyld: loaded: /usr/lib/libxar-nossl.dylib
dyld: loaded: /usr/lib/libDiagnosticMessagesClient.dylib
dyld: loaded: /usr/lib/libbz2.1.0.dylib
dyld: loaded: /usr/lib/libxml2.2.dylib
... ↑あっ
おや、ここでMac OS Xのlibxml2
が呼ばれていますね。これが原因っぽい。じゃあこれをロードした奴は誰だってことになるので、これより上でロードしているライブラリをotool -L
で調べていきます。
$ otool -L /usr/lib/libxar-nossl.dylib
/usr/lib/libxar-nossl.dylib:
/usr/lib/libxar-nossl.dylib (compatibility version 1.0.0, ...
...
/usr/lib/libxml2.2.dylib (compatibility version 10.0.0, ...
... ↑あっ
ほう、libxar-nossl.dylib
がlibxml2
をロードした犯人っぽいですね。で、
$ otool -L ... /Security.framework/Versions/A/Security:
... /Security.framework/Versions/A/Security (compatibility...
...
/usr/lib/libxar-nossl.dylib (compatibility version 1.0.0, ...
... ↑あっ
ほう、Security.framework
がlibxar-nossl.dylib
をロードしていますね。という感じで掘り進めます。で、この依存関係を呼ばれる逆順に書き出すとこうなります。
/usr/lib/libxml2.2.dylib
← /usr/lib/libxar-nossl.dylib
← Security.framework
← Kerberos.framework
← /usr/lib/sasl2/libgssapiv2.2.so
← /usr/lib/libsasl2.2.dylib ←これはotool -Lではわかりませんが、明らかですね。
← gems/memcached-1.3.1.1/lib/rlibmemcached.bundle
なんと、お前かー! まさか、memcached.gem
がlibxml2
をロードしているとは思いもよりませんでした。memcached.gem
はmemcached
のクライアントですがXMLを使う余地など無いのでパッと見さっぱりわからないですね。
多分、10.7.3でどこかの誰かがこの依存関係を創り上げたのではないかと思っていますが、今後SASL関係のやつらが全部libxml2
をロードすると思うとげんなりです。
で、この場合、解決策はnokogiri.gem
を素直に/usr/lib/libxml2.2.dylib
を使うようにビルドしなおせばいいのですがまあ、最適解はそれぞれの状況に応じて変わるのでなんとも言えません。
というわけで、原因がわかってめでたしめでたし!
おまけ - gdbで追いかける
最初、DYLD_PRINT_LIBRARIES
なんて気が付かなかったのでgdb
してdlopen
でブレイクポイント立ててロードしている奴らを知ろうと思いました。結果から言えばdlopen
だけではすべてのdyld
のロードを見られるわけではないので失敗したのですが、忘れないようにやり方をメモしておきます。この場合は、dtrace
しても良いんだけど。
ここでのポイントはdlopen
はデバッグ情報がないのでそのままではgdb
で引数が表示できないこと。そこでx86_64
の場合、rdiレジスタ
から順にrsi
、rdx
、rcx
、r8
、r9
に引数が入ってるので、それを見ていきます。こんな感じ。
$ gdb --args ruby a_script_requires_many_gems.rb
(gdb) b dlopen ←dlopenにブレイクポイント立てる
(gdb) r ←実行
...
Breakpoint 1, 0x00007fff93c18929 in dlopen () ←止まる
(gdb) i r ←レジスタ一覧
...
rax 0x1 1
rbx 0x7fff8cbe1000 140735554654208
rcx 0x69 105
rdx 0x9 9
rsi 0x10 16
rdi 0x7fff8cbe1980 140735554656640
rbp 0x7fff5fbfe650 0x7fff5fbfe650
rsp 0x7fff5fbfe640 0x7fff5fbfe640
...
(gdb) p (char *)0x7fff8cbe1980 ← rdi をchar *として見る。$rdi でも同じ。
$1 = 0x7fff8cbe1980 "/usr/lib/libobjc.A.dylib" ←見えた
(gdb) display (char *)$rdi ←止まるたびに出るようにする
(gdb) c ←続行
...
Breakpoint 1, 0x00007fff93c18929 in dlopen ()
1: (char *) $rdi = 0x100275210 "... /thread.bundle" ←見えた
(gdb)
便利ですね!
多分もっとかっこいい方法があるはずなので、是非@niwまで教えてください!