Rails6/7(with zeitwerk)でGemfileのgemが自動requireされないときに確認すること

## 問題の共有

railsで例えば、[twitter](https://github.com/sferik/twitter),と[rubyzip](https://github.com/rubyzip/rubyzip) gemを使う場合を考えてみます。その時Gemfileには以下のように書かれるでしょう

```ruby:Gemfile
gem 'twitter'
gem 'rubyzip'
```

bundle install後にrails consoleでrequireされているか確認してみましょう。以下の環境で試しましたが同じ状況です。

- rails 6, 2.4.17
- rails 7.0.6, bundler 2.4.16

```ruby:rails console
root@2fc4af94850b:/usr/src/app# bin/rails c
Running via Spring preloader in process 239
Loading development environment (Rails 6.1.7.4)
irb(main):001:0> defined?(Twitter)
=> "constant"                       # <- 定義済み。こちらは問題ない
irb(main):002:0> defined?(Zip)
=> nil                              # <- 未定義！！Gemfileの指定は同じなのになぜ？？？？
irb(main):003:0> require 'zip'
=> true
irb(main):004:0> defined?(Zip)
=> "constant"                       # <- 参照できたのでLOAD_PATHは間違っていない
```

`Twitter` はきちんと定義されていますが、`Zip` は定義されていません。。つまり、タイトルの `「Gemfileのgemが自動requireされない」` が確認できました。

## ちょっと待って！そもそもzeitwerkって、gemはrequire必須じゃなかったっけ？

はい、正しいです。 [Railsガイド](https://railsguides.jp/classic_to_zeitwerk_howto.html#%E4%B8%8D%E8%A6%81%E3%81%AArequire%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%97%E3%81%AF%E3%81%99%E3%81%B9%E3%81%A6%E5%89%8A%E9%99%A4%E3%81%99%E3%82%8B%E3%81%93%E3%81%A8)でも明確に書かれています。

> Railsアプリケーションでは、requireは「libのコード」「gemなどのサードパーティ依存関係」「標準ライブラリ」の読み込みにしか使いません。アプリケーションのオートロード可能なコードは決してrequireしないでください。


ちなみに 「libのコード」について具体的には `ActiveSupport::Dependencies.autoload_paths` で参照されるパスしかautoloadされません。以下rails6の例です。

```ruby:rails console
root@2fc4af94850b:/usr/src/app# bin/rails runner 'puts ActiveSupport::Dependencies.autoload_paths.join("\n")'
Running via Spring preloader in process 208
/usr/src/app/app/channels
/usr/src/app/app/controllers
/usr/src/app/app/controllers/concerns
/usr/src/app/app/helpers
/usr/src/app/app/jobs
/usr/src/app/app/mailers
/usr/src/app/app/models
/usr/src/app/app/models/concerns
/usr/local/bundle/gems/actiontext-6.1.7.4/app/helpers
/usr/local/bundle/gems/actiontext-6.1.7.4/app/models
/usr/local/bundle/gems/actionmailbox-6.1.7.4/app/controllers
/usr/local/bundle/gems/actionmailbox-6.1.7.4/app/jobs
/usr/local/bundle/gems/actionmailbox-6.1.7.4/app/models
/usr/local/bundle/gems/activestorage-6.1.7.4/app/controllers
/usr/local/bundle/gems/activestorage-6.1.7.4/app/controllers/concerns
/usr/local/bundle/gems/activestorage-6.1.7.4/app/jobs
/usr/local/bundle/gems/activestorage-6.1.7.4/app/models
/usr/src/app/test/mailers/previews
```

このようにrailsガイドにgemファイルはrequireしてね、と書いてあるのになぜGemfileに記載のgemが自動でrequireされているかですが、これは `bundlerの仕組み(Bundler.require)のおかげ` です。詳細は以下の記事などご参照ください。

- [なぜRuby on Railsではrequireを実施なくてよいのか](https://blog.wyrd.co.jp/2022/04/01/%E3%81%AA%E3%81%9Cruby-on-rails%E3%81%A7%E3%81%AFrequire%E3%82%92%E5%AE%9F%E6%96%BD%E3%81%AA%E3%81%8F%E3%81%A6%E3%82%88%E3%81%84%E3%81%AE%E3%81%8B/)
- [Railsにおいてrequireがなぜ要らないのかを追ってみた(Gemfile編)](https://zenn.dev/toshi023/articles/d99b00510aafb3)

## じゃあGemfileに書いておけばrequireいらんのじゃ？

はい、ただしうまくrequireされないパターンがあります。これは [実際にrequireしている箇所を見れば明らか](https://github.com/rubygems/rubygems/blob/36ea2bc28306a1487ec45f5bfbab1bdcb0779e87/bundler/lib/bundler/runtime.rb#L51-L78) です。[^1]
[^1]: Qiitaってgithubコードの自動展開まだできないのですね。。。残念。

```ruby:bundler/lib/bundler/runtime.rb(requireメソッド内)
...
        begin
          # Loop through all the specified autorequires for the
          # dependency. If there are none, use the dependency's name
          # as the autorequire.
          Array(dep.autorequire || dep.name).each do |file|
            # Allow `require: true` as an alias for `require: <name>`
            file = dep.name if file == true
            required_file = file
            begin
              Kernel.require file
            rescue RuntimeError => e
              raise e if e.is_a?(LoadError) # we handle this a little later
              raise Bundler::GemRequireError.new e,
                "There was an error while trying to load the gem '#{file}'."
            end
          end
        rescue LoadError => e
          raise if dep.autorequire || e.path != required_file

          if dep.autorequire.nil? && dep.name.include?("-")
            begin
              namespaced_file = dep.name.tr("-", "/")
              Kernel.require namespaced_file
            rescue LoadError => e
              raise if e.path != namespaced_file
            end
          end
        end
...
```

ここで、実際にrequireしていることを抜粋すると

```ruby
file = dep.name if file == true
required_file = file
begin
  Kernel.require file
  ...
```

と、 `dep.name` をもとにKernel.requireしていることが確認できます。 `dep.name` はデフォルトではGemfileのgem名前なので、 

 - `twitter` の場合は `require 'twitter'` なのでrequireされる
 - `rubyzip` の場合は `require 'zip'` でrequire されるべきだが `require 'rubyzip'` となるのでrequire失敗する

のでした。

そこで動作を確認するために `gem 'rubyzip', require: true` と明示的にrequireするように指定してrails consoleを起動すると

```ruby:rails console
root@2fc4af94850b:/usr/src/app# bin/rails c
<internal:/usr/local/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:37:in `require': cannot load such file -- rubyzip (LoadError)
	from <internal:/usr/local/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:37:in `require'
	from /usr/local/bundle/gems/bootsnap-1.16.0/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:17:in `require'
	from /usr/local/bundle/gems/bundler-2.4.17/lib/bundler/runtime.rb:60:in `block (2 levels) in require'
	from /usr/local/bundle/gems/bundler-2.4.17/lib/bundler/runtime.rb:55:in `each'
	from /usr/local/bundle/gems/bundler-2.4.17/lib/bundler/runtime.rb:55:in `block in require'
	from /usr/local/bundle/gems/bundler-2.4.17/lib/bundler/runtime.rb:44:in `each'
	from /usr/local/bundle/gems/bundler-2.4.17/lib/bundler/runtime.rb:44:in `require'
	from /usr/local/bundle/gems/bundler-2.4.17/lib/bundler.rb:187:in `require'
	from /usr/src/app/config/application.rb:7:in `<top (required)>'
	from <internal:/usr/local/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:37:in `require'
	from <internal:/usr/local/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:37:in `require'
	from /usr/local/bundle/gems/spring-4.1.1/lib/spring/application.rb:92:in `preload'
	from /usr/local/bundle/gems/spring-4.1.1/lib/spring/application.rb:166:in `serve'
	from /usr/local/bundle/gems/spring-4.1.1/lib/spring/application.rb:148:in `block in run'
	from /usr/local/bundle/gems/spring-4.1.1/lib/spring/application.rb:142:in `loop'
	from /usr/local/bundle/gems/spring-4.1.1/lib/spring/application.rb:142:in `run'
	from /usr/local/bundle/gems/spring-4.1.1/lib/spring/application/boot.rb:19:in `<top (required)>'
	from <internal:/usr/local/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:85:in `require'
	from <internal:/usr/local/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:85:in `require'
	from -e:1:in `<main>'
```

予想通り `cannot load such file -- rubyzip (LoadError)` となりました。

これを回避するには [ソースコード](https://github.com/rubygems/rubygems/blob/36ea2bc28306a1487ec45f5bfbab1bdcb0779e87/bundler/lib/bundler/runtime.rb#L56) にも[ドキュメント](https://bundler.io/v2.4/man/gemfile.5.html#REQUIRE-AS) にも書いてありますが ``gem 'rubyzip', require: 'zip'`` とrequireで名前を指定してあげればよいです。

```ruby:Gemfile
gem 'twitter'
gem 'rubyzip', require: 'zip'
```

原因を紐解いてみるとシンプルですが、使えて当たりまえな便利な機能が想定外の動きをすると、前提知識があるため無駄に調査時間がかかりますね。。。とはいえ、昔を知っている身からはzeitwerkのおかげでだいぶ楽になった感🥰

## まとめ

Gemfileに書いたgemが自動でrequireしない場合は、以下の3つを確認すること

- `gem 'twitter', require: false`  と、明示的にrequireしないよう宣言していないか？
- `gem 'rubyzip'` のような `gemの名前とrequireされるモジュール名が異なるgem` ではないか？
- 動作させたいRails環境(test, development...など)と、bundlerのgroupが異なっていないか？[^2]

[^2]: 記事にはありませんが、チェック箇所としては必要なので追記