ククログ

株式会社クリアコード > ククログ > 2025年、fat gemをやめる

2025年、fat gemをやめる

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年にこのあたりに関心が高そうな次の人たちと相談しました。

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-pluginAnormal-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、やめない?