rbsecp256k1 gemについて調べてる

先日のhsbtさんの日記を読んでいて

https://www.hsbt.org/diary/20221227.html#p02

RubyGems 3.4 では C 拡張をインストール後にビルドディレクトリを make clean するようになった

タイトルが全部シリーズです。C 拡張な gem は ext ディレクトリの下に .c などを配置してその下でビルドしたのち lib にコピー(=インストール)という手続きを踏んでいます。今回 RubyGems 3.4 ではこの処理のうち、インストールが終わったあとは ext の下を make clean して不要なファイルを消すようになったんですが、この影響でいくつかの gem で動かないよ〜という報告が来たのであれこれ直していた。

https://github.com/rubygems/rubygems/issues/6205

要は ext の下を直接参照するようなコードがあって、そういう行儀が悪いコードは落ちますというものだった。最初はこれは revert した方がいいかなあとも思ったけど、数次第なら gem の方を直せばいいか、という気持ちになってきた。

ほ〜んと思っていたら、自分が携わっているプロジェクトでもこれに関係するエラーが出ているようだったので調べています。

対象のgemは https://github.com/etscrivner/rbsecp256k1 です。

とりあえず、Ruby 3.2を入れて irbrequire 'rbsecp256k1' したらエラーが出ることは確認しました。

% ruby -v
ruby 3.2.0 (2022-12-25 revision a528908271) [arm64-darwin20]

% brew install automake openssl libtool pkg-config gmp libffi  # ref: https://github.com/etscrivner/rbsecp256k1#macos
🍺 ... # brew install のログは割愛

% gem install rbsecp256k1
Building native extensions. This could take a while...
Successfully installed rbsecp256k1-5.1.0
Parsing documentation for rbsecp256k1-5.1.0
Installing ri documentation for rbsecp256k1-5.1.0
Done installing documentation for rbsecp256k1 after 3 seconds
1 gem installed

% irb
irb(main):001:0> require 'rbsecp256k1'
Ignoring debug-1.7.1 because its extensions are not built. Try: gem pristine debug --version 1.7.1
Ignoring rbs-2.8.2 because its extensions are not built. Try: gem pristine rbs --version 2.8.2            
<internal:/Users/kentarotanaka/.rbenv/versions/3.2.0/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:85:in `require': cannot load such file -- rbsecp256k1/rbsecp256k1 (LoadError)                                                                                                
        from <internal:/Users/kentarotanaka/.rbenv/versions/3.2.0/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:85:in `require'
        from /Users/kentarotanaka/.rbenv/versions/3.2.0/lib/ruby/gems/3.2.0/gems/rbsecp256k1-5.1.0/lib/rbsecp256k1.rb:10:in `<top (required)>'
        from <internal:/Users/kentarotanaka/.rbenv/versions/3.2.0/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:160:in `require'
        from <internal:/Users/kentarotanaka/.rbenv/versions/3.2.0/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:160:in `rescue in require'
        from <internal:/Users/kentarotanaka/.rbenv/versions/3.2.0/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:149:in `require'
        from (irb):1:in `<main>'                                                                          
        from /Users/kentarotanaka/.rbenv/versions/3.2.0/lib/ruby/gems/3.2.0/gems/irb-1.6.2/exe/irb:11:in `<top (required)>'
        from /Users/kentarotanaka/.rbenv/versions/3.2.0/bin/irb:25:in `load'
        from /Users/kentarotanaka/.rbenv/versions/3.2.0/bin/irb:25:in `<main>'
<internal:/Users/kentarotanaka/.rbenv/versions/3.2.0/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:85:in `require': cannot load such file -- rbsecp256k1 (LoadError)
        from <internal:/Users/kentarotanaka/.rbenv/versions/3.2.0/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:85:in `require'
        from (irb):1:in `<main>'
        from /Users/kentarotanaka/.rbenv/versions/3.2.0/lib/ruby/gems/3.2.0/gems/irb-1.6.2/exe/irb:11:in `<top (required)>'
        from /Users/kentarotanaka/.rbenv/versions/3.2.0/bin/irb:25:in `load'
        from /Users/kentarotanaka/.rbenv/versions/3.2.0/bin/irb:25:in `<main>'

cannot load such file -- rbsecp256k1/rbsecp256k1 (LoadError) が出ていますね。

参考になる事例

同様の問題が確認されている ox という gem がありまして、こちらのissue https://github.com/ohler55/ox/issues/300 で調査され、こちらのプルリクエスhttps://github.com/ohler55/ox/pull/301 で解決に至っています。

このプルリクエストでは、このように lib/ox.rb にて .so ファイルを明示的に require しています。

begin
  require_relative 'ox.so'
rescue LoadError
  require 'ox/ox'
end

改めて今回のgemについて

/lib/rbsecp256k1.rb にて require 'rbsecp256k1/rbsecp256k1' で LoadError になっているので、 /lib/rbsecp256k1 ディレクトリ内になにがあるのか?を知りたくなりました。

とりあえず /lib/rbsecp256k1.rb にこんなコードを書き加えて、雑にデバッグしてみます。

puts '*' * 300
Dir.foreach(File.join(__dir__,'rbsecp256k1')) do |item|
  puts item
end
puts '*' * 300

gemをbuildしてirbでrequireしてみると↓のようなログが出ました。

************************************************************************************************************************************************************************************************************************************************************************************************************
.
util.rb
context.rb
version.rb
..
***************************************************************************************************************************************************************************

なんかしらの拡張ライブラリ(.so.bundle など)があるかな?思ったけれどなかったです。

一応 /lib ディレクトリも確認しておこうとおもって今度は↓でデバッグしてみました。

puts '*' * 300
Dir.foreach(File.join(__dir__)) do |item|
  puts item
end
puts '*' * 300
************************************************************************************************************************************************************************************************************************************************************************************************************
rbsecp256k1.rb
.
..
rbsecp256k1
************************************************************************************************************************************************************************************************************************************************************************************************************

先ほどと同じく .so.bundle などのファイルはないですね。

というか、手元の環境(Mac)で gem を build したときに /lib/rbsecp256k1/rbsecp256k1.bundle ファイルが作られていることに気づきました。

じゃあなんでgemにはこのファイルが無いんじゃろ...?ともう少し調べると、gemspecで files の指定が lib 配下 .rb だけになっていたのでした。。

https://github.com/etscrivner/rbsecp256k1/blob/81dcbc5033806625b934211eb4f7173710795bee/rbsecp256k1.gemspec#L16-L20

  s.files = (
    Dir['lib/**/**.rb'] +
    Dir['documentation/**.md'] +
    %w[ext/rbsecp256k1/rbsecp256k1.c ext/rbsecp256k1/extconf.rb Rakefile README.md]
  )

files に指定されていないファイルはパッケージに含まれないので、gemのビルド時に生成された .so.bundle などのファイルが lib 配下に含まれなくなっていたようです。

gemspec を書き換えるだけで解決できそう、ということがわかったので、これから改めて動作確認とプルリクエストを出そうと思います。