fat gemを簡単に作れるようにするgemであるrake-compilerをメンテナンスしている須藤です。2019年にもfat gemをやめる話をしていましたが、5-6年経ってもまだfat gemが使われているので、この5-6年でのアップデートを紹介します。
おさらい
まず、簡単に関連情報を整理しておきます。
まず、「fat gem」についてです。fat gemとはビルド済みバイナリーが入ったgemのことです。pre-compiled gemとかnative gemとかbinary gemとかと呼ばれることもあります。
fat gemの嬉しいところは次の通りです。
- インストール時にビルドツールが必要ない(そもそもビルドしないから)
- インストール時にビルドで失敗しない(そもそもビルドしないから)
- インストールが速い(ビルドしないから)
fat gemのキビシイところは次の通りです。
- 開発者視点:
- たくさんの環境ごとにfat gemを用意してリリースする必要がある(たとえば、このあたりを便利にしてくれるrake-compiler-dockは13個の環境をサポートしている)
- クロスコンパイルをしないといけないが、クロスコンパイルは普通にコンパイルするより大変(多くのプロダクトはクロスコンパイルサポートをがんばっていない)
- 新しいCRubyがリリースされたらできるだけ早く新しいCRuby用のfat gemをリリースする必要がある
- 依存ライブラリーに脆弱性が発見されたら迅速に対応してリリースしないといけない
- リリース作業が大変
- ユーザー視点:
- 新しいCRubyで使えるようになるまでにラグがある
- 依存ライブラリーが脆弱性に対応してもfat gemが対応してくれないと対応できない
- 複数のfat gemが同じ依存ライブラリーの違うバージョンを使っているとコンフリクトすることがある
同じことをもう少し説明しているものは↓にあるので、省略されすぎてピンとこないという場合は↓も読んでみてください。
ところで、2024年にどのくらいのgemがfat gemとしてリリースされたか調べたいんですが、どうやって調べればいいか知っている人いません?RubyGems.org APIだとできない気がするんですよねぇ。fat gemが減っているのか増えているのかとかそういうことを調べたいんですよねぇ。
この5-6年でのアップデート
その後、なにもしていなかったわけではなくて、少しずつなにかをしていました。ということで、この5-6年の話をまとめておきます。
まず、2022年にこのあたりに関心が高そうな次の人たちと相談しました。
- @postmodern: ruby-installというRubyのインストーラーとかを作っている
- @flavorjones: Nokogiriとかrake-compiler-dockとかをメンテナンスしている。Nokogiriはfat gemをかなりがんばっているgem。
RubyGemsが依存ライブラリーを自動でインストールしてくれる機能を提供するといいんじゃない?ということでこんな仕様がいいかも?というのを相談していました。
結局、RubyGemsに提案するところまでいかなかったのですが、それぞれの人たちのこういうユースケースに対応できないとダメじゃない?というのが集まったのが成果だと、私は思っています。
その後、RubyKaigi 2022があったので、RubyGemsの開発者でもある@hsbtに相談したところ、RubyGemsに似たようなissueがすでにあるということを教えてもらいました。
そこでは、JRuby用には似たような機能がjar-dependenciesで実現されているという話を聞いたりしたのですが、前述のGistでの相談がまとまっていなかったので、そんなに話は進みませんでした。(私もがんばって話を進めませんでした。)
といっても、一応、私はこう実現するのがいいんじゃない?という案はあったので、それを実現するための準備は進めていました。それがLoad plugin immediately by kou · Pull Request #6673 · rubygems/rubygemsで、2023年に仕上げてはありました。それのサイドストーリーとしてRed Data Tools:RDocとRubyGemsを疎結合にしたい!がありますが、ここでは関係ないので省略。
私もRubyGemsが依存ライブラリーを自動でインストールしてくれるといいだろうなぁという方向性には同意していたんですが、いきなりRubyGems本体に入れるのはあまりよくないんじゃないかと思っていました。というのも、前述の相談でいくつものユースケースやコーナーケースがありそうというのはわかっていたからです。RubyGems本体に入れると段階的にそういうユースケースやコーナーケースを解消していくのはキビシイ気がしていました。RubyGemsはRuby本体と一緒にも配布されているので、簡単に新しいバージョンにアップグレードしてもらいにくいからです。
ということで、まずはRubyGemsのプラグインからはじめて、ある程度落ち着いたらRubyGems本体の機能にするのが現実的ではないかと思っていました。それのブロッカーだと思っている挙動を改良するのが、前述のプルリクエストになります。
どういう挙動かと言うと、gem install rubygems-pluginA normal-gem
みたいにRubyGemsプラグインと普通のgemを一緒にインストールしたときに、rubygems-pluginA
はnormal-gem
のインストール時に動かないという挙動です。gem install rubygems-pluginA && gem install normal-gem
とすると動くのですが、そうすると、normal-gem
の依存ライブラリーにrubygems-pluginA
を入れてgem install normal-gem
しただけでインストール時にrubygems-pluginA
を動かすということができなくなります。まぁ、文章での説明だけだとピンとこないと思いますが、なにか機能が足りなかったんだなと思ってもらえればそれで十分です。
で、そういう準備をしたり、2023年に自動で依存ライブラリーをシステムにインストールされるがイヤなんだよねーと言われたり、2024年にJRubyでもApache Arrowを使えるようにしたときに前述のjar-dependenciesを触って雰囲気がわかったり、とかがあったので、そろそろやるか!という気持ちになりました。
ようは、5-6年かけてちょっとずつ進めてきて、準備も整ってきたのでようやくやる気になったわけです。
rubygems-requirements-system
ということで、rubygems-requirements-systemというRubyGemsのプラグインを作りました。もともと作っていたnative-package-installerをRubyGemsプラグインとして使えるようにしたようなやつです。
jar-dependenciesのようにspec.requirements
に依存ライブラリーの情報を入れておくと、gem install
時に自動で依存ライブラリーをインストールしてくれます。jar-dependenciesを触ってみて、このアプローチでも結構いけそうだなと思ったのでspec.requirements
を使うようにしました。
前述の相談をしていたときは、RubyInstallerがmsys2_mingw_dependencies
でやっているようにメタデータを使うのがいいかも?という話をしていましたが、1つのメタデータに複雑な情報を文字列として入れるのはしんどそうだと私は思っていました。メタデータの値には文字列しか入れられないという仕様になっているのです。JSONとかYAMLとかで入れる?という話もありますが、RubyGemsがjson gemとかpsych gemとかに依存するといろいろ面倒そう(json gemをアップグレードするためにRubyGemsを使える?)なのでやりたくありません。まぁ、RubyGemsには、すでにPsychを使ってYAMLを読んでいたり、gemrc
用の独自YAMLパーサーを持っていたりしますが。。。
まぁ、そんなrubygems-requirements-systemの使い方を、一応、簡単に紹介しておきます。詳細はREADMEをどうぞ。
基本的な使い方
まず、実行時の依存gemにrubygems-requirements-system
を追加します。
Gem::Specification.new do |spec|
# ...
spec.add_runtime_dependency("rubygems-requirements-system")
# ...
end
その後に、spec.requirements
に依存ライブラリーの情報を入れていきます。ざっくりいうと次のフォーマットで情報を指定します。
system: #{pkg-configのID}: #{プラットフォーム}: #{インストールしたいパッケージ名}
それでは、具体例を見ていきましょう。
1つの依存ライブラリーを自動インストール
この例はGObjectを自動でインストールする例です。
GObjectのpkg-configのIDはgobject-2.0
なのですべてsystem: gobject-2.0:
から始まっています。その後に、対応するプラットフォームごとにGObjectをインストールできるパッケージ名を指定しています。
Gem::Specification.new do |spec|
# ...
# Install GObject. Package ID is pkg-config's package name for now.
# We'll add support for other package system's name such as CMake
# package's name.
# We can specify package names for each platform.
spec.requirements << "system: gobject-2.0: alt_linux: glib2-devel"
spec.requirements << "system: gobject-2.0: arch_linux: glib2"
spec.requirements << "system: gobject-2.0: conda: glib"
spec.requirements << "system: gobject-2.0: debian: libglib2.0-dev"
spec.requirements << "system: gobject-2.0: gentoo_linux: dev-libs/glib"
spec.requirements << "system: gobject-2.0: homebrew: glib"
spec.requirements << "system: gobject-2.0: macports: glib2"
# We can omit the Red Hat Enterprise Linux family case because
# "pkgconfig(gobject-2.0)" can be generated automatically.
spec.requirements << "system: gobject-2.0: rhel: pkgconfig(gobject-2.0)"
# ...
end
依存ライブラリーをどれか自動インストール
mysql2 gemのようにMySQLのクライアント実装かMariaDBのクライアント実装のどちらかがあればOKという場合は次のように指定します。pkg-configのIDのところでmysqlclient|libmariadb
と|
で複数のIDを指定しているところがポイントです。
Gem::Specification.new do |spec|
# ...
# We need mysqliclient or libmariadb for this gem.
# Try libmysqlclient-dev and then libmariadb-dev on Ubuntu. Because
# "debian: libmariadb-dev" is also used on Ubuntu.
#
# mysqlclient or libmariadb will be satsfied by a system package.
spec.requirements << "system: mysqlclient|libmariadb: ubuntu: libmysqlclient-dev"
# Try only libmariadb-dev on Debian.
#
# libmariadb will be satsfied by a system package.
spec.requirements << "system: mysqlclient|libmariadb: debian: libmariadb-dev"
# ...
end
1つの依存ライブラリーのために複数のパッケージをインストール
パッケージングミスのような気もするのですが、1つの依存ライブラリーを使えるようにするために複数のパッケージをインストールしないといけない場合があります。そういう場合は次のように同じ「pkg-configのID」と「プラットフォーム」に複数のパッケージ名を指定します。
Gem::Specification.new do |spec|
# ...
# We need to install multiple packages to use cairo with conda.
spec.requirements << "system: cairo: conda: cairo"
spec.requirements << "system: cairo: conda: expat"
spec.requirements << "system: cairo: conda: xorg-kbproto"
spec.requirements << "system: cairo: conda: xorg-libxau"
spec.requirements << "system: cairo: conda: xorg-libxext"
spec.requirements << "system: cairo: conda: xorg-libxrender"
spec.requirements << "system: cairo: conda: xorg-renderproto"
spec.requirements << "system: cairo: conda: xorg-xextproto"
spec.requirements << "system: cairo: conda: xorg-xproto"
spec.requirements << "system: cairo: conda: zlib"
# ...
end
HTTPS経由でパッケージをインストール
プラットフォームが提供するパッケージリポジトリーでは依存ライブラリーのパッケージが提供されていないことがあります。そのようなときのためにHTTPS経由でパッケージをダウンロードしてインストールできるようになっています。
これは、主にGroongaやApache Arrow向けの機能です。GroongaやApache Arrowではパッケージリポジトリーを登録するためのパッケージを提供しているので、それをHTTPS経由でインストールして、その後に、普通にパッケージをインストールします。
プレースホルダーという便利機能もありますが、それの説明は面倒なのでここではしません。↓のコメントを参考にしてください。
Gem::Specification.new do |spec|
# ...
# Install Groonga's APT repository for libgroonga-dev on Debian
# family platforms.
#
# %{distribution} and %{code_name} are placeholders.
#
# On Debian GNU/Linux bookworm:
# https://packages.groonga.org/%{distribution}/groonga-apt-source-latest-%{code_name}.deb ->
# https://packages.groonga.org/debian/groonga-apt-source-latest-bookworm.deb
#
# On Ubuntu 24.04:
# https://packages.groonga.org/%{distribution}/groonga-apt-source-latest-%{code_name}.deb ->
# https://packages.groonga.org/ubuntu/groonga-apt-source-latest-noble.deb
spec.requirements << "system: groonga: debian: https://packages.groonga.org/%{distribution}/groonga-apt-source-latest-%{code_name}.deb"
# Install libgroonga-dev from the registered repository.
spec.requirements << "system: groonga: debian: libgroonga-dev"
# Install 2 repositories for pkgconfig(groonga) package on RHEL
# family plaforms:
# 1. Apache Arrow: https://apache.jfrog.io/artifactory/arrow/almalinux/%{major_version}/apache-arrow-release-latest.rpm
# 2. Groonga: https://packages.groonga.org/almalinux/%{major_version}/groonga-release-latest.noarch.rpm
#
# %{major_version} is placeholder.
#
# On AlmaLinux 8:
# https://apache.jfrog.io/artifactory/arrow/almalinux/%{major_version}/apache-arrow-release-latest.rpm ->
# https://apache.jfrog.io/artifactory/arrow/almalinux/8/apache-arrow-release-latest.rpm
#
# https://packages.groonga.org/almalinux/%{major_version}/groonga-release-latest.noarch.rpm ->
# https://packages.groonga.org/almalinux/8/groonga-release-latest.noarch.rpm
#
# On AlmaLinux 9:
# https://apache.jfrog.io/artifactory/arrow/almalinux/%{major_version}/apache-arrow-release-latest.rpm ->
# https://apache.jfrog.io/artifactory/arrow/almalinux/9/apache-arrow-release-latest.rpm
#
# https://packages.groonga.org/almalinux/%{major_version}/groonga-release-latest.noarch.rpm ->
# https://packages.groonga.org/almalinux/9/groonga-release-latest.noarch.rpm
spec.requirements << "system: groonga: rhel: https://apache.jfrog.io/artifactory/arrow/almalinux/%{major_version}/apache-arrow-release-latest.rpm"
spec.requirements << "system: groonga: rhel: https://packages.groonga.org/almalinux/%{major_version}/groonga-release-latest.noarch.rpm"
# Install pkgconfig(groonga) from the registered repositories.
spec.requirements << "system: groonga: rhel: pkgconfig(groonga)"
# ...
end
Opt-out
ユーザーは環境変数や~/.gemrc
で無効にすることができます。opt-inの方がよさそうな話もあって気持ちもわかるんですが、デフォルトでgem install
で失敗するのってユーザー体験としてどうなの?という気持ちがあって、今のところはopt-inにはなっていません。
RubyGemsのプラグインにしたことでインタラクティブにインストールしていい?と聞けるはずなので、opt-out + インタラクティブなUIでいい感じにできないかな?という気持ちではいます。
課題
という感じで動くやつを作って実験しています。
実験した結果、すでにいくつか問題が見つかっているので、どうしよっかなぁという気持ちでいます。まぁ、やるだけなんですが、やる気が。。。
たとえば、Bundlerと一緒に使うと前述のRubyGemsでの挙動と同じ問題がありました。Bundlerにパッチを送らないとなぁとは思うんですが、やる気がでずにまだ手つかずです。
他には、こいつは実行時には必要なくてインストール時に必要なんだけど、今のRubyGemsだと実行時の依存情報と開発時の依存情報しかなくて、インストール時のみ必要ということを表現できないんだよなぁとかがあります。
まとめ
この5-6年でちょいちょいやる気になってちょっとずつ進めてきたfat gemのやつを紹介しました。今年こそ、fat gem、やめない?