From 5913e2f0db6a99495970e235bdfa643cd1172728 Mon Sep 17 00:00:00 2001 From: schneems Date: Sun, 21 Nov 2021 08:17:21 -0600 Subject: [PATCH] Support Ruby 3.1 ## 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 https://github.com/ruby/ruby/pull/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 https://github.com/Shopify/bootsnap/commit/0d64e7e5e493884dc6a23a3ac4632d069172c4a1 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 ' 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 `' from :85:in `require' from :85:in `require' from /home/circleci/project/lib/dead_end/core_ext.rb:19:in `require' ``` --- .circleci/config.yml | 14 +++++++++++++- CHANGELOG.md | 1 + lib/dead_end/core_ext.rb | 4 +++- spec/integration/ruby_command_line_spec.rb | 13 ++++++++++--- 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 803997f..f296eb2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,6 +1,6 @@ version: 2.1 orbs: - ruby: circleci/ruby@1.1.2 + ruby: circleci/ruby@1.2.0 references: unit: &unit run: @@ -45,6 +45,17 @@ jobs: - ruby/install-deps - <<: *unit + "ruby-3-1": + docker: + - image: 'cimg/base:stable' + steps: + - checkout + - ruby/install: + version: '3.1.0-preview1' + - run: ruby -v + - ruby/install-deps + - <<: *unit + "lint": docker: - image: circleci/ruby:3.0 @@ -61,4 +72,5 @@ workflows: - "ruby-2-6" - "ruby-2-7" - "ruby-3-0" + - "ruby-3-1" - "lint" diff --git a/CHANGELOG.md b/CHANGELOG.md index ac88977..64b3821 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## HEAD (unreleased) +- Add support for Ruby 3.1 by updating `require_relative` logic (https://github.com/zombocom/dead_end/pull/120) - Requiring `dead_end/auto` is now deprecated please require `dead_end` instead (https://github.com/zombocom/dead_end/pull/119) - Requiring `dead_end/api` now loads code without monkeypatching core extensions (https://github.com/zombocom/dead_end/pull/119) - The interface `DeadEnd.handle_error` is declared public and stable (https://github.com/zombocom/dead_end/pull/119) diff --git a/lib/dead_end/core_ext.rb b/lib/dead_end/core_ext.rb index c89abd5..2886785 100644 --- a/lib/dead_end/core_ext.rb +++ b/lib/dead_end/core_ext.rb @@ -25,7 +25,9 @@ def require_relative(file) if Pathname.new(file).absolute? dead_end_original_require file else - dead_end_original_require File.expand_path("../#{file}", Kernel.caller_locations(1, 1)[0].absolute_path) + relative_from = caller_locations(1..1).first + relative_from_path = relative_from.absolute_path || relative_from.path + dead_end_original_require File.expand_path("../#{file}", relative_from_path) end rescue SyntaxError => e DeadEnd.handle_error(e) diff --git a/spec/integration/ruby_command_line_spec.rb b/spec/integration/ruby_command_line_spec.rb index df82dca..e124287 100644 --- a/spec/integration/ruby_command_line_spec.rb +++ b/spec/integration/ruby_command_line_spec.rb @@ -24,15 +24,22 @@ module DeadEnd Process.wait(d_pid) Process.wait(r_pid) - dead_end_methods_array = dead_end_methods_file.read.strip.lines.map(&:strip) kernel_methods_array = kernel_methods_file.read.strip.lines.map(&:strip) + dead_end_methods_array = dead_end_methods_file.read.strip.lines.map(&:strip) api_only_methods_array = api_only_methods_file.read.strip.lines.map(&:strip) + # In ruby 3.1.0-preview1 the `timeout` file is already required + # we can remove it if it exists to normalize the output for + # all ruby versions + [dead_end_methods_array, kernel_methods_array, api_only_methods_array].each do |array| + array.delete("timeout") + end + methods = (dead_end_methods_array - kernel_methods_array).sort - expect(methods).to eq(["dead_end_original_load", "dead_end_original_require", "dead_end_original_require_relative", "timeout"]) + expect(methods).to eq(["dead_end_original_load", "dead_end_original_require", "dead_end_original_require_relative"]) methods = (api_only_methods_array - kernel_methods_array).sort - expect(methods).to eq(["timeout"]) + expect(methods).to eq([]) end end