0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-07-14

問題の共有

railsで例えば、twitter,とrubyzip gemを使う場合を考えてみます。その時Gemfileには以下のように書かれるでしょう

Gemfile
gem 'twitter'
gem 'rubyzip'

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

  • rails 6, 2.4.17
  • rails 7.0.6, bundler 2.4.16
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ガイドでも明確に書かれています。

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

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

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)のおかげ です。詳細は以下の記事などご参照ください。

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

はい、ただしうまくrequireされないパターンがあります。これは 実際にrequireしている箇所を見れば明らか です。1

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していることを抜粋すると

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を起動すると

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) となりました。

これを回避するには ソースコード にもドキュメント にも書いてありますが gem 'rubyzip', require: 'zip' とrequireで名前を指定してあげればよいです。

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
  1. Qiitaってgithubコードの自動展開まだできないのですね。。。残念。

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?