Ruby の bundler を 1.13 から 1.15 にアップデートしたら今まで動いたプログラムが動かなくなりました。
こんな感じ:
% bundle _1.13.7_ exec ruby -r./hoge.rb -e Hoge.new % % bundle _1.15.4_ exec ruby -r./hoge.rb -e Hoge.new hoge.rb:3:in `initialize': uninitialized constant Hoge::Timeout (NameError) Did you mean? Time from -e:1:in `new' from -e:1:in `<main>'
この hoge.rb の中味はこんな感じで、
class Hoge def initialize Timeout.timeout(5){sleep 1} end end
本来 require "timeout"
しないと使えない Timeout を使っていたのでエラーになるのが正しいんですけど、今までは bundler が暗黙的に timeout ライブラリを読み込んでいたので気がつかなかったという話でした。
bundler 1.13 では有効だったけど 1.14 では有効でないトップレベル定数(≒クラス)を抽出してみました。
% comm -23 <(bundle _1.13.7_ exec ruby -e 'puts Object.constants.sort') <(bundle _1.14.6_ exec ruby -e 'puts Object.constants.sort') Addrinfo BasicSocket CGI Date DateTime IPSocket Net OpenSSL Resolv ScanError SecureRandom Socket SocketError StringScanner TCPServer TCPSocket Timeout TimeoutError UDPSocket UNIXServer UNIXSocket Zlib
1.14 と 1.15 の違いはこんな感じ。
% comm -23 <(bundle _1.14.6_ exec ruby -e 'puts Object.constants.sort') <(bundle _1.15.4_ exec ruby -e 'puts Object.constants.sort') BundlerVendoredPostIt OptParse OptionParser Tempfile
RSpecとかの自動テストでは、1プロセスで全ファイルをテストしてしまい、他のファイルで require したものが有効になってしまって、今回の現象は見つけられませんでした。テストライブラリが require してる場合もあるでしょうし。
一応、今回見つけたファイル以外についても require が漏れていないかどうか調べてみました。
自分が作るライブラリは、中で使用している外部クラスについて冒頭で require するようにしているので、ライブラリを読み込んだ後でそのクラスを参照してエラーになるかどうかを確認すれば良さそうです。
for class in $(<class.txt); do # class.txt は上で抽出したクラス一覧 echo "== $class ==" for rb in **/*.rb; do grep -qw $class $rb && (ruby -r $rb -e $class 2> /dev/null || echo $rb) done done
ただこれだと文字列リテラルやコメント内の文字列にも引っかかってしまうので、それをいちいち目で確認するのも面倒でした。
Ripperとかを使ってRubyプログラムとしてパースして、その中で参照している定数をすべて抽出して、使えるかどうかを確認すればいいのかもしれませんが…。