Rubyのbundlerをアップデートしたらプログラムが動かなくなった話

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プログラムとしてパースして、その中で参照している定数をすべて抽出して、使えるかどうかを確認すればいいのかもしれませんが…。