Ruby フルタイムコミッタの仕事報告 2023年Q4

こんにちは hsbt です。RubyConf Taiwan 2023 の登壇から帰国して後はバルダーズ・ゲート3をプレイして年を越すのみ...! とは行かず、Ruby 3.3.0 のリリースに向けて最後の準備作業をしている真っ最中です。

さて、今回はANDPAD Advent Calendar 2023の20日目として、今年1年私がプログラミング言語 Ruby の開発のために手を動かしてきた内容の中からテックブログやカンファレンスで発表していない内容をまとめてご紹介します。

default gems から bundled gems への変更の目的

私は数年前から Ruby に元々添付されていたライブラリ(標準添付ライブラリ)を default gems と呼ばれる gem としてもインストールやアップデートができる状態にし、そこからさらに bundled gems と呼ばれる Ruby 本体の開発プロセスからは独立した rails や rubocop のような通常の gem に近い状態にするという活動を続けています。

何故このような活動をしているかというと、標準添付ライブラリのセキュリティアップデートをより容易にしたり、次のバージョンの Ruby で入るであろうライブラリの新機能などをいち早く試すことを可能にしたりするなどの提供側の理由もありますが、default gems から bundled gems にしていく過程で開発が行われる場所を GitHub に移し、Ruby コミッタ(Ruby 本体にコミットができる人)でなくてもライブラリの開発に参加できるようにするという目的もあります。

ここ数年の結果として、より多くの人がプログラミング言語 Ruby のインタプリタに近い領域で活動できるようになりました。今回はより多くの人が Ruby の開発に参加できるようにするために私が行った作業の中からいくつかご紹介します。

すべての default gems にバージョンを示す定数を入れた

Ruby の標準添付ライブラリはこれまでは「Ruby 3.0 の etc ライブラリ」というような表現しかありませんでした。default gems として gem としてリリースするからには、Ruby のバージョンとは別途ライブラリごとにバージョンを持つ必要があります。初期段階として、 default gems として作成する際に、バージョンを振り直すことで etc-0.1.0 というような馴染みのあるバージョンと、そのバージョンにおける変更点を参照可能となりました。

しかし、そのバージョンはあくまでも gem としてのリリースバージョンでしかなく、Ruby のプロセスからは参照できません。そこで私がせっせと Ruby のプロセスからも参照できるようにバージョンを示す定数をすべての default gems に導入しました。

上記は Ruby と C 拡張それぞれのライブラリの例です。実際には20前後のライブラリすべてについて Etc::VERSION というように自分が使っている default gems のバージョンはいくつか、Ruby から参照できるようにしました。

Ruby 本体のテストライブラリを test-unit の plugin として実行できるようにした

default gems の開発は GitHub を upstream として利用しています。そのため、CI やテストは GitHub Actions と何かしらの Ruby のテストライブラリで実行できる必要があります。

これまで、Ruby 本体では、Ruby 本体用の test-unit 風の独自のライブラリが用いられており、default gems として標準添付ライブラリから分離したものは test-unit gem を用いるか、Ruby 本体のテストライブラリをハードコピーしてテストしていました。Ruby 本体のテストライブラリは現在も新しい機能、例えば Ractor 向けの assertion などが追加されるなど、開発が完了しているわけではないので、本体のテストライブラリを更新したら、利用している default gems すべてへコピーしなおす必要があります。

この状況はとても保守性が良いとは言えない状況なので、以下の作業を段階的に進めました。

  1. Ruby 本体のテストライブラリの assertion などのインタフェースを test-unit と同じにする
  2. Ruby 本体にのみ存在する assertion を CoreAssertion というモジュールに切り出して、test-unit と Ruby 本体のテストライブラリそれぞれで include して使えるようにする
  3. Update test libraries from ruby/ruby 2023-03-24 by hsbt · Pull Request #25 · ruby/etc
  4. CoreAssertion を test-unit-ruby-core という gem としてリリースすることで、必要な default gems では test-unit + test-unit-ruby-core を gem install (bundle install) して使う。
  5. Use test-unit-ruby-core gem by hsbt · Pull Request #31 · ruby/etc

上記の作業により、Ruby 本体のテストライブラリを継続的に開発しつつ、test-unit には存在しない機能を提供している部分については test-unit-ruby-core としてリリースするだけで、全ての default gems に適用できるようになりました。最高に便利です。

re-using workflow を用いてテスト対象のバージョンを自動で導出するようにした

最後に、default gems は gem としてリリースはされているものの、従来の Ruby の標準添付ライブラリ同様に Ruby に同梱され続けています。つまり、Ruby 本体のリポジトリ(ruby/ruby)にも存在しつつ、default gems としてのリポジトリ(ruby/etc など)それぞれの環境でテストを実行しています。default gems は gem としての機能も有するので、Ruby 本体のリポジトリのように最新の開発版バージョンの Ruby でだけ動けばいいというわけではなく、安定版バージョンから開発版バージョンの3または4バージョンで動く必要があります。

また、default gems によっては json などのように Ruby 2.3 などのバージョンも引き続きサポートしているものもあります。これは積極的にサポート対象のバージョンを減らす必要はない、ということでサポートを続けていることを意味しますが、このような状況で GitHub Actions の matrix を手で更新するのは煩雑です。しかも、Ruby は毎年新しいバージョンがリリースされるので、毎年30以上の default gems のリポジトリに 3.3 などのテスト対象のバージョンを手で追加するのは良い話とはいえません。

今回、この問題を解決するために、GitHub Actions の re-using workflow という機能を用いて、ruby/setup-ruby にわたすための Ruby のバージョンを自動で導出するようにしました。

name: ubuntu

on: [push, pull_request]

jobs:
  ruby-versions:
    uses: ruby/actions/.github/workflows/ruby_versions.yml@master
    with:
      engine: cruby
      min_version: 2.5
  test:
    needs: ruby-versions
    name: build (${{ matrix.ruby }} / ${{ matrix.os }})
    strategy:
      matrix:
        ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }}
        os: [ ubuntu-latest, macos-latest, windows-latest ]
    runs-on: ${{ matrix.os }}
    steps:
    - uses: actions/checkout@v4
    - name: Set up Ruby
      uses: ruby/setup-ruby@v1
      with:
        ruby-version: ${{ matrix.ruby }}
    - name: Install dependencies
      run: bundle install
    - name: Run test
      run: rake test

使い方としては上記のような yaml を書くことで、手で修正しなくても自動で指定したレンジ、例では Ruby 2.5 以降のすべての Ruby のバージョンをテストするようになります。また、JRuby や TruffleRuby を対象とする機能もあり、その場合は engine として cruby-jruby などと指定することで利用可能となり、ライブラリ開発者には便利と思います。実装については以下のような内容となってます。

name: ruby_versions

on:
  workflow_call:
    inputs:
      engine:
        description: "The type of Ruby engine"
        default: "all"
        type: string
      versions:
        description: "Additional Ruby versions"
        default: "[]"
        type: string
      min_version:
        description: "Minimum Ruby version"
        type: number
    outputs:
      versions:
        description: "Ruby versions"
        value: ${{ jobs.ruby_versions.outputs.versions }}
      latest:
        description: "The latest Ruby release version"
        value: ${{ jobs.ruby_versions.outputs.latest }}

jobs:
  ruby_versions:
    name: Generate Ruby versions
    runs-on: ubuntu-latest
    outputs:
      versions: ${{ steps.versions.outputs.versions }}
      latest: ${{ steps.versions.outputs.latest }}
    steps:
      - id: versions
        run: |
          #! ruby
          require 'json'
          require 'open-uri'
          versions = JSON.parse(URI(ENV['CI_VERSIONS']).read)
          min = versions.min.to_f
          if (min_version = ENV['MIN_VERSION'].to_f) > 1.8
            versions += min_version.step(by: 0.1, to: min).
                        map {|v| sprintf("%.1f",v)} - %w[2.8 2.9]
          end
          versions.concat(JSON.parse(ENV['VERSIONS'])).tap(&:uniq!).tap(&:sort!)
          output = [
            "versions=#{versions.to_json}\n",
            "latest=#{versions.grep(/^\d/).last}\n",
          ].join("")
          File.open(ENV['GITHUB_OUTPUT'], "a") {|f| f.print output}
          print output
        shell: /usr/bin/ruby {0}
        env:
          CI_VERSIONS: https://cache.ruby-lang.org/pub/misc/ci_versions/${{ inputs.engine }}.json
          VERSIONS: ${{ inputs.versions }}
          MIN_VERSION: ${{ inputs.min_version }}

github.com

そろそろ、この re-using workflow 自体のテストも用意しないとだめなのでは...という気持ちになってきていますが、Actions の実行をテストするというのは中々難しく、この re-using workflow を Actions から呼び出してテストを同じリポジトリ実行しているという事例は見たことがありますが、どうしたものか...というのが現状です。良い案がある方は教えていただけると幸いです。

まとめ

以上、default gems と bundled gems の開発に関わる環境整備という観点で hsbt が進めて来た作業についてご紹介しました。少しずつではありますが、標準添付ライブラリを default gems 、そして bundled gems にすることで net-imap や irb などのように新しいメンテナが誕生して、その方に今後のメンテナンスをおまかせするという体制をつくる事ができており、Ruby の継続的な発展に寄与できていると感じています。来週は待ちに待った Ruby 3.3 のリリース日ですね。皆さんお楽しみに!

アンドパッドでは、「幸せを築く人を、幸せに。」というミッションの実現のため、一緒に働く仲間を大募集しています。 会社や事業、開発チームにご興味を持たれた方は、下記のサイトをぜひご覧ください。

https://engineer.andpad.co.jp/