ja ja ja ja ja ja ja ja ja en ja ja ja ja ja ja ja ja ja ja ja de ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja en ja en ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja en ja en en ja ja ja ja ja ja ja ja pl ja ja ja ja ja ja ja ja en ja ja en ja ja vi ja sv eu ja en en hu ja ja en en pl ja ja jbo hbs_lat en ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja
Rubyはバージョン間でのABIの互換性を保証していません。たとえば、Ruby 2.6用の拡張ライブラリーはRuby 2.7では使えません。そのため、新しいバージョンのRubyがリリースされたらそのバージョンのRuby用にビルドしないといけません。(メンテナンスリリースでは互換性があるのでRuby 2.6.0用の拡張ライブラリーはRuby 2.6.5でも使えます。)
しかし、すべてのfat gem開発者がそんなにタイミングよく作業できるわけではありません。そのため、脆弱性対応リリースが遅くなりがちです。
念のため補足しておくと、これはfat gemにしていない場合でも発生しえます。たとえば、Nokogiriのようにデフォルトで特定バージョンの依存ライブラリーをビルドするタイプのgemでも発生しえます。指定したバージョンのライブラリーに脆弱性があったらバージョンを更新してリリースしないと脆弱なバージョンを使ったままのユーザーが増えてしまいます。明示的に--use-system-librariesを指定すればNokogiriのバージョンを上げなくても対応できるのですが、残念ながら多くのユーザーはそこまで頑張ってくれないでしょう。
新しい依存ライブラリーを使えるまでにタイムラグがある
これもバインディングの場合ですが、バインディング対象のライブラリーが新しいバージョンをリリースしてもfat gemを更新しなければユーザーは新しいバージョンを使えません。
fat gemに対応するとrequireが遅くなる
fat gemに対応するには次のようなコードを入れる必要があります。2.6/io/console.so(ビルド済みバイナリー)があればそっちを優先し、なければio/console.so(自分でビルドしたバイナリー)を読み込むというロジックです。
begin require "#{RUBY_VERSION[/\d+\.\d+/]}/io/console.so" rescue LoadError require 'io/console.so' end
すべてのケースでfat gemを使うなら↓だけで大丈夫です。
require "#{RUBY_VERSION[/\d+\.\d+/]}/io/console.so"
しかし、それではユーザーが自分でビルドして使うという選択肢がなくなります。逆に言うと、開発者がすべてのプラットフォーム向けにfat gemを用意する覚悟を決める必要があります。
それは現実的ではないので、普通は前述のように自分でビルドしたバイナリーにフォールバックします。
そうすると、fat gemを提供していない環境では必ずfat gem用のrequireが失敗します。この分requireが遅くなるということです。$LOAD_PATHにたくさんのパスが入っている環境では無視できないくらい遅くなります。gemをたくさんインストールしているとその分$LOAD_PATHも大きくなります。たとえばRuby on Railsアプリケーションではたくさんのgemを使うことになるので影響が大きいです。
これを回避するために、fat gemを提供している環境でだけフォールバックする対策をとっているgemもあります。(ありました。)
fat gemのリリースを忘れる
多くのgemはrake releaseだけでgemをリリースできるようにしています。そしてこれはすぐに完了します。.gemファイルを作ってrubygems.orgにアップロードするだけだからです。(他にもgit tagをするとかちょろっとしたことをしています。)
しかし、fat gemをリリースするにはもう一手間必要です。各環境用のバイナリーをビルドしてそれぞれの環境毎にfat gemを作り、それらをrubygems.orgにアップロードします。
各環境用のバイナリーは大変です。rake-compiler-dockを使えば楽になりますが、それでも面倒です。
その結果どうなるかというとリリースが億劫になったり、fat gemのリリースを忘れたりします。たとえば、Ruby-GNOMEはリリースが億劫だなと思っていました。たとえば、io-console 0.4.8はfat gemのリリースを忘れていました。
fat gemのリリースが大変
fat gemのリリースは大変なんです。特にバインディングのfat gemのリリースは大変です。
私はRuby-GNOMEでWindows用のfat gemを作っていました。バインディング対象のGTKなどのライブラリーはLinux上でMinGWを使ってクロスコンパイルしていました。これがすごく大変です。というのは、クロスコンパイルしている人がほとんどいないので、バインディング対象のライブラリーのバージョンを上げるとビルドエラーになることがよくあるからです。Ruby-GNOMEをリリースするたびにアップストリームにパッチを送っていたものです。ただ、librsvgがRustを使うようになってクロスコンパイルできなくなったときにfat gemをやめる決心をしました。
fat gemはそんなにポータブルじゃない(気がする)
fat gemは主にWindows向けに提供されていますが、Linux向けに提供している野心的なgemもあります。たとえば、sasscです。
Windowsはバージョンが限られていますし、後方互換性があるので古いWindows向けにビルドしていればいろんなWindowsでもだいたい大丈夫です。
しかし、Linuxディストリビューションはたくさんあり、使っているlibcも違います。スタティックリンクしたバイナリーを用意すれば大丈夫なのかというとそうでもない気がします。どうなんでしょうか。。。?
Pythonのwheelではmanylinuxという(だいたいの)Linux環境で動く仕組み(?)を用意しているので、このくらい頑張れば大丈夫なのかもしれません。が、私としては、この方向で頑張っちゃうの。。。?という気持ちになります。RubyGemsはそうなって欲しくないなという気持ちです。
fat gemの問題点の解決方法
ここまででfat gemの問題点をまとめました。それではfat gemの問題点を解決する方法を示します。それはfat gemをやめることです。どーん!
そもそもfat gemが必要だったのはユーザーがビルド環境を持っていないことが多かったからです。しかし、今は状況が変わっています。ユーザーがビルド環境を持っていないプラットフォームの代表はWindowsでしたが、今はRubyInstaller for Windowsがほぼ標準でDevKitを提供しています。Ruby 2.3以前はそうではなかったですが、Ruby 2.3がEOLになったので、今はWindowsユーザーでもビルド環境があるのです。
Linuxではパッケージをインストールすればすぐにビルド環境を整えられます。
macOSでもXcodeをインストールすればビルド環境を整えられます。Homebrewを使っている人はすでに整っているはずです。
他の環境(たとえば*BSD)でもビルド環境はすぐに整えられるでしょう。
つまり、今はユーザーがビルド環境を持っていると仮定してもよい状態になっています。そのため、fat gemを提供しなくてもユーザーがインストールできる状態が整っています。実際、私はWindowsユーザーがいるRuby-GNOMEでfat gemをやめましたが、最近はWindowsでのインストールトラブルはほとんど報告されていません。
それではfat gemをやめるとうれしいことをまとめます。
新しいRubyをすぐに使える
新しいバージョンのRubyは以前のバージョンのRubyとC APIが変わっている可能性があります。たとえば、Ruby 2.7ではrb_f_notimplement()が変わります。(Ruby 2.7の対応が必要な例)
しかし、多くのC APIは互換性があるのでなにも変更しなくても新しいRubyで動くことが多いです。その場合は、特になにもしなくてもすぐに新しいRubyを使えます。ユーザーは単に新しいRubyを使ってgemをインストールすればよいだけだからです。
また、もしRuby 2.7で動かない場合でも事前にプレビュー版で動作確認し、Ruby 2.7より前にRuby 2.7対応版をリリースしておくこともできます。こうすればクリスマス後にリリースしなくてもよくなるのでgem開発者に余裕があります。
fat gemを使った場合でも、プレビュー版でバイナリーを作ってリリースしておくことができなくはありませんが、rake-compiler-dockなど各種ツールが事前に対応していないと難しいです。
脆弱性対応をシステムに任せられる
バインディングをシステムのライブラリーを使ってビルドするようにしていた場合、バインディング対象のライブラリーに脆弱性があってもシステムのライブラリーを更新すれば対策できます。gemの開発チームよりシステムのライブラリーをメンテナンスしている人たちの方が層が厚いので迅速に脆弱性に対応してもらえます。
なお、Nokogiriのようにデフォルトで依存ライブラリーを自前で管理するタイプのgemはfat gemでもそうでなくても関係ありません。
新しい依存ライブラリーをすぐに使える。。。こともある
バインディングをシステムのライブラリーを使ってビルドするようにしていた場合、バインディング対象のライブラリーの更新はシステムのパッケージシステムが面倒をみてくれます。Debian GNU/Linux sid、Fedora Rawhide、ArchLinux、Homebrew、MSYS2などのように最新のバージョンに随時アップデートされるシステムではgemの更新を待たずに新しいライブラリーを使えます。
ただし、ライブラリーのバージョンアップでAPIが変わった場合はgemの更新が必要です。
requireが遅くならない
fat gem用のrequireがいらなくなるので失敗するrequireを実行しなくてもよくなります。これによりrequireが遅くなりません。
bigdecimalがfat gemのサポートをやめたのはこれが理由です。
面倒なfat gemのリリースをしなくてよくなるので開発者は本来の開発にリソースを注力できます。
fat gemは事前にビルドしたバイナリーをすべてのユーザーが共通で使うことになるので最大公約数の最適化しかできません。
しかし、fat gemをやめて各ユーザーごとにインストールする場合はその環境毎に最適化できません。たとえば、速度が非常に重要な拡張ライブラリーをGCCでビルドする場合は-O3 -march=nativeというオプションをつけてビルドするとその環境向けに最適化されます。たとえば、CPUがSIMDをサポートしていればSIMDを使ったバイナリーを生成することもあります。
fat gemをやめたときの問題点と解決策
fat gemをやめるとユーザーも開発者もハッピーになれそうですね。でも、本当にそうでしょうか?fat gemをやめたときの問題点とその解決策をまとめます。
インストール時間が長くなる
fat gemの場合はビルド済みのバイナリーをコピーするだけなのですぐにインストールは完了します。しかし、fat gemをやめるとインストールするたびにビルドすることになるので時間がかかります。
解決策は。。。特にありません。。。
依存ライブラリーがなくてインストールが失敗しやすくなる
バインディングはバインディング対象のライブラリーがないとインストールに失敗します。たとえば、RMagickはImageMagickがないとインストールに失敗します。Nokogiriがデフォルトで自分で依存ライブラリーをビルドするようになっているのはこの失敗を防ぐためです。
たしかに、自分でビルドしてしまうというのはこの問題の解決策の1つではあります。ただ、そんなに筋がよいとは思えません。脆弱性があったときの対応に関する問題があるからです。
私がオススメする方法はシステムのパッケージシステムを使って自動で足りない依存ライブラリーをインストールする方法です。このための便利gemがnative-package-installerです。私が開発しています。
native-package-installerはpkg-config gemと一緒に使うことを想定していて、extconf.rbに次のように書いておけば、cairoがインストールされていなければ自動でインストールします。
require "pkg-config" require "native-package-installer" unless PKGConfig.have_package("cairo") unless NativePackageInstaller.install(:arch_linux => "cairo", :debian => "libcairo2-dev", :homebrew => "cairo", :macports => "cairo", :msys2 => "cairo", :redhat => "cairo-devel") exit(false) end unless PKGConfig.have_package("cairo") exit(false) end end
なお、RubyInstaller for Windows用のRubyではgemのメタデータにMSYS2のパッケージを指定しておくことで同じ機能(自動で依存ライブラリーをインストールする機能)を実現できます。以下はcairo.gemspecでの例です。
gemspec.metadata["msys2_mingw_dependencies"] = "cairo"
参考:MSYS2 library dependencies - For gem developers - onclick/rubyinstaller2 Wiki
ビルドに失敗してインストールが失敗しやすくなる
fat gemではすでにビルド済みなのでビルドが失敗することはありません。開発者が用意した環境でビルドが成功すればOKです。
一方、ユーザーの環境でビルドする場合は、開発者の環境では成功しているのにユーザーの環境では失敗することがあります。
解決策は、CIでサポートしている環境を常にテストすることです。Travis CIやGitHub Actionsなどを使えば、いろんな環境でテストできます。Linuxの亜種はDockerを使うとよいでしょう。
検討するべき項目が他にもある気がしますが、一通りまとめたので公開します。fat gemをやめたくなりましたか?それともfat gemはやめないで!という気持ちになりましたか?
もし、これはどうなの?という項目があったらなんらかの手段で私に聞いてください。回答します。
現在クリアコードではソフトウェア開発者を募集しています! フリーソフトウェア開発での開発の仕方をベースに、成果や知識を公開し共有しながら業務に携われます。
「Fluentd」のサポートサービスを開始しました。
FirefoxでInternet Explorerのwindow.open()周りの挙動を再現する
Apache Arrow
milter manager
ククログの記事はCC BY-SA 4.0とGFDLのデュアルライセンスで自由に利用できます。
クリアコードはプログラミングが楽しいソフトウェア開発者を1名募集しています。
クリアコードは「クリアコードをいい感じにする人」を1名募集しています。
クリアコードはフリーソフトウェア開発で培った技術力を提供しています。特にMozilla製品(Mozilla FirefoxとMozilla Thunderbird)とRubyとGroonga(全文検索)に関連した開発を得意としています。