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.gemlibxml2を使ったRubyのXML/HTMLパーサーなんですが、ビルド時に利用したlibxml2のバージョンを覚えていて、実行時に違うバージョンを使うと文句を垂れます。というのも特定のlibxml2はバグがアレすぎてnokogiri.gemがまともに動かないのでそれを排除する目的でそういうことをしているんだと思います。

さて、こうなった時には誰が違うバージョンのlibxml2nokogiri.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.dyliblibxml2をロードした犯人っぽいですね。で、

$ otool -L ... /Security.framework/Versions/A/Security:
    ... /Security.framework/Versions/A/Security (compatibility...
    ...
    /usr/lib/libxar-nossl.dylib (compatibility version 1.0.0, ...
    ... ↑あっ

ほう、Security.frameworklibxar-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.gemlibxml2をロードしているとは思いもよりませんでした。memcached.gemmemcachedのクライアントですが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レジスタから順にrsirdxrcxr8r9に引数が入ってるので、それを見ていきます。こんな感じ。

$ 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まで教えてください!