2020年12月に、 Rails アプリの Docker イメージをビルドする CI が突然エラーを起こしたことがありました。これは bundler 2.2.0 がリリースされたことに起因していたため、イメージに入れる bundler を一旦 2.1.4 に固定することで対処しました。
そのままずっと問題に蓋をしていたものの、 bundler は既に 2.2.33 や 2.3.15 が出ているため、きちんと当時のエラーについて調べてバージョンアップしようと思い立ちました。
エラーが何だったのか、 bundler の changelog などを見てもすぐにはわからなかったので、実際に様々なバージョンで実験して探りました。( x86-64 の Linux および macOS を対象としています)
TL;DR
bundler 2.2 からは、 Gemfile.lock に Ruby のプラットフォームの情報( x86_64-linux
や x86_64-darwin-18
など)を記載して活用しようとします。特に初期の頃はこれがうまく働かず、 gem のバイナリ配布版を選ばずソース版からコンパイルしようとしてしまうことがありました。
対策として以下を実施すれば、多くの場合にうまく動くはずです。
- bundler 2.2.3 以上を使う1
- Gemfile.lock に適切なプラットフォームを書き込む
- 開発環境だけでなくデプロイ環境の分も明示的に追加が必要
- bundler 2.2.0 までにある
ruby
は不要なら抜いておく
gem install bundler -v '>= 2.2.3' --conservative
gem env platform | tr ':' '\n' # 現在のプラットフォームを表示
bundle lock --add-platform x86_64-linux # 必要なだけ追加
bundle lock --remove-platform ruby
実験
環境準備
実験は Ubuntu 18.04 上で、 Ruby 2.7.4 を用いて行いました。ただし後で、開発環境が macOS の場合を模倣します。
プラットフォーム毎にパッケージが用意されている gem でエラーが発生しうるので、今回は libv8 だけで試します。
source "https://rubygems.org"
gem "libv8"
また、様々なバージョンの bundler で試したいので、まとめてインストールしておきます。(結果的には 2.2.3 まであれば十分でした)
# bundler 2.1.4, 2.2.0〜33, 2.3.0〜15 をインストール
gem install --no-document bundler:{2.1.4,2.2.{0..33},2.3.{0..15}}
# 複数入れてある場合、バージョンを指定して実行できる
bundle _2.3.7_ -v
(1) Gemfile.lock が無い場合
新しく Rails アプリを作るときなどは、 Gemfile.lock はまだ無く Gemfile から生成することになります。この結果が bundler バージョンによって異なるか確認しておきます。
set -eu
versions=(2.1.4 2.2.{0..33} 2.3.{0..15})
gf0=Gemfile # 準備で作ったファイル
rm -rf .bundle/
bundle config set --local path 'vendor/bundle'
for v in ${versions[@]}
do
echo "#--- bundler ${v} ---#"
gf=${gf0}-${v}
cp ${gf0} ${gf}
bundle _${v}_ install --gemfile=${gf} || true
echo
done
生成された Gemfile-${v}.lock は以下のようになりました。この3パターンを以降の実験で使います。
version | PLATFORMS |
---|---|
2.1.4 |
ruby のみ |
2.2.0 |
ruby と x86_64-linux
|
2.2.1 以上 |
x86_64-linux のみ |
GEM
remote: https://rubygems.org/
specs:
libv8 (8.4.255.0)
libv8 (8.4.255.0-x86_64-linux)
PLATFORMS
ruby
x86_64-linux
DEPENDENCIES
libv8
BUNDLED WITH
2.2.0
(2) Gemfile.lock がある場合
新しい開発環境に gem をインストールするような場合です。少しややこしい場合を想定し、「 Gemfile.lock は macOS 上で生成した」「新しい環境は Linux 」という状況を再現して実験します( Gemfile.lock 内のプラットフォーム情報を無理やり書き換えておきます)。
set -eu
versions=(2.1.4 2.2.{0..33} 2.3.{0..15})
v0=2.1.4
gf0=Gemfile-${v0} # 実験(1)で作ったファイル
rm -rf .bundle/
bundle config set --local path 'vendor/bundle'
for v in ${versions[@]}
do
echo "#--- bundler ${v0} --> ${v} ---#"
gf=${gf0}-${v}
cp ${gf0} ${gf}
cp ${gf0}.lock ${gf}.lock
sed -i -e 's/x86_64-linux/x86_64-darwin-18/g' ${gf}.lock
bundle _${v}_ install --gemfile=${gf} || true
echo
done
初回が 2.1.4
2.1.4 で作った Gemfile.lock の PLATFORMS には ruby
しか書かれていません。そのため開発環境を変えた影響はありません。
2.2.0 〜 2.2.2 では、 PLATFORMS に現在の環境 x86_64-linux
が追加されました。 2.2.3 以上では何も変更されませんでした。
GEM
remote: https://rubygems.org/
specs:
libv8 (8.4.255.0)
libv8 (8.4.255.0-x86_64-linux)
PLATFORMS
ruby
x86_64-linux
DEPENDENCIES
libv8
BUNDLED WITH
2.2.2
初回が 2.2.0
初期状態では PLATFORMS に ruby
と x86_64-darwin-18
が書かれています。
2.2.0 〜 2.2.2 では、 PLATFORMS に現在の環境 x86_64-linux
が追加されました。 2.2.3 以上では gem をコンパイルしようとしました(手元ではコンパイル失敗しました)。
初回が 2.2.1
初期状態では PLATFORMS に x86_64-darwin-18
のみが書かれています。
どのバージョンでも PLATFORMS に情報が追加されました。バージョン毎の追加内容は実験(1)と同じでした。
GEM
remote: https://rubygems.org/
specs:
libv8 (8.4.255.0)
libv8 (8.4.255.0-x86_64-darwin-18)
libv8 (8.4.255.0-x86_64-linux)
PLATFORMS
ruby
x86_64-darwin-18
x86_64-linux
DEPENDENCIES
libv8
BUNDLED WITH
2.2.1
(3) deploymentを指定した場合
デプロイ環境で gem をインストールする際は、開発時に想定しているものがそのまま使われるべきです。そのために Gemfile.lock を変更しないよう deployment
または frozen
を指定します。変更しないという制約により、実験(2)とは異なった結果になります。
set -eu
versions=(2.1.4 2.2.{0..33} 2.3.{0..15})
v0=2.1.4
gf0=Gemfile-${v0} # 実験(1)で作ったファイル
rm -rf .bundle/
bundle config set --local deployment 'true' # ここを追加
bundle config set --local path 'vendor/bundle'
for v in ${versions[@]}
do
echo "#--- bundler ${v0} --> ${v} (deployment) ---#"
gf=${gf0}-${v}-deployment
cp ${gf0} ${gf}
cp ${gf0}.lock ${gf}.lock
sed -i -e 's/x86_64-linux/x86_64-darwin-18/g' ${gf}.lock
bundle _${v}_ install --gemfile=${gf} || true
echo
done
初回が 2.1.4
PLATFORMS に ruby
のみが書かれています。
2.2.0 では gem をコンパイルしようとしました(手元ではコンパイル失敗しました)。他では問題なくインストールされました。
初回が 2.2.0
PLATFORMS に ruby
と x86_64-darwin-18
が書かれています。
2.2.0 以上では gem をコンパイルしようとしました(手元ではコンパイル失敗しました)。したがって 2.1.4 のみで問題なくインストールされました。
初回が 2.2.1
PLATFORMS に x86_64-darwin-18
のみが書かれています。
2.1.4 では普通に x86_64-linux
がインストールされましたが、 2.2.0 〜 2.2.2 では x86_64-darwin-18
が誤インストールされました。 2.2.3 以上ではプラットフォーム不一致のエラーが出ました。
#--- bundler 2.2.1 --> 2.2.0 (deployment) ---#
Warning: the running version of Bundler (2.2.0) is older than the version that created the lockfile (2.2.1). We suggest you to upgrade to the version that created the lockfile by running `gem install bundler:2.2.1`.
Fetching gem metadata from https://rubygems.org/.
Using bundler 2.2.0
Fetching libv8 8.4.255.0 (x86_64-darwin-18)
Installing libv8 8.4.255.0 (x86_64-darwin-18)
Bundle complete! 1 Gemfile dependency, 2 gems now installed.
Bundled gems are installed into `./vendor/bundle`
#--- bundler 2.2.1 --> 2.2.3 (deployment) ---#
Your bundle only supports platforms ["x86_64-darwin-18"] but your local platform is x86_64-linux.
Add the current platform to the lockfile with `bundle lock --add-platform x86_64-linux` and try again.
動作の背景
実験結果をもとに、 bundler の changelog から動作変更の内容を探しました。
2.2.0
2.2 系から、 Gemfile.lock にプラットフォームの情報を持つようになりました。
しかし、 2.1.4 までに作られた Gemfile.lock は ruby
しか書いていません。これにより 2.2.0 は「厳密に ruby
のパッケージを選ぶ」という動作をし、ソース版をダウンロードしてコンパイルしてしまいます。
2.2.1
2.2.0 で判明した問題への暫定対応として、「 2.2 以前に作られた Gemfile.lock は 2.2 以前の方法で解釈する」ようになりました。これにより deployment (frozen) であっても昔の Gemfile.lock は問題なく使えます。
https://github.com/rubygems/rubygems/pull/4127
一方で、 2.2 以降に作られた Gemfile.lock に対しては記載のプラットフォームを守ろうとします。 2.2.0 が書き込んだ ruby
だけが一致すればそれを使おうとするため、やはりソース版をダウンロードしてコンパイルしてしまいます。
さらに 2.2.1 から PLATFORMS に ruby
を書かなくなったため、一致するプラットフォームが無い場合が出てきます。このときに誤ったバージョンをインストールしてしまう問題が報告されました。
https://github.com/rubygems/rubygems/issues/4166
2.2.3
開発環境とデプロイ環境とでプラットフォームが異なる場合の問題が修正されました。一致するプラットフォームが無いときは明確にエラーを出します。
https://github.com/rubygems/rubygems/pull/4172
また、 2.2 以前の Gemfile.lock に対する互換性が上がったようです。(本記事では実験(1)の「初回が 2.1.4 」において、不要な更新が発生しなくなっています)
対処法
bundler 2.2.x に由来するエラーは、だいたい Gemfile.lock 生成時と参照時の環境の違いによって起きます。最も単純な対処法は両者で bundler バージョンもプラットフォームも一致させることです。
とはいえ開発環境が macOS 、デプロイ環境が Linux といったことは普通にあります。このような場合は、「 Gemfile.lock にプラットフォームの情報を持つようになった」ということを理解して対処法を考えます。
- 使用する bundler は、動作の安定した 2.2.3 以降が良いです1
- 開発環境のプラットフォームを追加します:
bundle lock --add-platform <name>
- 新しく Gemfile.lock を生成する場合と同じものを入れておくのが良いと思います
- 特に 2.2.0 まであった
ruby
は、デプロイ環境でソース版を選ぶ原因になりうるので、必要ない限りは抜いておきます
- デプロイ環境のプラットフォームも追加します
- 2.2.3 以降なら、足りない場合はエラーの中で必要なコマンドを教えてくれます