先日Ruby 3に正式に対応したfastlane v2.183.0を無事リリースすることができた。昨年末にCore Contributorになって勢いでRuby 3サポートやるぞと宣言して、約半年弱かかったがようやく達成することが出来た。 https://github.com/fastlane/fastlane/releases/tag/2.183.0

今回の記事ではどのようにしてRuby 3サポートを進めていったについて知見を紹介したいと思う。ここ最近、賃貸の家の退去と日本への一時帰国などで少々忙しかったためリリースしてから1ヶ月弱後と少し遅れてしまった。一般的に、Ruby 3サポートは、他の小・中規模のgemではそこまで難しい作業ではないことが多いし、もしくはRuby 2.7がリリースされたタイミングでwarningsに対応ちょっとやって終わりというところがほとんどだと思う。しかしfastlaneというgemは規模も大きいし、依存しているgemの数もかなりあったため作業を進めるのが少々困難だった。

Ruby 3.0サポートそのものの価値としては、fastlane自体がモバイルアプリ開発者向けにDSLとして使われるのがメインと考えるとfastlaneがRuby3.0で受ける恩恵はそこまで多くはない。だけど、ソフトウェアは時間と共に劣化していく(そのうち主流の環境で実行できなくなる)ので、常に最新の環境に対応しておくのはソフトウェアの寿命を伸ばすという点でとても意味があるし、実際に誰か手を動かしてやらないといけない。今回はその誰かが見当たらなかったので自分から手をあげてプロジェクトを1人でリードしてみたという感じ。自分はもともとWebエンジニアとしてもRubyを書いていてRuby言語そのものが好きだし、モバイルアプリの開発もそこそこやっていて仕事でfastlaneも触っていたので、fastlaneへのcontributionはちょうど自分にマッチしていると思った。何より、今所属している会社にRubyのコミッターの方々がいるので最悪Ruby 3対応で困ったら質問できるという安心感・福利厚生があったのが良かった。

段階的・継続的な修正のリリース

まず初めに決めていたのが、いわゆる作業用ブランチを切ってマイグレーションを続けて日々リベースをして完成したらビックバンリリースみたいなリスクの高いやり方はしないということ。fastlaneは今でこそ新機能が毎日追加されるということはないものの、依然としてかなりのPRが送られてきてマージし続けているし大体週1回はリリースしているので、一つの作業ブランチを作ろうものなら日々コンフリクトが発生してしまうので避けたかった。

そこで、Ruby 3.0サポートのための計画をGitHubのissueにまとめて、TODOリストを書き、そこから順番に実行して少しづつリリースしていった。またこのissueを早めに用意することによって気軽にRuby 3.0環境でfastlaneを試したユーザーたちが無駄にissueを無限に立て続けるのを防ぐ効果を期待していた。 https://github.com/fastlane/fastlane/issues/17931

その結果、最終的なリリースの2.183.0はgoogle-api-client gemのバージョンをあげてRuby 2.4のサポートを切りつつ3.0のサポートを正式に行う修正+漏れていたURI.escapeの修正ぐらいしか入ってなくてほとんどの修正はそれよりも前のバージョンでリリースしていた。2.183.0では修正漏れが多少あったものの大きな問題は発生せずにRuby 3.0サポートを達成することが出来た。

要件の洗い出し

最初にissueを書く際に、Ruby 3サポートを達成するために何が必要なのかを一通り洗い出す必要があった。Ruby 3での破壊的な変更は公式のブログなどを読めば明らかで、これをやれば良いのだなということは真っ先にわかったし、実際にRuby 3環境でgem installしてみたときにそもそも最新のfastlaneがインストール出来ないことなどを元にやらなきゃいけないことをissueにまとめていった。一番大きな変更はキーワード引数の分離というもので詳細は以下のブログを読むのが良いので割愛する。出現箇所が分かってさえいれば修正は割と簡単ではあるが、Rubyが動的型付け言語なため実行するまでエラーが発生するか分からないという問題はある。

キーワード引数の分離について https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/

またこのブログもRubyコミッター自身による解説でとても勉強になる https://techlife.cookpad.com/entry/2020/12/25/155741

この段階で全ての修正箇所が明らかになるわけではないので必要に応じてissueを更新したが、大まかな計画には最初に立てた通りで進行していった。

Ruby 2.7サポート

要件を洗い出してる最中にそもそも現状のfastlaneの対応Rubyバージョンもあやふやだということが分かったので、Ruby 2.7に対応しているというのをまず最初に明確にする必要があった。具体的には利用しているCircleCIでRuby 2.7に対するテストを実行できるようにした。

Ruby 2.7でテストを実際に実行してみると、非互換な変更があるわけでもないのに一部のテストが落ちたりしてめちゃくちゃ細かい対応をせざるを得なかった。 https://github.com/fastlane/fastlane/pull/17861

具体的にはSyntax errorが起きた時のエラーメッセージをテスト中に確認している箇所があって、そこのformatがちょっとだけ変わったことで落ちたり、Shellwords.escapeの挙動にあったバグが治ったことで、テスト中の期待値がマッチしなくなったので正しい値に直したり、巨大プロジェクトならではの大変さがあった。

ユニットテストでdeprecation warningsの収集

Ruby 2.7に正式に対応することでRuby 2.7が出力するdeprecationのwarningsをCI上で確認することが出来るようになった。実行するまで分からないエラー・警告ならばユニットテストを実行していけばいい。Ruby 2.7.3ではデフォルトでdeprecationは出力されないのでRUBYOPT=-W:deprecatedを設定してRubyを実行する必要がある。RSpecの実行時にこの環境変数を設定すればログにwarningsが残るのでテストでカバーされている範囲で修正が必要な箇所を洗い出し、一気に修正した。

https://github.com/fastlane/fastlane/pull/18021 https://github.com/fastlane/fastlane/pull/18395

また、CircleCI上でwarningsがこれ以上増えないようにrspecの実行後にチェックする仕組みを入れたことでこれ以上のdeprecation warningsが増えないようになった。fastlaneのCIはfastlaneのコマンドからrakeを経由してrspecを実行しているためwarningsを集めるための処理がちょっと複雑になってしまった。

  • Rubyのwarningsはstderrに出力される
  • stderrはCircleCI上で引き続きログで閲覧したいがファイルにも書き出したい

という要件を満たすためにstderrをstdoutにリダイレクトしてからteeを利用してwarningsを集めるという感じになった。

task(:test_all) do
  formatter = "--format progress"
  formatter += " -r rspec_junit_formatter --format RspecJunitFormatter -o #{ENV['CIRCLE_TEST_REPORTS']}/rspec/fastlane-junit-results.xml" if ENV["CIRCLE_TEST_REPORTS"]
  command = "rspec --pattern ./**/*_spec.rb #{formatter}"

  # To move Ruby 3.0 or next major version migration going forward, we want to keep monitoring deprecation warnings
  if Gem.win_platform?
    # Windows would not work with /bin/bash so skip collecting warnings
    sh(command)
  else
    # Mix stderr into stdout to let handle `tee` it and then collect warnings by filtering stdout out
    command += " 2>&1 | tee >(grep 'warning:' > #{File.join(ENV['CIRCLE_TEST_REPORTS'], 'ruby_warnings.txt')})" if ENV["CIRCLE_TEST_REPORTS"]
    # tee >(...) occurs syntax error with `sh` helper which uses /bin/sh by default.
    sh("/bin/bash -o pipefail -c \"#{command}\"")
  end
end

最後に集めたwarningsをgrepでチェックして一つでもwarningsが残っていたらエラーとなるようにした。grep -v ".bundle"はライブラリからのwarningsを一時的に無視するためのもの。

! cat ~/test-reports/ruby_warnings.txt | grep -v ".bundle" | grep -E "warning:\s.*(deprecated).*$" && echo "No deprecation message found."

Rubocopのアップデート

fastlaneでは利用していたRubocopのバージョンがv0.49.1という非常に古いバージョンで止まっていた。もちろんこのままではRuby 3に対応してないのでバージョンを上げる必要がある。推測するにバージョンを上げた時に新たなルールに対応するのが大変で固定されたままになってしまったのだと思うけど、実は.rubocop_todo.ymlと言うファイルを自動生成することで、新規に追加ルールの影響が大きくて有効にできない物を一時的に無効に出来る。これを利用してgemのバージョンをガッと最新まで上げた。

バージョンを上げるためにも、ルール名の変更された箇所を直したり、fastlane内に独自に実装されたルールないで利用しているAPIを最新のものに合わせて修正したりした。 https://github.com/fastlane/fastlane/pull/18564

ライブラリのアップデート

fastlaneの依存の中では以下のgemがRuby 3の対応が必要だった。

  • fakefs
  • rspec
  • slack-notifier
  • commander-fastlane
  • highline
  • google-api-client

fakefsはテスト用のライブラリで一瞬で最新のバージョンにあげることができた。rspecはどうやら最初に計画した時にはキーワード引数の分離に依存したエラーがあったのだけど、いつの間にか該当箇所がなくなっていた。rspecは割とRuby 3でも動くけど正式に対応したバージョンはリリースされてない(と思う)。google-api-clientはRuby 3に対してfastlaneがインストールできなかった元凶で、fastlaneが固定していたバージョンではrequire_ruby_version = '~> 2.4'となっていて2.xまでのアップデートしか許されていなかった。highlineはfastlane自身も直接依存しているが、commanderも依存しているためcommanderを上げないと行けなかった。しかし、fastlaneではちょっとしたhelp用の表示を切り替えるためにforkしてrubygem.orgにpublishされたバージョンを利用しているため、forkを止めるか、forkしたバージョンにfork元の変更を取り込む必要があった。残りのslack-notifierは、メンテナンスがしばらく止まっていて復活する見込みが少ないので自前実装に切り替えたいという気持ちになった。

ということで依存をアップデートするだけで治ったものは少なくて残りのライブラリの扱いが重いタスクとして残った。

メンテされてないライブラリの置き換え

slack-notifier

slack-notifierはincoming webhookを利用してSlackへ投稿するためのgemで、そこまでgemに頼らないと実装できないものではないけど地味に便利な機能として、htmlやmarkdown形式のリンクが投稿内容に含まれていたときに、Slack方式のリンクのフォーマットに変更してくれるものだった。slack-notifierのコードを読み進めることでfastlaneでは投稿そのものの機能とそのフォーマット機能しか利用していないことが発覚したため、利用している機能のみを再実装したらたった56行で収まった。

https://github.com/fastlane/fastlane/blob/0361ca0fa0433b82169891f4f77ca809227ba9e4/fastlane/lib/fastlane/notification/slack.rb

ちなみにこの置き換え作業が終わり、差分がマージされた1週間後にslack-notifierの作者が数年ぶりに復活して急にRuby 3対応を始めて修正済みのものがリリースされている。k今後もSlack側でなんらかの仕様変更があった際に追従していくのも大変になるので依存を止めるのは正しい判断だと思う。

commander-fastlane + highline

commanderはCLIツールのオプションを扱うライブラリだが、fastlaneではhelp用の表示を少々カスタマイズするためにforkしていたらしい。以下の例で言うとCommands: (* default)のように"*“を使ってデフォルトのサブコマンドと言うのを示したいというものだった。

% bundle exec fastlane --help
[✔] 🚀
  fastlane

  CLI for 'fastlane' - The easiest way to automate beta deployments and releases for your iOS and Android apps

        Run using `fastlane [platform] [lane_name]`
        To pass values to the lanes use `fastlane [platform] [lane_name] key:value key2:value2`

  Commands: (* default)
    action                 Shows more information for a specific command
    actions                Lists all available fastlane actions
    add_plugin             Add a new plugin to your fastlane setup
    ...
    trigger              * Run a specific lane. Pass the lane name and optionally the platform first.

こちらもcommanderとfastlaneのコードを読み進めた結果、forkしなくても全く同じものが実現できることがわかった。実はcommander自身がこのhelpをカスタマイズするための機能を持っていると言うことが分かり、その方法に応じた再実装をして、無事既存の表示を壊さずにforkしたバージョンを止めることができた。ついでにhighlineのchangelogを読んだ結果、fastlane側での必要な変更はほぼ無くて同時にhighlineのメジャーバージョンも上げることができた。

https://github.com/fastlane/fastlane/pull/18599

google-api-clientのマイグレーション

最後にRuby 3に対してのインストールをブロックしていたgoogle-api-clientへの対応を行った。いざ調べてみるとgoogle-api-clientそのもの自体が2021年1月初旬のちょうど自分が最初に計画を立てていた前後にdeprecatedになっていた。今までは1つのgemで全てのサービスを含める超巨大なgem(75MBもあったらしい)だったのを、サービス単位で分割してリリースされるようになったらしい。ちょうどRubyのAWS SDKのv2からv3への移行のような感じで、コード自体に変更はないもののインストール単位がサービス毎になった。

各利用サービス毎に個別にgemをgempsecで指定するだけで特別な修正は必要なかった。これでfastlaneのrequired_ruby_versiongoogle-apis-xxx系が要求する2.5以上にな るように修正し、.rubocop.ymlTargetRubyVersionも2.5になった(これによりRuby2.5で導入されたAPIでの書き方が推奨され始めたのでauto-correctで再度修正)。 https://github.com/fastlane/fastlane/pull/18656

これでついにRuby 3対応バージョンが無事リリースされた🚀

Hotfixes

ここまでバグなしでリリースしてきたけど最後のgoogle-api-clientのマイグレーションでRuby 3対応が漏れている箇所が現れた。Android向けのsupplyと言うモジュールでキーワード引数の分離がらみでエラーとなっていたのだが、どうやらここはユニットテストでカバーしきれていなかったため修正が漏れていたし、たまにしか実行されない箇所だったので手元のテストでも見逃してしまっていた。

https://github.com/fastlane/fastlane/pull/18699 https://github.com/fastlane/fastlane/pull/18703

終わりに

fastlaneという比較的大規模なRubyのプロジェクトでのRuby 3対応した時の知見を紹介した。gemでの事例なのでRailsアプリケーションなどでのRuby 3への対応とはまた異なってくると思うが、rspecを利用してdeprecation warningsを集めてCIでチェックするという手法は移行の過渡期で普通に使えるテクニックではないかと思う。

初めて大規模なOSSで自分で何かプロジェクトを主導して改善していくのをやってみたけどとても楽しかった。こんな感じで今後も何かやっていきたい。今やりかけているのはfastlaneの起動の高速化というタスクなのだけど、そのためにはまずrspecで実行するテストの順番をランダムにしてもテストケースが全部成功するように直さないといけなくてこれがとてつもなくしんどくて挫折しかけている。 https://github.com/fastlane/fastlane/pull/18278