Gemfile の Dual Boot 方法

Gemfile 内で切り替える

こういう ENV で切り替えるやつとか、eval_gemfile を使うやつとか。

https://github.com/discourse/discourse/blob/v2.6.2/Gemfile#L9-L16

def rails_master?
  ENV["RAILS_MASTER"] == '1'
end

if rails_master?
  gem 'arel', git: 'https://github.com/rails/arel.git'
  gem 'rails', git: 'https://github.com/rails/rails.git'
else
  # NOTE: Until rubygems gives us optional dependencies we are stuck with this needing to be explicit
  # this allows us to include the bits of rails we use without pieces we do not.
  #
  # To issue a rails update bump the version number here
  gem 'actionmailer', '6.0.3.3'
  gem 'actionpack', '6.0.3.3'
  gem 'actionview', '6.0.3.3'
  gem 'activemodel', '6.0.3.3'
  gem 'activerecord', '6.0.3.3'
  gem 'activesupport', '6.0.3.3'
  gem 'railties', '6.0.3.3'
  gem 'sprockets-rails'
end

Gemfile.lock に差分が出るので、アプリケーション (Gemfile.lock をリポジトリに含める) の場合はイマイチだと思う。

ライブラリの場合は Gemfile.lock は含めないことの方が多いので、大いにやると良さそう。マトリックステストも ENV を置くだけなので簡単。

eval_gemfile と言えば dry-rb や thredded 等で、開発用の gem を別ファイルに追い出している文化も面白いよね。(余談です)

これも余談だけど、一時期、Gemfile.local を読み込ませるために

eval_gemfile(__FILE__ + ".local") if File.exist?(__FILE__ + ".local")

と書くのが流行っていた時期もあって、これも Gemfile.lock に差分が出るので僕は苦手だったなって記憶が蘇ってきた。

各環境の Gemfile を用意する

各環境用の Gemfile を用意して、 BUNDLE_GEMFILE 環境変数を設定する。これも gem でよく見る。

# gemfiles/rails_5_2.gemfile
source "https://rubygems.org"

gem "rails", "~> 5.2.0"
gemspec path: '../'
# .github/workflows/main.yml
strategy:
  matrix:
    gemfile:
      - rails_5_2
      - rails_6_0
      - rails_6_1
env:
  BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile

gem は基本的に gemspec に依存が書かれていて、一部の指定が厳しい部分だけ *.gemfile で上書きすれば良いので、この形が自然に取れる。

アプリケーションの場合は eval_gemfile を利用して、共通部分は Gemfile.common とか Gemfile.global とかに抽出して同じことをやる。

3 ファイル (current 用, next 用, common) 必要なことと、common に何を追い出すかを考えるのがめんどくさいことがイマイチ。

Gemfile.next の symlink を置いて切り替える

RailsConf 2018 で発表されていた方法。

speakerdeck.com

僕は Getting Ready for Rails 6.0: How to Dual Boot - FastRuby.io | Rails Upgrade Service に書いてあるので知った。

fastruby.io

Gemfile に next? を定義して、symlink 経由で呼ばれたかどうかで切り替える。

def next?
  File.basename(__FILE__) == "Gemfile.next"
end

if next?
  gem 'rails', git: 'https://github.com/rails/rails.git', branch: 'main'
else
  gem 'rails', '~> 6.1.0'
end
  • next? 用の lock ファイルは Gemfile.next.lock になる
    • 差分が別ファイルになるのでコミットできる
  • Gemfile だけで一元管理できる

という、上であげた 2 つの方法のデメリットをそれぞれ潰すことができている。

Shopify/bootboot

id:takkan_m が教えてくれた。

https://github.com/Shopify/bootboot

symlink を使わず、Bundler の plugin として実装したもの。

if ENV['DEPENDENCIES_NEXT']
  ...
end

の内容が Gemfile_next.lock に書き出される。