Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Ruby: default gemをgemコマンドで更新してもbundlerは元のdefault gemを使う

🔗 概要

以下のように、default gemsであるRubyのopensslgem update opensslコマンドで更新したとします。
一方、bundlerを使うRubyアプリ(ここではRails)ではopensslをインストールしていないとします。

$ gem list|rg openssl
openssl (3.3.2, default: 3.3.0)

$ bundle list|rg openssl
# 表示なし

この場合、bundlerを使うRubyアプリでは新しいopenssl 3.3.2が使われると思いきや、何とデフォルトの3.3.0が使われるのです。

このような状態でRailsでopenssl 3.3.2を使うには、RailsのGemfileで明示的にopensslを追加してbundle installする必要があります↓。

# Gemfile

gem "openssl"

なお、どのgemがdefault gemもしくはbundled gemかについては以下で確認できます。

参考: Standard Gems 3.4.7
参考: standard librariesとdefault gemsとbundled gemsの違い - ESM アジャイル事業部 開発者ブログ

🔗 背景と経緯

この振る舞いは、以下のトラブルシューティングで知りました。

環境

  • macOS: Tahoe 26.1
  • Ruby: 3.4.7
    • rubygems: 3.6.9
    • bundler: 2.6.9
  • Rails: 8.1.1

bin/importmap pinコマンドを実行したところ、以下のエラーが発生しました。

$ bin/importmap pin turbo-transition
/Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/importmap-rails-2.2.2/lib/importmap/packager.rb:133:in 'Importmap::Packager#post_json': Unexpected transport error (OpenSSL::SSL::SSLError: SSL_connect returned=1 errno=0 peeraddr=[2001:4860:4802:36::15]:443 state=error: certificate verify failed (unable to get certificate CRL)) (Importmap::Packager::HTTPError)
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/importmap-rails-2.2.2/lib/importmap/packager.rb:24:in 'Importmap::Packager#import'
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/importmap-rails-2.2.2/lib/importmap/commands.rb:180:in 'Importmap::Commands#for_each_import'
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/importmap-rails-2.2.2/lib/importmap/commands.rb:17:in 'Importmap::Commands#pin'
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/thor-1.4.0/lib/thor/command.rb:28:in 'Thor::Command#run'
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/thor-1.4.0/lib/thor/invocation.rb:127:in 'Thor::Invocation#invoke_command'
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/thor-1.4.0/lib/thor.rb:538:in 'Thor.dispatch'
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/thor-1.4.0/lib/thor/base.rb:584:in 'Thor::Base::ClassMethods#start'
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/importmap-rails-2.2.2/lib/importmap/commands.rb:190:in '<main>'
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/3.4.0/bundled_gems.rb:82:in 'Kernel.require'
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/3.4.0/bundled_gems.rb:82:in 'block (2 levels) in Kernel#replace_require'
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/bootsnap-1.18.6/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:30:in 'Kernel#require'
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/zeitwerk-2.7.3/lib/zeitwerk/core_ext/kernel.rb:34:in 'Kernel#require'
    from bin/importmap:4:in '<main>'
/Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/net-protocol-0.2.2/lib/net/protocol.rb:46:in 'OpenSSL::SSL::SSLSocket#connect_nonblock': SSL_connect returned=1 errno=0 peeraddr=[2001:4860:4802:36::15]:443 state=error: certificate verify failed (unable to get certificate CRL) (OpenSSL::SSL::SSLError)
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/net-protocol-0.2.2/lib/net/protocol.rb:46:in 'Net::Protocol#ssl_socket_connect'
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/3.4.0/net/http.rb:1736:in 'Net::HTTP#connect'
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/3.4.0/net/http.rb:1636:in 'Net::HTTP#do_start'
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/3.4.0/net/http.rb:1625:in 'Net::HTTP#start'
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/3.4.0/net/http.rb:1064:in 'Net::HTTP.start'
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/3.4.0/net/http.rb:858:in 'Net::HTTP.post'
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/importmap-rails-2.2.2/lib/importmap/packager.rb:131:in 'Importmap::Packager#post_json'
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/importmap-rails-2.2.2/lib/importmap/packager.rb:24:in 'Importmap::Packager#import'
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/importmap-rails-2.2.2/lib/importmap/commands.rb:180:in 'Importmap::Commands#for_each_import'
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/importmap-rails-2.2.2/lib/importmap/commands.rb:17:in 'Importmap::Commands#pin'
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/thor-1.4.0/lib/thor/command.rb:28:in 'Thor::Command#run'
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/thor-1.4.0/lib/thor/invocation.rb:127:in 'Thor::Invocation#invoke_command'
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/thor-1.4.0/lib/thor.rb:538:in 'Thor.dispatch'
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/thor-1.4.0/lib/thor/base.rb:584:in 'Thor::Base::ClassMethods#start'
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/importmap-rails-2.2.2/lib/importmap/commands.rb:190:in '<main>'
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/3.4.0/bundled_gems.rb:82:in 'Kernel.require'
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/3.4.0/bundled_gems.rb:82:in 'block (2 levels) in Kernel#replace_require'
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/bootsnap-1.18.6/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:30:in 'Kernel#require'
    from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/zeitwerk-2.7.3/lib/zeitwerk/core_ext/kernel.rb:34:in 'Kernel#require'
    from bin/importmap:4:in '<main>'

Railsで以下のissueを見つけました。

まず、本家OpenSSL 3.xのCライブラリ(Rubyのopenssl gemのことではありません)にあった問題が3.6.0で修正されました。

openssl/openssl - GitHub

Rubyのopenssl gem v3.3.0はその修正を反映していなかったため、importmapで上述のエラーが発生します。その後v3.3.2で本家3.6.0の修正に対応しました(#957)。

ruby/openssl - GitHub

🔗 症状

冒頭の状況を再録します。

$ gem list|rg openssl
openssl (3.3.2, default: 3.3.0)

$ bundle list|rg openssl
# 表示なし

以上の経緯から、gemコマンドでopenssl gemを3.3.2にアップグレードすれば、importmapのOpenSSLエラーを修正できるだろうと考えました。

しかしgem update opensslコマンドを実行してもまったく問題が解消されず、慌てました。

おかしいと思って、Ruby 3.4.7をリビルトしたり、gemコマンドで全部のdefault gemsなどを取得し直したりしたのですが、一向に解決できません。

🔗 解決方法

ふと、issue #55886のコメントにこんなことが書いてありました。


#55886のコメントより

そこでGemfileにopenssl gemを追加してbundle installしたところ、あっさり問題が解消しました。

# Gemfile

gem "openssl"
# bundle install実行後
$ gem list|rg openssl
openssl (3.3.2, default: 3.3.0)

$ bundle list|rg openssl
  * openssl (3.3.2)

$ bin/importmap pin turbo-transition
Pinning "turbo-transition" to vendor/javascript/turbo-transition.js via download from https://ga.jspm.io/npm:turbo-transition@0.4.0/dist/turbo-transition.esm.js

なお、Ruby本体の新しいリリースでdefault gemsが更新されれば、rbenvなりasfdなりmiseなりで新しいRubyをインストール/ビルドし、そのRubyを使うように環境を切り替えれば、この対処は不要になります。

🔗 補足

冒頭に書いたように、gem update opensslを実行しても、もともとあったdefault gemとしてのopensslは置き換えられず、更新版のopensslが追加されるだけです。

しかしこのことは、意外にもbundlerのドキュメントには見当たりませんでした↓。

参考: Bundler: The best way to manage a Ruby application's gems

ChatGPTに相談してみたところ、bundlerのadd_default_gems_toメソッド↓ではdefault gemを取得するときに、Rubyに同梱されているオリジナルのバージョンしか取得していないと説明してもらいました。

# rubygems/bundler/lib/bundler/rubygems_integration.rb#280
    def add_default_gems_to(specs)
      specs_by_name = specs.reduce({}) do |h, s|
        h[s.name] = s
        h
      end

      Bundler.rubygems.default_stubs.each do |stub|
        default_spec = stub.to_spec
        default_spec_name = default_spec.name
        next if specs_by_name.key?(default_spec_name)

        specs_by_name[default_spec_name] = default_spec
      end

      specs_by_name
    end

BundlerはGem::Specification.default_stubsを参照しますが、これはRubyが ビルドした時点で同梱したdefault gemのバージョンだけを返します。gem updateで追加されたopenssl(3.3.2)はこのリストには入りません。


そもそもbundler単体というよりはrubygemと組み合わさったときの挙動なので、ドキュメントでそこまでカバーしていなくてもしょうがないかもしれません。

解消して欲しい気もしますが、いかにもどこかで互換性が壊れそうなので、今後類似の問題が起きたらGemfileに明示的に追加することにします。

なお、ChatGPTが以下のアドバイスもくれました。

同じパターンは date, rexml, net-http など他の default gem でも起こりうるので、挙動が怪しいときは一度 Gemfile に明示してみると原因切り分けがしやすくなります。

関連記事

Ruby: Gemfileに.ruby-versionを読み込む便利技(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。