Skip to content

Conversation

@schneems
Copy link
Collaborator

How does require_relative work

The require_relative method is different from other monkey patches because the code uses the caller location to change behavior. That means if you monkey patch that method and alias it back to the original require_relative it will not work as expected. Here is the c-code for require_relative https://github.com/ruby/ruby/blob/b35b7a1ef25347735a6bb7c28ab7e77afea1d856/load.c#L907-L924.

The way that bootsnap (and others) work around this limitation is by grabbing the second to last caller location and requiring relative to that file instead. This method uses the Thread::Backtrace::Location class:

caller_locations(1..1).first.class
=> Thread::Backtrace::Location

What changed in 3.1

This change in ruby ruby/ruby#4519 changed the behavior of that class when being returned against code with an eval. It appears that previously the behavior was to return the path of the file where the eval was defined. The new behavior is to return a nil, and then that would be used to implement the behavior described:

if (!eval_default_path) {
    eval_default_path = rb_fstring_lit("(eval)");
    rb_gc_register_mark_object(eval_default_path);
}
fname = eval_default_path;

That behavior had the side effect of breaking gems that were using Thread::Backtrace::Location#absolute_path to implement require_relative in their monkey patch (such as bootsnap and derailed_benchmarks). The fix is easy enough, while absolute_path returns a nil the path still returns a value for eval code.

This commit

Basically checks if Thread::Backtrace::Location#absolute_path is nil and if so, falls back to Thread::Backtrace::Location#path. The previous online was a little incomprehensible, so I refactored it a bit to highlight what's going on.

I found out about this fix from rails/bootsnap@0d64e7e after realizing that the test suite was failing on Ruby 3.1:

/home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler.rb:663:in `rescue in eval_gemspec':  (Bundler::Dsl::DSLError)
[!] There was an error parsing `Gemfile`:
[!] There was an error while loading `dead_end.gemspec`: cannot load such file -- /home/circleci/lib/dead_end/version. Bundler cannot continue.

 #  from /home/circleci/project/dead_end.gemspec:3
 #  -------------------------------------------
 #
 >  require_relative "lib/dead_end/version"
 #
 #  -------------------------------------------
 . Bundler cannot continue.

 #  from /home/circleci/project/Gemfile:6
 #  -------------------------------------------
 #  # Specify your gem's dependencies in dead_end.gemspec
 >  gemspec
 #
 #  -------------------------------------------
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler.rb:658:in `eval_gemspec'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler.rb:585:in `block in load_gemspec_uncached'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler/shared_helpers.rb:52:in `chdir'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler/shared_helpers.rb:52:in `block in chdir'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler/shared_helpers.rb:51:in `synchronize'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler/shared_helpers.rb:51:in `chdir'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler.rb:584:in `load_gemspec_uncached'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler.rb:570:in `load_gemspec'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler/dsl.rb:66:in `block in gemspec'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler/dsl.rb:66:in `map'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler/dsl.rb:66:in `gemspec'
	from /home/circleci/project/Gemfile:6:in `eval_gemfile'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler/dsl.rb:47:in `instance_eval'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler/dsl.rb:47:in `eval_gemfile'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler/dsl.rb:12:in `evaluate'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler/definition.rb:33:in `build'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler.rb:196:in `definition'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler.rb:144:in `setup'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler/setup.rb:20:in `block in <top (required)>'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler/ui/shell.rb:136:in `with_level'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler/ui/shell.rb:88:in `silence'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler/setup.rb:20:in `<top (required)>'
	from <internal:/home/circleci/.rvm/rubies/ruby-3.1.0-preview1/lib/ruby/3.1.0/rubygems/core_ext/kernel_require.rb>:85:in `require'
	from <internal:/home/circleci/.rvm/rubies/ruby-3.1.0-preview1/lib/ruby/3.1.0/rubygems/core_ext/kernel_require.rb>:85:in `require'
	from /home/circleci/project/lib/dead_end/core_ext.rb:19:in `require'

@schneems schneems force-pushed the schneems/ruby-31-support branch from 2bcd296 to 52a4e3a Compare November 21, 2021 15:09
## How does require_relative work

The `require_relative` method is different from other monkey patches because the code uses the caller location to change behavior. That means if you monkey patch that method and alias it back to the original `require_relative` it will not work as expected. Here is the c-code for `require_relative` https://github.com/ruby/ruby/blob/b35b7a1ef25347735a6bb7c28ab7e77afea1d856/load.c#L907-L924. 

The way that bootsnap (and others) work around this limitation is by grabbing the second to last caller location and requiring relative to that file instead. This method uses the `Thread::Backtrace::Location` class:

```
caller_locations(1..1).first.class
=> Thread::Backtrace::Location
```

## What changed in 3.1

This change in ruby ruby/ruby#4519 changed the behavior of that class when being returned against code with an `eval`. It appears that previously the behavior was to return the path of the file where the eval was defined. The new behavior is to return a nil, and then that would be used to implement the behavior described:


```
if (!eval_default_path) {
    eval_default_path = rb_fstring_lit("(eval)");
    rb_gc_register_mark_object(eval_default_path);
}
fname = eval_default_path;
```

That behavior had the side effect of breaking gems that were using `Thread::Backtrace::Location#absolute_path` to implement `require_relative` in their monkey patch (such as `bootsnap` and `derailed_benchmarks`). The fix is easy enough, while `absolute_path` returns a nil the `path` still returns a value for eval code.

## This commit

Basically checks if `Thread::Backtrace::Location#absolute_path` is nil and if so, falls back to `Thread::Backtrace::Location#path`. The previous online was a little incomprehensible, so I refactored it a bit to highlight what's going on.

I found out about this fix from rails/bootsnap@0d64e7e after realizing that the test suite was failing on Ruby 3.1:

```
/home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler.rb:663:in `rescue in eval_gemspec':  (Bundler::Dsl::DSLError)
[!] There was an error parsing `Gemfile`:
[!] There was an error while loading `dead_end.gemspec`: cannot load such file -- /home/circleci/lib/dead_end/version. Bundler cannot continue.

 #  from /home/circleci/project/dead_end.gemspec:3
 #  -------------------------------------------
 #
 >  require_relative "lib/dead_end/version"
 #
 #  -------------------------------------------
 . Bundler cannot continue.

 #  from /home/circleci/project/Gemfile:6
 #  -------------------------------------------
 #  # Specify your gem's dependencies in dead_end.gemspec
 >  gemspec
 #
 #  -------------------------------------------
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler.rb:658:in `eval_gemspec'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler.rb:585:in `block in load_gemspec_uncached'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler/shared_helpers.rb:52:in `chdir'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler/shared_helpers.rb:52:in `block in chdir'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler/shared_helpers.rb:51:in `synchronize'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler/shared_helpers.rb:51:in `chdir'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler.rb:584:in `load_gemspec_uncached'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler.rb:570:in `load_gemspec'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler/dsl.rb:66:in `block in gemspec'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler/dsl.rb:66:in `map'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler/dsl.rb:66:in `gemspec'
	from /home/circleci/project/Gemfile:6:in `eval_gemfile'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler/dsl.rb:47:in `instance_eval'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler/dsl.rb:47:in `eval_gemfile'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler/dsl.rb:12:in `evaluate'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler/definition.rb:33:in `build'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler.rb:196:in `definition'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler.rb:144:in `setup'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler/setup.rb:20:in `block in <top (required)>'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler/ui/shell.rb:136:in `with_level'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler/ui/shell.rb:88:in `silence'
	from /home/circleci/.rvm/gems/ruby-3.1.0-preview1/gems/bundler-2.2.30/lib/bundler/setup.rb:20:in `<top (required)>'
	from <internal:/home/circleci/.rvm/rubies/ruby-3.1.0-preview1/lib/ruby/3.1.0/rubygems/core_ext/kernel_require.rb>:85:in `require'
	from <internal:/home/circleci/.rvm/rubies/ruby-3.1.0-preview1/lib/ruby/3.1.0/rubygems/core_ext/kernel_require.rb>:85:in `require'
	from /home/circleci/project/lib/dead_end/core_ext.rb:19:in `require'
```
@schneems schneems force-pushed the schneems/ruby-31-support branch from 52a4e3a to 5913e2f Compare November 21, 2021 15:18
@schneems schneems merged commit c9e94db into main Nov 21, 2021
@schneems schneems deleted the schneems/ruby-31-support branch November 21, 2021 15:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants