Bug #21830
closedRuby::Box and Kernel#require
Description
If you enable Ruby::Box, decorations to Kernel#require do not work.
For example, given foo.rb:
the decoration works normally:
% ruby -v foo.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25]
decorated
true
but it does not with the feature enabled:
% RUBY_BOX=1 ruby -v foo.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25]
ruby: warning: Ruby::Box is experimental, and the behavior may change in the future!
See https://docs.ruby-lang.org/en/4.0/Ruby/Box.html for known issues, etc.
true
With this, Zeitwerk cannot work because it totally depends on a decoration of Kernel#require (done here).
Bootsnap also depends on the ability of decorating Kernel#require (done here).
On one hand, these gems are walking a fine line in the border of the public interface of Ruby. On the other hand, it is a goal of boxes to be transparent to the code being loaded under them.
@tagomoris (Satoshi Tagomori) (Satoshi Tagomori) what should be the way to go in your opinion?
/cc @byroot (Jean Boussier) (Jean Boussier)
Updated by fxn (Xavier Noria) 8 months ago
- Description updated (diff)
Updated by fxn (Xavier Noria) 8 months ago
ยท Edited
Just in case it matters, let me also add that it is assumed that Module#autoload invokes Kernel#require as introduced in cd465d5.
Zeitwerk relies on this too, because the main intercepted require calls are coming from autoloads set by autoloaders.
Updated by tagomoris (Satoshi Tagomori) 8 months ago
- Assignee set to tagomoris (Satoshi Tagomori)
Updated by tagomoris (Satoshi Tagomori) 7 months ago
I'll work on #21881 to solve this problem.
Updated by hsbt (Hiroshi SHIBATA) 10 days ago
The direct Kernel#require decoration now works on master (after #21881), but the autoload path is still broken: an autoload fired under RUBY_BOX=1 bypasses the decorated require entirely.
module M
def require(path)
if path == "/nonexistent/virtual"
Object.const_set(:A, Module.new)
return true
end
super
end
end
Kernel.prepend(M)
Object.autoload(:A, "/nonexistent/virtual")
A # defined without RUBY_BOX, LoadError with RUBY_BOX=1
Root cause: autoload_feature_require in variable.c replaces the receiver with the registered box object when boxes are enabled, so the call dispatches to Ruby::Box#require, which goes straight to rb_require_string and skips everything prepended to Kernel in that box. This breaks Zeitwerk's implicit namespaces: with RUBY_BOX=1, a plain Rails 8.1 app boots but the first request fails with LoadError: cannot load such file -- .../turbo-rails-2.0.23/app/channels/turbo/streams.
I opened https://github.com/ruby/ruby/pull/18534 to fix this. It calls require on the registered box's top self in a frame running in that box, so the box's decorated require is dispatched and the feature is still loaded into the registering box. With it, RUBY_BOX=1 rails server serves requests.
Updated by fxn (Xavier Noria) 9 days ago
Thanks @hsbt (Hiroshi SHIBATA).
For the archives, I'd like to add something to
This breaks Zeitwerk's implicit namespaces
The main idea in Zeitwerk is:
- Scan the filesystem
- Set autoloads accordingly
- React to constants being autoloded
The Kernel#require decoration takes care of (3). In that decoration, we update internal state, we trigger user on load callbacks, etc.
So, while it is correct that implicit namespaces won't work, the implications are more dramatic: Zeitwerk in general won't work, callbacks won't fire, reloading won't know what to reload, etc. If the decoration is not in place the gem is generally broken.
Updated by hsbt (Hiroshi SHIBATA) 9 days ago
- Related to Misc #22275: Ruby::Box support plan for RubyGems and Bundler added
Updated by hsbt (Hiroshi SHIBATA) 6 days ago
- Status changed from Open to Closed
@fxn (Xavier Noria) Thank you for your following up. I merged https://github.com/ruby/ruby/pull/18534, so rails s is now working with RUBY_BOX=1.
I'm traking the support status of RubyGems and Ruby::Box at https://bugs.ruby-lang.org/issues/22275