Rubygemsのplatform の文字列はどこで定義されているか？ 〜fatgemを作る人のために〜

:::note info
ネット上の情報が少ないので調べた内容をまとめていますが、この記事の信頼性はそこまで高くありません。より詳しい情報をご存知の方はコメント欄でコメントして頂けると幸いです。
:::

Rubyでしばしば見られる `x86_64-linux` `arm64-darwin` `x64-mingw32` といった記法がどこで定義されているのか調べてみました。まえがきが長いので、結論を知りたい人のために結論を書くと、

- IT業界標準的な cpu-os 記法があるわけではなくRubyの書き方である。
- `rubygems/platform.rb` で定義されていて特に一覧表があるわけではない。
- このplatform記法の概要は `gem help platform` で読むことができる


です。さらに詳しく読みたい方は「platform の文字列はどこで定義されているか？」まで飛ばしてください。

## はじめに

### fatgemとは

RubyのパッケージのことをGemと言います。Gemは通常はRubyのコードが入っていることが多いですが、中にはC言語など他言語のソースコードが入っていたり、バイナリファイルが含まれている場合もあります。多くの場合、それらのバイナリファイルは、共有ライブラリです。共有ライブラリはRubyのC拡張ライブラリの場合もありますし、そうではなく、OSSで配布されている共有ライブラリの場合もあります。今後は wasm が含まれるGemも出てくるでしょう。こういったバイナリファイルが含まれているRubyGemsのことを、fatgem と呼びます。

### fatgem の作成は手間がかかる

fatgemsは、OSやCPUのアーキテクチャごとに異なる形式のバイナリファイルを用意する必要があります。（ただし、主要なプラットフォームのバイナリをすべて一つのgemに入れて配布するということも可能です。）

そのためGemの作成は手間がかかります。例えば、Linuxと、macOSと、Windows に向けてバイナリファイルを用意することを考えましょう。Linuxで、`x86_64` Raspberry Pi向けに `arm` 。macOS向けに `x86_64` および `aarch64` あるいは `universal` 。Windowsでも `x64` `x86` などを用意する必要があるかもしれません。さらに大規模に使われているプロジェクトであれば、`jruby` も考慮する必要もあるかもしれません。 `arm` といっても本当はもっと多くの分類があるしょう。

プラットフォームが多いと、動作確認が大変になります。また共有ライブラリを自分で用意する場合は、コンパイルも大変です。GitHub Actionsなどで主要なプラットフォームは賄えるかも知れませんが、実際に実機で動かしてみないとわからないこともあるでしょう。

### 避けられる場合はfatgemを避ける

ここは、いろいろな考え方があるところですが、fatgemよりも、パッケージマネージャーで配布されている共有ライブラリを利用していこうという考え方があり、基本的には私も賛同します。可能であれば、C拡張ライブラリ以外のバイナリファイルをGemの中に配置するのはやめて、OSの標準的な場所に置くべきでしょう。また、OSSの作者としては、APTやhomebrewといった便利なパッケージマネージャーにただ乗りするばかりではなく、コントリビューションしていくという姿勢も必要かもしれません。下のリンクは、Rubykaigi 2020 の kou さんの発表のスライドです。

https://slide.rabbit-shocker.org/authors/kou/rubykaigi-takeout-2020/

ライブラリのパスはしばしば問題になりますが、`pkg-config` を利用したり（Rubyにも同名のgemがある）、`gem install` コマンドのオプションを指定することでRubyにライブラリの場所を教えることができます。たとえば、Ubuntuでruby-tkをインストールする場合は、次のようにします。

```sh
gem install tk -- --with-tcltkversion=8.6 \
--with-tcl-lib=/usr/lib/x86_64-linux-gnu \
--with-tk-lib=/usr/lib/x86_64-linux-gnu \
--with-tcl-include=/usr/include/tcl8.6 \
--with-tk-include=/usr/include/tcl8.6 \
--enable-pthread
```

### それでもfatgemを作りたくなる時もある

しかし、現在でも一部のGemのメンテナは積極的にfatgemを配布しています。特に有名なエンジニアの方が作っている名前のあるGemでそういったものが見られます。その理由は、fatgemはユーザー側からみたインストールが簡単で失敗することが少ないという点が挙げられるでしょう。また、最近ではRust言語で書かれたGemも少しずつ登場してきました。2023年現在では、OSにRust処理系が標準で搭載されていることは期待できないので、最初からfatgemで配ってしまおうという発想になるのはわかる気がします。

fatgem で配布されているGemの例として nokogiri を見てみましょう。

https://rubygems.org/gems/nokogiri

- 無印
- x86_64-linux
- x86-linux
- arm-linux
- aarch64-linux
- x86_64-darwin
- arm64-darwin
- x86-mingw32
- x64-mingw32
- x64-mingw-ucrt
- java

このように、多岐に渡るプラットフォームに向けて、個別のGemが提供されていることがわかります。そして、platform が cpuのアーキテクチャ-OS という形式で表現されていることがわかります。それでは、このplatformを示す文字列はどこに定義されているのでしょうか？

## platform の文字列はどこで定義されているか？

聞いた話によりますと、このような `CPUアーキテクチャ-OS` という記法は、かならずしもIT業界に統一的な標準があるわけではなくて、あくまでRubyにおける記法になるそうです。この Rubygems platforms は `gem help platform` コマンドで閲覧することができます。

### gemコマンドの説明を見る

`gem help platform`

```
RubyGems platforms are composed of three parts, a CPU, an OS, and a
version.  These values are taken from values in rbconfig.rb.  You can view
your current platform by running `gem environment`.

RubyGems matches platforms as follows:

  * The CPU must match exactly unless one of the platforms has
    "universal" as the CPU or the local CPU starts with "arm" and the gem's
    CPU is exactly "arm" (for gems that support generic ARM architecture).
  * The OS must match exactly.
  * The versions must match exactly unless one of the versions is nil.

For commands that install, uninstall and list gems, you can override what
RubyGems thinks your platform is with the --platform option.  The platform
you pass must match "#{cpu}-#{os}" or "#{cpu}-#{os}-#{version}".  On mswin
platforms, the version is the compiler version, not the OS version.  (Ruby
compiled with VC6 uses "60" as the compiler version, VC8 uses "80".)

For the ARM architecture, gems with a platform of "arm-linux" should run on a
reasonable set of ARM CPUs and not depend on instructions present on a limited
subset of the architecture.  For example, the binary should run on platforms
armv5, armv6hf, armv6l, armv7, etc.  If you use the "arm-linux" platform
please test your gem on a variety of ARM hardware before release to ensure it
functions correctly.

Example platforms:

  x86-freebsd        # Any FreeBSD version on an x86 CPU
  universal-darwin-8 # Darwin 8 only gems that run on any CPU
  x86-mswin32-80     # Windows gems compiled with VC8
  armv7-linux        # Gem complied for an ARMv7 CPU running linux
  arm-linux          # Gem compiled for any ARM CPU running linux

When building platform gems, set the platform in the gem specification to
Gem::Platform::CURRENT.  This will correctly mark the gem with your ruby's
platform.
```

ChatGPTに日本語に翻訳してもらいましょう。

> RubyGemsのプラットフォームは、CPU、OS、バージョンの3つの部分で構成されています。これらの値は、rbconfig.rbの値から取得されます。現在のプラットフォームは、`gem environment`を実行することで確認することができます。
>
> RubyGemsは以下のようにプラットフォームをマッチさせます：
>
>   * CPUは、プラットフォームの一方が「universal」というCPUであるか、ローカルのCPUが「arm」から始まり、gemのCPUがまさに「arm」である場合（汎用的なARMアーキテクチャをサポートするgemのため）を除き、完全に一致する必要があります。
>   * OSは完全に一致する必要があります。
>   * バージョンは、いずれかのバージョンがnilである場合を除き、完全に一致する必要があります。
>
> gemのインストール、アンインストール、リスト表示のコマンドでは、--platformオプションを使用して、RubyGemsがあなたのプラットフォームだと考えるものを上書きすることができます。渡すプラットフォームは、「#{cpu}-#{os}」または「#{cpu}-#{os}-#{version}」と一致する必要があります。mswinのプラットフォームでは、バージョンはOSのバージョンではなく、コンパイラのバージョンです。（例えば、VC6でコンパイルされたRubyはコンパイラのバージョンとして「60」を、VC8は「80」を使用します。）
>
> ARMアーキテクチャについては、「arm-linux」のプラットフォームを持つgemは、合理的なセットのARM CPU上で動作し、アーキテクチャの限定的な一部に存在する命令に依存しないべきです。例えば、バイナリは、armv5、armv6hf、armv6l、armv7などのプラットフォームで動作するべきです。「arm-linux」プラットフォームを使用する場合は、リリース前にさまざまなARMハードウェアでgemをテストし、正しく機能することを確認してください。
>
> プラットフォームの例：
>
>   x86-freebsd        # x86 CPU上の任意のFreeBSDバージョン
>   universal-darwin-8 # 任意のCPU上で動作するDarwin 8専用のgem
>   x86-mswin32-80     # VC8でコンパイルされたWindows gem
>   armv7-linux        # Linuxを実行するARMv7 CPU用にコンパイルされたgem
>   arm-linux          # Linuxを実行する任意のARM CPU用にコンパイルされたgem
>
> プラットフォームgemをビルドする際は、gemの仕様のプラットフォームをGem::Platform::CURRENTに設定します。これにより、gemがあなたのrubyのプラットフォームで正しくマークされるようになります。

### rubygems/platform.rb

そして、実際の内容はソースコード上に定義されています。

https://github.com/rubygems/rubygems/blob/master/lib/rubygems/platform.rb

OSは次のような種類のものが想定されているようですね。

```ruby
      @os, @version = case os
                      when /aix(\d+)?/ then             ["aix",       $1]
                      when /cygwin/ then                ["cygwin",    nil]
                      when /darwin(\d+)?/ then          ["darwin",    $1]
                      when /^macruby$/ then             ["macruby",   nil]
                      when /freebsd(\d+)?/ then         ["freebsd",   $1]
                      when /^java$/, /^jruby$/ then     ["java",      nil]
                      when /^java([\d.]*)/ then         ["java",      $1]
                      when /^dalvik(\d+)?$/ then        ["dalvik",    $1]
                      when /^dotnet$/ then              ["dotnet",    nil]
                      when /^dotnet([\d.]*)/ then       ["dotnet",    $1]
                      when /linux-?(\w+)?/ then         ["linux",     $1]
                      when /mingw32/ then               ["mingw32",   nil]
                      when /mingw-?(\w+)?/ then         ["mingw",     $1]
                      when /(mswin\d+)(\_(\d+))?/ then
                        os = $1
                        version = $3
                        @cpu = "x86" if @cpu.nil? && os =~ /32$/
                        [os, version]
                      when /netbsdelf/ then             ["netbsdelf", nil]
                      when /openbsd(\d+\.\d+)?/ then    ["openbsd",   $1]
                      when /solaris(\d+\.\d+)?/ then    ["solaris",   $1]
                      # test
                      when /^(\w+_platform)(\d+)?/ then [$1,          $2]
                      else ["unknown", nil]
      end
```

また、例外的に、次のような記法の存在も考慮されているようです。

```ruby
  def =~(other)
    case other
    when Gem::Platform then # nop
    when String then
      # This data is from http://gems.rubyforge.org/gems/yaml on 19 Aug 2007
      other = case other
              when /^i686-darwin(\d)/     then ["x86",       "darwin",  $1]
              when /^i\d86-linux/         then ["x86",       "linux",   nil]
              when "java", "jruby"        then [nil,         "java",    nil]
              when /^dalvik(\d+)?$/       then [nil,         "dalvik",  $1]
              when /dotnet(\-(\d+\.\d+))?/ then ["universal","dotnet",  $2]
              when /mswin32(\_(\d+))?/    then ["x86",       "mswin32", $2]
              when /mswin64(\_(\d+))?/    then ["x64",       "mswin64", $2]
              when "powerpc-darwin"       then ["powerpc",   "darwin",  nil]
              when /powerpc-darwin(\d)/   then ["powerpc",   "darwin",  $1]
              when /sparc-solaris2.8/     then ["sparc",     "solaris", "2.8"]
              when /universal-darwin(\d)/ then ["universal", "darwin",  $1]
              else other
      end

      other = Gem::Platform.new other
    else
      return nil
    end

    self === other
  end
```

## まとめ

以上、あまりまとまりはありませんが、冒頭に述べましたように

- IT業界標準的な cpu-os 記法があるわけではなくRubyの書き方である。
- `rubygems/platform.rb` で定義されていて特に一覧表があるわけではない。
- このplatform記法の概要は `gem help platform` で読むことができる

ということになります。fatgemを作成している人はRubyユーザーでも非常に少ないと思うので、これだけの情報でもググってもなかなか出てきません。もっと詳しい情報をお持ちの方はぜひコメント欄で教えて頂けると助かります。

この記事は以上です。