こんにちは 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 に導入しました。
- Pathname ライブラリの例: Expose Pathname::VERSION by hsbt · Pull Request #30 · ruby/pathname
- Syslog ライブラリ(C拡張)の例: Expose Syslog::VERSION by hsbt · Pull Request #5 · ruby/syslog
上記は 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 すべてへコピーしなおす必要があります。
この状況はとても保守性が良いとは言えない状況なので、以下の作業を段階的に進めました。
- Ruby 本体のテストライブラリの assertion などのインタフェースを test-unit と同じにする
- Ruby 本体にのみ存在する assertion を
CoreAssertion
というモジュールに切り出して、test-unit と Ruby 本体のテストライブラリそれぞれでinclude
して使えるようにする - Update test libraries from ruby/ruby 2023-03-24 by hsbt · Pull Request #25 · ruby/etc 等
- CoreAssertion を test-unit-ruby-core という gem としてリリースすることで、必要な default gems では test-unit + test-unit-ruby-core を gem install (bundle install) して使う。
- 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 }}
そろそろ、この re-using workflow 自体のテストも用意しないとだめなのでは...という気持ちになってきていますが、Actions の実行をテストするというのは中々難しく、この re-using workflow を Actions から呼び出してテストを同じリポジトリ実行しているという事例は見たことがありますが、どうしたものか...というのが現状です。良い案がある方は教えていただけると幸いです。
まとめ
以上、default gems と bundled gems の開発に関わる環境整備という観点で hsbt が進めて来た作業についてご紹介しました。少しずつではありますが、標準添付ライブラリを default gems 、そして bundled gems にすることで net-imap や irb などのように新しいメンテナが誕生して、その方に今後のメンテナンスをおまかせするという体制をつくる事ができており、Ruby の継続的な発展に寄与できていると感じています。来週は待ちに待った Ruby 3.3 のリリース日ですね。皆さんお楽しみに!
アンドパッドでは、「幸せを築く人を、幸せに。」というミッションの実現のため、一緒に働く仲間を大募集しています。 会社や事業、開発チームにご興味を持たれた方は、下記のサイトをぜひご覧ください。