Get rid of RealPathCache and the `Kernel.require_relative` decorator
Ref: https://github.com/Shopify/bootsnap/pull/402
Ref: https://bugs.ruby-lang.org/issues/10222
Ref: https://github.com/ruby/ruby/commit/5754f15975d82896c2566ca01b9ce7b3122d67ec
Ref: https://github.com/ruby/ruby/commit/b6d3927e16357408720203a949cfa8741b9ebf6c
Up to Ruby 2.3, `require` would resolve symlinks, but `require_relative` wouldn't:
```ruby
require 'fileutils'
FileUtils.mkdir_p("realpath")
File.write("realpath/a.rb", "p :a_loaded")
File.symlink("realpath", "symlink") rescue nil
$LOAD_PATH.unshift(File.realpath(__dir__) + "/symlink")
require "a.rb" # load symlink/a.rb in 2.3 and older, load realpath/a.rb on 2.4 and newer
require_relative "realpath/a.rb" # noop on 2.4+
```
This would easily cause double loading issue when `require` and `require_relative`
were mixed, but was fixed in 2.4 (https://bugs.ruby-lang.org/issues/10222).
The problem is that `Bootsnap` kinda negated this fix, because `realpath()`
wouldn't be applied to absolute paths:
```ruby
require 'fileutils'
FileUtils.mkdir_p("realpath")
File.write("realpath/a.rb", "p :a_loaded")
File.symlink("realpath", "symlink") rescue nil
$LOAD_PATH.unshift(File.realpath(__dir__) + "/symlink")
require File.expand_path("symlink/a.rb") # load symlink/a.rb in 3.0 and older, load realpath/a.rb on 3.1 and newer
require_relative "realpath/a.rb" # noop on 3.1+
```
And for performance reasons, Bootsnap tried really hard not to call `realpath`,
as it's a syscall, instead it used `expand_path`, which is entirely in use
space and doesn't reach to the file system. So if you had a `symlink` in
`$LOAD_PATH`, `bootcsnap` would perpetuate this bug, which led to the
addition of https://github.com/Shopify/bootsnap/pull/136.
This was ultimately fixed in Ruby 3.1 (https://bugs.ruby-lang.org/issues/17885),
now `realpath` is applied even on absolute paths.
While `realpath` is indeed expensive, I think the performance impact is ok if
we only call it for `$LOAD_PATH` members, rather than for all requirable files.
So if you have X gems, it's going to be more or less X `realpath` calls.
It would stay a problem if a gem actually contained symlinks and used
`require_relative`, but it's quite the stretch, and with 3.1 now
handling it, it's not worth keeping such workaround.
See: https://github.com/Shopify/bootsnap/pull/402
2022-02-07 19:49:16 +08:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
require("test_helper")
|
|
|
|
|
|
|
|
module Bootsnap
|
|
|
|
class KernelTest < Minitest::Test
|
2023-06-30 06:59:39 +08:00
|
|
|
include LoadPathCacheHelper
|
Get rid of RealPathCache and the `Kernel.require_relative` decorator
Ref: https://github.com/Shopify/bootsnap/pull/402
Ref: https://bugs.ruby-lang.org/issues/10222
Ref: https://github.com/ruby/ruby/commit/5754f15975d82896c2566ca01b9ce7b3122d67ec
Ref: https://github.com/ruby/ruby/commit/b6d3927e16357408720203a949cfa8741b9ebf6c
Up to Ruby 2.3, `require` would resolve symlinks, but `require_relative` wouldn't:
```ruby
require 'fileutils'
FileUtils.mkdir_p("realpath")
File.write("realpath/a.rb", "p :a_loaded")
File.symlink("realpath", "symlink") rescue nil
$LOAD_PATH.unshift(File.realpath(__dir__) + "/symlink")
require "a.rb" # load symlink/a.rb in 2.3 and older, load realpath/a.rb on 2.4 and newer
require_relative "realpath/a.rb" # noop on 2.4+
```
This would easily cause double loading issue when `require` and `require_relative`
were mixed, but was fixed in 2.4 (https://bugs.ruby-lang.org/issues/10222).
The problem is that `Bootsnap` kinda negated this fix, because `realpath()`
wouldn't be applied to absolute paths:
```ruby
require 'fileutils'
FileUtils.mkdir_p("realpath")
File.write("realpath/a.rb", "p :a_loaded")
File.symlink("realpath", "symlink") rescue nil
$LOAD_PATH.unshift(File.realpath(__dir__) + "/symlink")
require File.expand_path("symlink/a.rb") # load symlink/a.rb in 3.0 and older, load realpath/a.rb on 3.1 and newer
require_relative "realpath/a.rb" # noop on 3.1+
```
And for performance reasons, Bootsnap tried really hard not to call `realpath`,
as it's a syscall, instead it used `expand_path`, which is entirely in use
space and doesn't reach to the file system. So if you had a `symlink` in
`$LOAD_PATH`, `bootcsnap` would perpetuate this bug, which led to the
addition of https://github.com/Shopify/bootsnap/pull/136.
This was ultimately fixed in Ruby 3.1 (https://bugs.ruby-lang.org/issues/17885),
now `realpath` is applied even on absolute paths.
While `realpath` is indeed expensive, I think the performance impact is ok if
we only call it for `$LOAD_PATH` members, rather than for all requirable files.
So if you have X gems, it's going to be more or less X `realpath` calls.
It would stay a problem if a gem actually contained symlinks and used
`require_relative`, but it's quite the stretch, and with 3.1 now
handling it, it's not worth keeping such workaround.
See: https://github.com/Shopify/bootsnap/pull/402
2022-02-07 19:49:16 +08:00
|
|
|
include TmpdirHelper
|
|
|
|
|
|
|
|
def test_require_symlinked_file_twice
|
|
|
|
setup_symlinked_files
|
|
|
|
if RUBY_VERSION >= "3.1"
|
|
|
|
# Fixed in https://github.com/ruby/ruby/commit/79a4484a072e9769b603e7b4fbdb15b1d7eccb15 (Ruby 3.1)
|
|
|
|
assert_both_pass(<<~RUBY)
|
|
|
|
require "symlink/test"
|
|
|
|
require "real/test"
|
|
|
|
RUBY
|
|
|
|
else
|
|
|
|
assert_both_pass(<<~RUBY)
|
|
|
|
require "symlink/test"
|
|
|
|
begin
|
|
|
|
require "real/test"
|
|
|
|
rescue RuntimeError
|
|
|
|
exit 0
|
|
|
|
else
|
|
|
|
exit 1
|
|
|
|
end
|
|
|
|
RUBY
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_require_symlinked_file_twice_aliased
|
|
|
|
setup_symlinked_files
|
|
|
|
assert_both_pass(<<~RUBY)
|
|
|
|
$LOAD_PATH.unshift(File.expand_path("symlink"))
|
|
|
|
require "test"
|
|
|
|
|
|
|
|
$LOAD_PATH.unshift(File.expand_path("a"))
|
|
|
|
require "test"
|
|
|
|
RUBY
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_require_relative_symlinked_file_twice
|
|
|
|
setup_symlinked_files
|
|
|
|
if RUBY_VERSION >= "3.1"
|
|
|
|
# Fixed in https://github.com/ruby/ruby/commit/79a4484a072e9769b603e7b4fbdb15b1d7eccb15 (Ruby 3.1)
|
|
|
|
assert_both_pass(<<~RUBY)
|
|
|
|
require_relative "symlink/test"
|
|
|
|
require_relative "real/test"
|
|
|
|
RUBY
|
|
|
|
else
|
|
|
|
assert_both_pass(<<~RUBY)
|
|
|
|
require_relative "symlink/test"
|
|
|
|
begin
|
|
|
|
require_relative "real/test"
|
|
|
|
rescue RuntimeError
|
|
|
|
exit 0
|
|
|
|
else
|
|
|
|
exit 1
|
|
|
|
end
|
|
|
|
RUBY
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_require_and_then_require_relative_symlinked_file
|
|
|
|
setup_symlinked_files
|
|
|
|
assert_both_pass(<<~RUBY)
|
|
|
|
$LOAD_PATH.unshift(File.expand_path("symlink"))
|
|
|
|
require "test"
|
|
|
|
|
|
|
|
require_relative "real/test"
|
|
|
|
RUBY
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_require_relative_and_then_require_symlinked_file
|
|
|
|
setup_symlinked_files
|
|
|
|
assert_both_pass(<<~RUBY)
|
|
|
|
require_relative "real/test"
|
|
|
|
|
|
|
|
$LOAD_PATH.unshift(File.expand_path("symlink"))
|
|
|
|
require "test"
|
|
|
|
RUBY
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_require_deep_symlinked_file_twice
|
|
|
|
setup_symlinked_files
|
|
|
|
if RUBY_VERSION >= "3.1"
|
|
|
|
# Fixed in https://github.com/ruby/ruby/commit/79a4484a072e9769b603e7b4fbdb15b1d7eccb15 (Ruby 3.1)
|
|
|
|
assert_both_pass(<<~RUBY)
|
|
|
|
require "symlink/dir/deep"
|
|
|
|
require "real/dir/deep"
|
|
|
|
RUBY
|
|
|
|
else
|
|
|
|
assert_both_pass(<<~RUBY)
|
|
|
|
require "symlink/dir/deep"
|
|
|
|
begin
|
|
|
|
require "real/dir/deep"
|
|
|
|
rescue RuntimeError
|
|
|
|
exit 0
|
|
|
|
else
|
|
|
|
exit 1
|
|
|
|
end
|
|
|
|
RUBY
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_require_deep_symlinked_file_twice_aliased
|
|
|
|
setup_symlinked_files
|
|
|
|
assert_both_pass(<<~RUBY)
|
|
|
|
$LOAD_PATH.unshift(File.expand_path("symlink"))
|
|
|
|
require "dir/deep"
|
|
|
|
|
|
|
|
$LOAD_PATH.unshift(File.expand_path("a"))
|
|
|
|
require "dir/deep"
|
|
|
|
RUBY
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_require_relative_deep_symlinked_file_twice
|
|
|
|
setup_symlinked_files
|
|
|
|
if RUBY_VERSION >= "3.1"
|
|
|
|
# Fixed in https://github.com/ruby/ruby/commit/79a4484a072e9769b603e7b4fbdb15b1d7eccb15 (Ruby 3.1)
|
|
|
|
assert_both_pass(<<~RUBY)
|
|
|
|
require_relative "symlink/dir/deep"
|
|
|
|
require_relative "real/dir/deep"
|
|
|
|
RUBY
|
|
|
|
else
|
|
|
|
assert_both_pass(<<~RUBY)
|
|
|
|
require_relative "symlink/dir/deep"
|
|
|
|
begin
|
|
|
|
require_relative "real/dir/deep"
|
|
|
|
rescue RuntimeError
|
|
|
|
exit 0
|
|
|
|
else
|
|
|
|
exit 1
|
|
|
|
end
|
|
|
|
RUBY
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_require_and_then_require_relative_deep_symlinked_file
|
|
|
|
setup_symlinked_files
|
|
|
|
assert_both_pass(<<~RUBY)
|
|
|
|
$LOAD_PATH.unshift(File.expand_path("symlink"))
|
|
|
|
require "dir/deep"
|
|
|
|
|
|
|
|
require_relative "real/dir/deep"
|
|
|
|
RUBY
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_require_relative_and_then_require_deep_symlinked_file
|
|
|
|
setup_symlinked_files
|
|
|
|
assert_both_pass(<<~RUBY)
|
|
|
|
require_relative "real/dir/deep"
|
|
|
|
|
|
|
|
$LOAD_PATH.unshift(File.expand_path("symlink"))
|
|
|
|
require "dir/deep"
|
|
|
|
RUBY
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def assert_both_pass(source)
|
|
|
|
Help.set_file("without_bootsnap.rb", source)
|
|
|
|
unless execute("without_bootsnap.rb", "debug.txt")
|
|
|
|
flunk "expected snippet to pass WITHOUT bootsnap enabled:\n#{debug_output}"
|
|
|
|
end
|
|
|
|
|
|
|
|
Help.set_file("with_bootsnap.rb", %{require "bootsnap/setup"\n#{source}})
|
|
|
|
unless execute("with_bootsnap.rb", "debug.txt")
|
|
|
|
flunk "expected snippet to pass WITH bootsnap enabled:\n#{debug_output}"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def debug_output
|
|
|
|
File.read("debug.txt")
|
|
|
|
rescue Errno::ENOENT
|
|
|
|
end
|
|
|
|
|
|
|
|
def execute(script_path, output_path)
|
|
|
|
system(
|
|
|
|
{"BOOTSNAP_CACHE_DIR" => "tmp/cache"},
|
|
|
|
RbConfig.ruby, "-I.", script_path,
|
|
|
|
out: output_path, err: output_path
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
def assert_successful(source)
|
|
|
|
Help.set_file("test_case.rb", source)
|
|
|
|
|
|
|
|
Help.set_file("test_case.rb", %{require "bootsnap/setup"\n#{source}})
|
2022-01-28 22:40:01 +08:00
|
|
|
assert system({"BOOTSNAP_CACHE_DIR" => "tmp/cache"}, RbConfig.ruby, "-Ilib:.", "test_case.rb")
|
Get rid of RealPathCache and the `Kernel.require_relative` decorator
Ref: https://github.com/Shopify/bootsnap/pull/402
Ref: https://bugs.ruby-lang.org/issues/10222
Ref: https://github.com/ruby/ruby/commit/5754f15975d82896c2566ca01b9ce7b3122d67ec
Ref: https://github.com/ruby/ruby/commit/b6d3927e16357408720203a949cfa8741b9ebf6c
Up to Ruby 2.3, `require` would resolve symlinks, but `require_relative` wouldn't:
```ruby
require 'fileutils'
FileUtils.mkdir_p("realpath")
File.write("realpath/a.rb", "p :a_loaded")
File.symlink("realpath", "symlink") rescue nil
$LOAD_PATH.unshift(File.realpath(__dir__) + "/symlink")
require "a.rb" # load symlink/a.rb in 2.3 and older, load realpath/a.rb on 2.4 and newer
require_relative "realpath/a.rb" # noop on 2.4+
```
This would easily cause double loading issue when `require` and `require_relative`
were mixed, but was fixed in 2.4 (https://bugs.ruby-lang.org/issues/10222).
The problem is that `Bootsnap` kinda negated this fix, because `realpath()`
wouldn't be applied to absolute paths:
```ruby
require 'fileutils'
FileUtils.mkdir_p("realpath")
File.write("realpath/a.rb", "p :a_loaded")
File.symlink("realpath", "symlink") rescue nil
$LOAD_PATH.unshift(File.realpath(__dir__) + "/symlink")
require File.expand_path("symlink/a.rb") # load symlink/a.rb in 3.0 and older, load realpath/a.rb on 3.1 and newer
require_relative "realpath/a.rb" # noop on 3.1+
```
And for performance reasons, Bootsnap tried really hard not to call `realpath`,
as it's a syscall, instead it used `expand_path`, which is entirely in use
space and doesn't reach to the file system. So if you had a `symlink` in
`$LOAD_PATH`, `bootcsnap` would perpetuate this bug, which led to the
addition of https://github.com/Shopify/bootsnap/pull/136.
This was ultimately fixed in Ruby 3.1 (https://bugs.ruby-lang.org/issues/17885),
now `realpath` is applied even on absolute paths.
While `realpath` is indeed expensive, I think the performance impact is ok if
we only call it for `$LOAD_PATH` members, rather than for all requirable files.
So if you have X gems, it's going to be more or less X `realpath` calls.
It would stay a problem if a gem actually contained symlinks and used
`require_relative`, but it's quite the stretch, and with 3.1 now
handling it, it's not worth keeping such workaround.
See: https://github.com/Shopify/bootsnap/pull/402
2022-02-07 19:49:16 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def setup_symlinked_files
|
|
|
|
skip("Platform doesn't support symlinks") unless File.respond_to?(:symlink)
|
|
|
|
|
|
|
|
Help.set_file("real/test.rb", <<-RUBY)
|
|
|
|
if $test_already_required
|
|
|
|
raise "test.rb required more than once"
|
|
|
|
else
|
|
|
|
$test_already_required = true
|
|
|
|
end
|
|
|
|
RUBY
|
|
|
|
|
|
|
|
Help.set_file("real/dir/deep.rb", <<-RUBY)
|
|
|
|
if $deep_already_required
|
|
|
|
raise "deep.rb required more than once"
|
|
|
|
else
|
|
|
|
$deep_already_required = true
|
|
|
|
end
|
|
|
|
RUBY
|
|
|
|
File.symlink("real", "symlink")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|