fxn/zeitwerk

[Question] How to use zeitwerk correctly in the gem entry file?

Closed this issue · 5 comments

I am writing a code referring to here in the document.
https://github.com/fxn/zeitwerk#autoloading

# lib/my_gem.rb (main file)
require "bundler/inline"

gemfile do
  source "https://rubygems.org"
  gem "zeitwerk"
end

require "pathname"
Pathname("my_gem").mkpath
Pathname("my_gem/my_logger.rb").write("MyGem::MyLogger = 'OK'")

require "zeitwerk"
loader = Zeitwerk::Loader.for_gem
loader.setup

module MyGem
  p MyLogger
end

Execute the above code to get the following error:

ruby my_gem.rb
WARNING: Zeitwerk defines the constant MyGem after the directory

    ./my_gem

To prevent that, please configure the loader to ignore it:

    loader.ignore("#{__dir__}/my_gem")

Otherwise, there is a flag to silence this warning:

    Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
WARNING: Zeitwerk defines the constant MyGem after the file

    ./my_gem.rb

To prevent that, please configure the loader to ignore it:

    loader.ignore("#{__dir__}/my_gem.rb")

Otherwise, there is a flag to silence this warning:

    Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
/usr/local/var/rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/zeitwerk-2.6.6/lib/zeitwerk/loader.rb:471:in `block (3 levels) in raise_if_conflicting_directory': loader (Zeitwerk::Error)

#<Zeitwerk::GemLoader:0x000000010c7f4768
 @autoloaded_dirs=[],
 @autoloads={},
 @collapse_dirs=#<Set: {}>,
 @collapse_glob_patterns=#<Set: {}>,
 @eager_load_exclusions=#<Set: {}>,
 @eager_loaded=false,
 @ignored_glob_patterns=#<Set: {}>,
 @ignored_paths=#<Set: {}>,
 @inflector=#<Zeitwerk::GemInflector:0x000000010c7f4010 @version_file="/private/tmp/lib/my_gem/version.rb">,
 @initialized_at=2022-11-30 12:07:19.210589 +0900,
 @lib="/private/tmp/lib",
 @logger=nil,
 @mutex=#<Thread::Mutex:0x000000010c7f4128>,
 @mutex2=#<Thread::Mutex:0x000000010c7f40d8>,
 @namespace_dirs={},
 @on_load_callbacks={},
 @on_setup_callbacks=[],
 @on_unload_callbacks={},
 @reloading_enabled=false,
 @root_file="/private/tmp/lib/my_gem.rb",
 @roots={},
 @setup=false,
 @shadowed_files=#<Set: {}>,
 @tag="my_gem",
 @to_unload={},
 @warn_on_extra_files=true>


wants to manage directory /private/tmp/lib, which is already managed by

#<Zeitwerk::GemLoader:0x000000010c7ad638
 @autoloaded_dirs=[],
 @autoloads={"/private/tmp/lib/my_gem.rb"=>[Object, :MyGem]},
 @collapse_dirs=#<Set: {}>,
 @collapse_glob_patterns=#<Set: {}>,
 @eager_load_exclusions=#<Set: {}>,
 @eager_loaded=false,
 @ignored_glob_patterns=#<Set: {}>,
 @ignored_paths=#<Set: {}>,
 @inflector=#<Zeitwerk::GemInflector:0x000000010c7ac080 @overrides={}, @version_file="./my_gem/version.rb">,
 @initialized_at=2022-11-30 12:07:19.197537 +0900,
 @lib=".",
 @logger=nil,
 @mutex=#<Thread::Mutex:0x000000010c7ac3c8>,
 @mutex2=#<Thread::Mutex:0x000000010c7ac378>,
 @namespace_dirs={"MyGem"=>["/private/tmp/lib/my_gem"]},
 @on_load_callbacks={},
 @on_setup_callbacks=[],
 @on_unload_callbacks={},
 @reloading_enabled=false,
 @root_file="/private/tmp/lib/my_gem.rb",
 @roots={"/private/tmp/lib"=>Object},
 @setup=true,
 @shadowed_files=#<Set: {}>,
 @tag="my_gem",
 @to_unload={},
 @warn_on_extra_files=true>

	from /usr/local/var/rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/zeitwerk-2.6.6/lib/zeitwerk/loader.rb:465:in `each_key'
	from /usr/local/var/rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/zeitwerk-2.6.6/lib/zeitwerk/loader.rb:465:in `block (2 levels) in raise_if_conflicting_directory'
	from /usr/local/var/rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/zeitwerk-2.6.6/lib/zeitwerk/loader.rb:461:in `each'
	from /usr/local/var/rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/zeitwerk-2.6.6/lib/zeitwerk/loader.rb:461:in `block in raise_if_conflicting_directory'
	from /usr/local/var/rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/zeitwerk-2.6.6/lib/zeitwerk/loader.rb:458:in `synchronize'
	from /usr/local/var/rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/zeitwerk-2.6.6/lib/zeitwerk/loader.rb:458:in `raise_if_conflicting_directory'
	from /usr/local/var/rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/zeitwerk-2.6.6/lib/zeitwerk/loader/config.rb:123:in `push_dir'
	from /usr/local/var/rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/zeitwerk-2.6.6/lib/zeitwerk/gem_loader.rb:26:in `initialize'
	from /usr/local/var/rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/zeitwerk-2.6.6/lib/zeitwerk/gem_loader.rb:13:in `new'
	from /usr/local/var/rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/zeitwerk-2.6.6/lib/zeitwerk/gem_loader.rb:13:in `_new'
	from /usr/local/var/rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/zeitwerk-2.6.6/lib/zeitwerk/registry.rb:90:in `loader_for_gem'
	from /usr/local/var/rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/zeitwerk-2.6.6/lib/zeitwerk/loader.rb:286:in `for_gem'
	from /private/tmp/lib/my_gem.rb:14:in `<top (required)>'
	from /usr/local/var/rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/zeitwerk-2.6.6/lib/zeitwerk/kernel.rb:30:in `require'
	from /usr/local/var/rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/zeitwerk-2.6.6/lib/zeitwerk/kernel.rb:30:in `require'
	from my_gem.rb:17:in `<main>'

Compilation exited abnormally with code 1 at Wed Nov 30 12:07:19

If I change the Loader part as follows, the error will not occur.

require "zeitwerk"
loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
loader.ignore("#{__dir__}/my_gem.rb")
loader.setup

As a way to set up zeitwerk in gem entry file,
Is it wrong to use it?
Or is this okay?

fxn commented

The entrypoint should be a file my_gem.rb at the same level that the my_gem directory.

So, it would loook like this:

lib/my_gem.rb
lib/my_gem/my_logger.rb

@fxn Thank you for your reply.
Certainly my_gem.rb is at the same level as my_gem directory.

/tmp/lib $ exa -T
.
├── my_gem
│  └── my_logger.rb
└── my_gem.rb

/tmp/lib $ find
.
./my_gem.rb
./my_gem
./my_gem/my_logger.rb

/tmp/lib $ ruby my_gem.rb
(The error is displayed here)

Is it strange somewhere?

fxn commented

Ah! I see.

So, the problem is that gem entrypoints are expected to be required:

% ruby -I. -rmy_gem -e1
"OK"

Thanks!
I understanded that it should not be executed directly even with entrypoint.
If I called by require from parent directory, it worked correctly.

# /tmp/a.rb
$LOAD_PATH.unshift("lib")
require "my_gem"
$ ruby a.rb
"OK"
fxn commented

Exactly, if you require the entrypoint from any client code, it will work.

This is an inception in which the file that defines the root namespace of the gem is being executed, even before the loader is even instantiated, and before the constant is defined. But the convenience of for_gem, so easy to use, wins over the edge case and Zeitwerk is a bit more complicated internally to make it work.