(この記事でもともと書いていた解決法はよく考えたらトンデモナイので,「追記:よりマシな解決法(2017-01-21)」をご参照ください)
Ruby のプログラムで gem を読み込むとき,Bundler を使えば gem のバージョンが管理しやすくて便利なのだけど。
ライブラリーの中に require
でエラーを出すものがあった場合の挙動が,Bundler 1.10 → 1.11 で変わったようなので報告する。
経緯
gtk2 を require するときのエラーを無視したい
gtk2 という gem に含まれる機能を使いたいのだが,gtk2 は本来 GUI 環境で何かするものなので, X Window などの GUI 環境が無いシステムで
require 'gtk2'
とすると,
Cannot open display: (Gtk::InitError)
という例外が発生する。いまの場合,このエラーは単純に無視してもかまわないので,
begin
require 'gtk2'
rescue RuntimeError
end
にすればいい。
Bundler を使うときはどうするか
Bundler を使うときは,Gemfile に
gem 'gtk2'
と書くわけだが,実際の require
は
Bundler.require
で行う。
さて,Bundler 1.10.2 の頃は,Bundler.require
したときに Gtk::InitError
が出ていたので,ちょっと乱暴だけれども
begin
Bundler.require
rescue RuntimeError
end
と書いていれば用は足りていた。(Gtk::InitError
は RuntimeError
の下位クラス)
ある日突然死んだ
ところが,ある日突然,プログラムが動かなくなった。その前後でプログラムに変更は加えていないはず。
いろいろな可能性を探っていたところ,Bundler のバージョンが 1.10.5 から 1.11.2 に上がったことが怪しいと思われた。
原因と対策
原因は,Bundler 1.11 で,Bundler.require
時に gem が出した例外の扱い方が変わったことだった。
Bundler 1.11 では,gtk2 が Gtk::InitError
を出してもそのままにせず,
There was an error while trying to load the gem 'gtk2'. (Bundler::GemRequireError)
を出してくれるようになったらしい。
この Bundler::GemRequireError
は RuntimeError
の下位クラスではないので,前述の rescue
では拾ってくれなかったんである。
そこで,
begin
Bundler.require
rescue StandardError
end
に変えてみたところ,従来通りの動作になった。(Bundler::GemRequireError
は StandardError
の下位クラス)
感想
焦った~
Bundler を使うと require
する gem のバージョンが合理的に制御できていいんだけども,bundler
gem 自体は Gemfile には書かず,バージョンを管理しない(していなかった)。落とし穴だったなあ。
追記:よりマシな解決法(2017-01-21)
よく考えたら,bundler の仕様についての記述は問題ないと思うが,問題の解決法としては良くないと思ったので,ここで訂正したい。
まず,Bundler の仕様変更前に使っていた解決法は,
begin
Bundler.require
rescue RuntimeError
end
だったわけだけど,これだと atk2
が引き起こすエラー以外も全部無視することになって,他の gem の require で問題が起きていても気づかない。
実際に発生していた例外は Gtk::InitError
なわけだから,
rescue RuntimeError
でなく
rescue Gtk::InitError
にしておけばマシだったと思う。
Bundler の仕様変更後に対応する解決法でも,
rescue StandardError
ではなく,
rescue Bundler::GemRequireError
とすべきだった。ただ,これでも広すぎる。ほかの gem が同じような例外を出しても Bundler::GemRequireError
になっちゃうので。
それから,書くのを忘れていたけど,Bundler.require
での例外を無視するこのような解決法を取る場合,
gem 'atk2'
は Gemfile の一番最後に書かないといけない。
でないと,atk2
の require で例外が発生したあと,それ以降の gem の require が行われないからだ。
これは実際ハマったんだけど,記事に入れるのを忘れていた。
結局,いまのところ,一番マシな解決法は,atk2
の require を Bundler にはさせないということだと考えている。
つまり,Gemfile では
gem 'gtk2', require: false
としておいて,自動 require をやめさせる(Bundler.require
では require されない)
そして,
Bundler.require
したあとに
begin
require 'gtk2'
rescue Gtk::InitError
end
とする。
これならば gtk2
による Gtk::InitError
だけを無視することになって問題が少ない。
また,Gemfile
中で gem 'atk2'
を書く位置が自由になる。