Rails初心者に送る「Bundlerチョットワカルマニュアル」

## はじめに

Railsアプリケーション開発に必要不可欠なツールのひとつがBundlerです。
Railsを学習している人なら必ず一度は`bundle install`コマンドを実行したことがあるでしょう。

Railsの学習書などに従って指示されたコマンドを入力するだけなら簡単ですが、実務では`bundle install`以外にも必要な知識が出てきます。
しかし、Bundlerは意外とクセがあるというか、使っているうちに徐々に「あー、そういうことね」と経験から使い方や仕様の理解が進むような印象があります。筆者の場合はそうでした。

そこでこの記事では、Rails初心者を対象に「実務でRailsを使うなら知っておくと役立つBundlerの知識」を説明します。
題して「Bundlerチョットワカルマニュアル」です！

### 本記事の対象読者
本記事では以下のような読者を想定しています。

- Railsアプリケーション開発を現在学習している方や、実務に参加してまだ間もない方
- Bundlerの役割や基本的なコマンドについてはすでに学習済みの方

Bundlerのイロハについては本記事では説明しません。
別途ネット記事や技術書等で学習しておいてください。
（拙著「[プロを目指す人のためのRuby入門 改訂2版](https://gihyo.jp/book/2021/978-4-297-12437-3)」の第13章でもBundlerの説明をしていますが、事前知識はそのレベルで大丈夫です）

### 本記事で使用するBundlerのバージョン

本記事ではBundler 2.4の仕様をベースに執筆しました。
メジャーバージョンが変わらない限り、基本的な考え方は大きく変わらないはずです。

### 本記事で想定する実行環境と、おことわり

本記事で想定している実行環境はMacやWindowsのWSL環境で直接Railsアプリケーションを開発するケースです。
筆者は普段Dockerを使っていないので詳しいことはわかりませんが、もしかするとDocker環境では考え方が異なる部分があるかもしれません。

加えて、筆者は `bundle install` 時の `--path vendor/bundle` （もしくは`bundle config set path 'vendor/bundle'` ）も使っていません。
こちらもやはり、 `--path vendor/bundle` を使ってインストールした場合は考え方が異なる部分があるかもしれません。あらかじめご了承ください。

また、本記事で説明するのはRailsアプリケーション開発時のBundler利用法です。
本番サーバーにデプロイする場合もやはり考え方が異なるかもしれません。


それでは以下が本編です！

## bundle = bundle install

`bundle`コマンドのデフォルトコマンドは`install`です。
なので、`bundle install`の代わりに`bundle`だけ実行しても同じ挙動になります。

```shell
# 以下の2つはどちらも同じ
$ bundle
$ bundle install
```

慣れてきたら`bundle install`よりタイプ量の少ない`bundle`で済ませてもよいでしょう。

**追記：将来的には仕様が変わるそうです**

記事の公開後、Bundlerのコミッタの @hsbt さんから以下のコメントをいただきました。

> `bundle install` は `bundle` にしてもよいの箇所、将来的には廃止して help を出すと変える予定なので、明示的に bundle install とうつ、の方が良いかもです。
> https://twitter.com/hsbt/status/1721683961871802810

・・・とのことなので、明示的に`bundle install`と打ち込む、もしくは将来仕様が変わることを認識した上で、`bundle`を使う、というふうにした方が良さそうです。

@hsbt さん、コメントありがとうございました！

## --path vendor/bundle は付けなくてもよい

Bundlerの入門記事ではよく `bundle install --path vendor/bundle` というように `--path vendor/bundle` を付ける（もしくは`bundle config set path 'vendor/bundle'`で設定を変更する）ように指示しているものを見かけますが、このオプションは必ずしも付ける必要はありません。

もしあなたが「よくわからないけどおまじないとして `--path vendor/bundle` を付けている」という場合は以下の記事を読んで本当に必要かどうかを検討してください。

https://qiita.com/jnchito/items/99b1dbea1767a5095d85

ちなみに筆者は特別な理由がない限り `--path vendor/bundle` は付けません。

## Bundlerを利用するならbundle execを付けてコマンドを実行する

Bundlerを使うとそのプロジェクトで使用するgemのバージョンを固定することができます。
ただし、その場合は実行したいコマンドの手前に `bundle exec` を付ける必要があります。

```shell
# ローカルマシンにインストールされている最新バージョンのrspecを実行する
# （Bundlerを利用しない）
$ rspec

# Gemfile.lockで指定されたバージョンのrspecを実行する
# （Bundlerを利用する）
$ bundle exec rspec
```

## railsコマンドはbundle execを付けなくても大丈夫

ただし、`rails` コマンドはRailsがうまくやってくれるので、`bundle exec`を付けなくてもGemfile.lockで指定したバージョンのRailsが実行されます。ついでにいうと、`bin/rails`とした場合も同じです。

```shell
# 以下の3つはいずれもGemfile.lockで指定されたバージョンのrailsで実行される
$ rails g model User name email
$ bundle exec rails g model User name email
$ bin/rails g model User name email
```

ですが、上の話はRailsプロジェクト内（appフォルダやconfigフォルダがあるフォルダ内）の話である点に注意してください。`rails new` コマンドなど、Railsプロジェクト以外の場所で実行する場合はローカルマシンにインストールされている最新バージョンのRailsが使われます。

```shell
# Railsプロジェクト内だとGemfile.lockで指定されたバージョンのRailsが使われる
$ ls
Gemfile      README.md    app          config       db           log          storage      tmp
Gemfile.lock Rakefile     bin          config.ru    lib          public       test         vendor
$ rails --version
Rails 7.0.6

# Railsプロジェクトの外に出ると、ローカルマシンにインストールされている最新バージョンのRailsが使われる
$ cd ..
$ rails --version
Rails 7.1.0
```

参考文献

https://qiita.com/jnchito/items/c5a0848144203dce6e26

## 開発時もしくはテスト実行時しか使わないgemはグループを指定する

新しいgemを追加するときは、そのgemを本番環境でも使うgemかどうかを確認してください。
開発時（ローカル環境）にしか使わないgemはdevelopmentグループへ、テスト実行時にしか使わないgemはtestグループへそれぞれ追加しましょう。

```ruby:Gemfile
# すべての環境（本番、開発、テスト）で使われるgem（defaultグループ）
gem 'devise'
gem 'kaminari'

# 開発環境とテスト環境で使われるgem
group :development, :test do
  gem 'debug', platforms: %i[mri mingw x64_mingw]
  gem 'dotenv-rails'
end

# 開発環境でのみ使われるgem
group :development do
  gem 'web-console'
  gem 'letter_opener_web'
end

# テスト環境でのみ使われるgem
group :test do
  gem 'capybara'
  gem 'selenium-webdriver'
end
```

なお、上のコメントにも書いた通り、`group`のブロックに囲まれていない部分は「defaultグループ」と呼ばれます。

## Gemfileにgemを追加したら bundle install (bundle updateではない)

Gemfileに新しいgemを追加した後に実行すべきコマンドは `bundle install` です。

```diff_ruby:Gemfile 
 # ...

 # deviseを追加した → bundle installを実行
+gem 'devise'
```

ときどき `bundle update` を実行する人がいますが、gem名を指定しない `bundle update` はプロジェクト内のすべてのgemを最新化しようとするため、場合によっては後方互換性問題が発生してアプリケーションが壊れることがあります。

## Gemfileからgemを削除したら bundle install (bundle updateではない)

不要になったgemをGemfileから削除する場合も`bundle install`を実行してください。

```diff_ruby:Gemfile 
 # ...

 # deviseを削除した → bundle installを実行
-gem 'devise'
```

削除したのにinstallというのは少し不自然ですが、この`bundle install`はGemfile.lockを更新するための`bundle install`だと考えてください。

## bundle installでは最新バージョンがインストールされないこともある
Gemfileに新しいgemを追加して `bundle install` を実行すると原則として最新バージョンがインストールされます。

しかし、ローカルマシンにすでに同じgemがインストールされていて、そのバージョンが他のgemとの依存関係的にも問題がない場合は、インストール済みのgemがそのまま使われるため、最新バージョンにならないことがあります。

その場合はこの次に説明する方法でgemをアップデートしてください。

## 既存のgemをアップデートしたいときはスコープを絞る

既存のgemをアップデートしたいときは、`bundle update <gem-name>`のようにして更新対象のgemを限定しましょう。

```shell
# ✅ deviseだけを更新
$ bundle update devise

# ✅ deviseとomniauthを更新
$ bundle update devise omniauth
```

`-g development`や`-g test`といったオプションを指定して特定のグループのgemだけを更新することもできます。

```shell 
# developmentとtestグループのgemをまとめて更新
$ bundle update -g development -g test
```

引数やオプションを指定しない `bundle update` は前述の通り、プロジェクト内のすべてのgemを最新化しようとするため、場合によっては後方互換性問題が発生してアプリケーションが壊れることがあります。

```shell
# ❌ gem名を指定しないbundle updateは危険!!（何が起きるか理解した上で使う）
$ bundle update
```

もちろん、引数やオプションを指定しない `bundle update` を実行すると何が起きるのか理解した上で実行する場合はそれでも構いません。しかし、Rails初心者の場合は前述したような問題が起きることを知らずに `bundle update` を実行するケースが多いように思います。

## Gemfileではなるべくバージョンを指定しない

Gemfile内では以下のようにしてバージョンを指定することができます。

```ruby:Gemfile
# バージョン1.2.2以上、1.2.x以下のkaminariをインストール
gem 'kaminari', '~> 1.2.2'
```

しかし、gemはなるべく最新のものを使った方がいいです。
上の例であれば、kaminari 1.3.0や2.0.0がリリースされてもそのままではアップデートできません。
「原則として常に最新のgemを使う」という運用にするのであれば、バージョンは固定しない方が手軽にgemをバージョンアップできます。

```ruby:Gemfile
# 最新バージョンを使いたいのでバージョンを固定しない
gem 'kaminari'
```

ただし例外として、うかつにバージョンを上げてしまうと大きな問題が起きるgemに関してはバージョンを固定した方が良いです。
たとえば、Railsはマイナーバージョンやメジャーバージョンが上がると毎回何かしら後方互換性が失われるので、こういう場合はバージョンを固定した方が良いです。

```ruby:Gemfile
# Rails 7.1系へのアップデートは気軽にできないのでバージョンを固定しておく
gem 'rails', '~> 7.0.6'
```

## バージョンを指定する場合はコメントを残す

「本来なら最新バージョンを使うべきだが、やんごとなき理由でバージョンを固定せざるを得ない」という場合は、その理由やバージョン指定を解除できる条件をコメントとして残しておきましょう。

```ruby:Gemfile
# 1.5系にすると PG::Coder.new(hash) is deprecated のような例外が出る
# Rails 7では修正されているので、Rails 7アップデート時に最新化する
# https://github.com/rails/rails/issues/48060
gem 'pg', '~> 1.4.6'
```

## bundle outdatedで古くなっているgemを探す

`bundle outdated`コマンドを実行すると、プロジェクト内で最新バージョンを利用していないgemの一覧が表示されます。

``` 
$ bundle outdated
Fetching gem metadata from https://rubygems.org/.........
Resolving dependencies...

Gem                        Current        Latest         Requested  Groups
actioncable                7.0.5          7.1.1
actionmailbox              7.0.5          7.1.1
actionmailer               7.0.5          7.1.1
actionpack                 7.0.5          7.1.1
actiontext                 7.0.5          7.1.1
actionview                 7.0.5          7.1.1
activejob                  7.0.5          7.1.1
activemodel                7.0.5          7.1.1
activerecord               7.0.5          7.1.1
activestorage              7.0.5          7.1.1
activesupport              7.0.5          7.1.1
addressable                2.8.4          2.8.5
bootsnap                   1.16.0         1.17.0         >= 0       default
brakeman                   6.0.0          6.0.1          >= 0       development
bugsnag                    6.25.2         6.26.0         >= 0       default
carrierwave                2.2.4          3.0.4          >= 0       default
ckeditor                   4.3.0          5.1.1          ~> 4.3.0   default
climate_control            0.2.0          1.2.0
config                     4.2.1          5.0.0          >= 0       default
# ...
```

各列の意味は以下の通りです。

- Gem = gemの名前
- Current = プロジェクト内で現在利用しているバージョン
- Latest = 最新版のバージョン
- Requested = Gemfile内で指定されているバージョン
  - 特にバージョンを指定していない場合は `>= 0`
  - 空欄の場合は依存関係によって自動的にインストールされたgem
- Groups = gemをインストールするグループ
  - developmentやtestなど
  - グループを指定していない場合はdefault
  - 空欄の場合は依存関係によって自動的にインストールされたgem

bundle outdatedで表示されるgemがなるべく少なくなるように、こまめにgemをアップデートしましょう。

なお、Gemfile内の全gemをアップデートするときの手順については以下の記事で詳しく説明しています。

https://qiita.com/jnchito/items/0ee47108972a0e302caf

## 依存関係の解消に失敗したときは

`bundle install`や`bundle update <gem-name>`を実行すると、依存関係の解消に失敗して処理が中断される場合があります。

```   
$ bundle install
Fetching gem metadata from https://rubygems.org/........
Resolving dependencies...
Bundler could not find compatible versions for gem "rubyzip":
  In snapshot (Gemfile.lock):
    rubyzip (>= 2.3.2)

  In Gemfile:
    rubyzip (~> 2.3.2)

    axlsx (~> 2.0.1) was resolved to 2.0.1, which depends on
      rubyzip (~> 1.0.0)

Deleting your Gemfile.lock file and running `bundle install` will rebuild your snapshot from scratch, using only
the gems in your Gemfile, which may resolve the conflict.
```

たとえば上の例ではエラーメッセージから以下のような問題が発生していることがわかります。

- axlsx 2.0.1はrubyzip 1.0.xに依存している（バージョン1.1以上のrubyzipは使えない）
- Gemfileでrubyzip 2.3.2以上を使うように指定されている
- よって今のままではaxlsx 2.0.1とrubyzip 2.3.2以上を同時に使う方法がない

この問題が起きる場合、最新版のgemを使うようにbundle updateすると解決することが多いです。ただし、gemのバージョンが上がると後方互換性がなくなったりすることがあるので、Changelog等を確認してBreaking changeが含まれないか確認しておきましょう。

```shell
# ケンカしているgemをまとめて更新する（Gemfile内のバージョン指定は外しておく）
$ bundle update axlsx rubyzip
```

bundle updateするとさらに別の問題が起きて依存関係の解消に失敗する場合があります。その場合はエラーメッセージをよく読んで、アップデート対象のgemを追加してください。

```shell 
# 依存関係が解消するまで、アップデート対象のgemを増やしていく
$ bundle update axlsx rubyzip sassc webmock
```

しかし、場合によってはどうがんばっても問題が解消しない場合があります。
その場合は、問題が起きているgemのissueを検索してみてください。
同じような問題に遭遇している人がissueを上げていて、解決策を載せてくれているかもしれません。
（ちなみに上の例では、`axlsx`から後継の`caxlsx`に置き換えると問題が解決します）

https://github.com/randym/axlsx/issues/501

依存関係の問題を解消するのはなかなか面倒です。
Rails初心者さんは自力で解決するのが難しいと思ったら、メンターや先輩プログラマに早めに相談するのをお勧めします。

## まだリリースされていないGitHub上のgemをインストールする

前述した依存関係の問題やgemのバグなど、問題は公式に解消されていないが、その問題を解消するプルリクエストが作成されていてマージ待ちになっていることもあります。その場合はGemfileにGitHubのリポジトリ名やブランチ名を指定することができます。

```ruby:Gemfile 
# スラッシュ（/）が"&#x2F;"にエスケープされる問題を回避する
# https://github.com/rack/rack/pull/2097 のマージ待ち
gem 'rack', github: 'JunichiIto/rack', branch: 'delegate-to-cgi-escapehtml'
```

GitHub上のgemをインストールする場合は何らかの理由があるはずなので、上の例のようにコメントで理由を残しておくことをお勧めします。

すでにプルリクエストがmainブランチにマージ済みで正式リリースを待つだけ、という場合は、そのgemのGitHubリポジトリを指定するだけでもOKです。

```ruby:Gemfile
# https://github.com/rack/rack/pull/2097 の正式リリース待ち
gem 'rack', github: 'rack/rack'
```

ただし、GitHubのリポジトリを直接指定する場合は以下のようなリスクがあることを覚えておきましょう。

- まだ正式にリリースされていないということは何かしらの不具合が隠れている可能性がある
- ある日突然、そのリポジトリやブランチが削除されてインストールできなくなる可能性がある

## Gemfile.lockは手で編集しない
人間が編集していいのはGemfileです。Gemfile.lockはBundlerが自動的に更新するファイルなので、手作業で編集してはいけません。

## Gemfileを変更したら一緒にGemfile.lockも更新する
Gemfileに何らかの変更を加えたら、必ず`bundle install`または`bundle update <gem-name>`を実行してGemfile.lockを更新してください。
その上で、GemfileとGemfile.lockの変更を同じコミットに含めてください。

プログラミング初心者さんのコードレビューをしていると、ときどき「Gemfileにgemが追加されているが、Gemfile.lockの変更がどこにも見当たらない（もしくはその逆）」というケースがあります。

## Gemfile.lockがコンフリクトしたら
チーム開発していると、ときどきGemfile.lockがコンフリクトします。
対処方法はいろいろあると思いますが、筆者は次のような手順でコンフリクトを解消しています。

ここでは例として、my-featureブランチをmainブランチにマージしようとして（＝プルリクエストを出していて）、Gemfile.lockがコンフリクトしたとします。

1. ローカル環境でmainブランチに切り替える（`git checkout main`）
1. GitHubから最新のmainブランチのコードを持ってくる（`git pull origin main`）
1. 開発ブランチに切り替える（`git checkout my-feature`）
1. mainブランチをマージする（`git merge main`）
1. Gemfile.lockがコンフリクトするので、いったんmainブランチのGemfile.lockを正とする（mainブランチのGemfile.lockにまるっと置き換える）
1. Gemfile.lockを`git add`する（`git add Gemfile.lock`）
1. マージを続行する（`git merge --continue`）
1. マージが正常に完了したら、`bundle install`を実行する（my-featureブランチでgemを追加していた場合。既存のgemを更新していた場合は`bundle update <gem-name>`）
1. `git diff`でGemfile.lockの変更点を確認する。おそらくmy-featureブランチで追加しようとしていたgemがdiffに含まれるはず
1. 開発中の機能が問題なく動作することを確認する
1. Gemfile.lockの変更点をコミットし、GitHubにpushする

ただし、最初のうちはなかなか自信をもって操作できないと思うので、Rails初心者さんはメンターや先輩プログラマと一緒に作業するのをお勧めします。

## Gemfile.lockを見てgemの依存関係を調べる

Gemfile.lockを見るとgemの依存関係がわかります。

```Gemfile.lock
# ...
axlsx (2.0.1)
  htmlentities (~> 4.3.1)
  nokogiri (>= 1.4.1)
  rubyzip (~> 1.0.0)
# ...
```

たとえば上の例では以下の依存関係があることがわかります。

- axlsx 2.0.1は、
  - htmlentities 4.3.1以上4.3.x以下に依存
  - nokogiri 1.4.1以上に依存
  - rubyzip 1.0.0以上1.0.x以下に依存

## なぜか期待したバージョンまで上がらない？

`bundle update <gem-name>`を実行しても期待したバージョンまでバージョンが上がらない場合は、他のgemがアップデートしようとしているgemの古いバージョンに依存している可能性があります。
そういうときはGemfile.lockを覗いてgemの依存関係を調べてみてください。

gemの依存関係がわかったら依存関係にあるgemをまとめてbundle updateすると問題が解決することがあります。

```shell 
# アップデートしたいgem-name-1と、古いバージョンのgem-name-1に
# 依存していたgem-name-2をまとめてアップデートする
$ bundle update gem-name-1 gem-name-2
```

もしくは一時的にGemfileにgemのバージョンを指定して、`bundle update <gem-name>` を実行すると、コマンドの実行が失敗し、そのエラーメッセージから問題の原因（依存関係の詳細）が判明することもあります。

```ruby:Gemfile 
# 「このバージョンまで上げたい！」というバージョン番号を指定する（デバッグ目的で）
gem 'axlsx', '~> 2.0.1'
gem 'rubyzip', '~> 2.3.2'
```

```shell 
# エラーメッセージを読んでバージョンが上がらない原因（gem同士の依存関係）を把握する
$ bundle update axlsx rubyzip
Fetching gem metadata from https://rubygems.org/........
Resolving dependencies...
Bundler could not find compatible versions for gem "rubyzip":
  In snapshot (Gemfile.lock):
    rubyzip (>= 2.3.2)

  In Gemfile:
    rubyzip (~> 2.3.2)

    axlsx (~> 2.0.1) was resolved to 2.0.1, which depends on
      rubyzip (~> 1.0.0)

Deleting your Gemfile.lock file and running `bundle install` will rebuild your snapshot from scratch, using only
the gems in your Gemfile, which may resolve the conflict.
```

## まとめ

というわけで、この記事ではRails初心者向けに「実務でRailsを使うなら知っておくと役立つBundlerの知識」を説明してみました。

いずれも開発経験が長いと自然に身に付いてくるテクニックなのですが、初心者のうちはやり方がわからなかったり、間違った操作をしてしまったりしてトラブルの原因になりがちです。

これから実務に入っていこうとしている人や実務に入って間もない人は、ぜひ本記事の内容を理解して「Bundlerチョットワカルエンジニア」になってください！