bootsnap 1.4.4 → 1.9.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +129 -0
  3. data/README.md +46 -15
  4. data/exe/bootsnap +5 -0
  5. data/ext/bootsnap/bootsnap.c +276 -87
  6. data/ext/bootsnap/extconf.rb +20 -14
  7. data/lib/bootsnap/bundler.rb +1 -0
  8. data/lib/bootsnap/cli/worker_pool.rb +135 -0
  9. data/lib/bootsnap/cli.rb +281 -0
  10. data/lib/bootsnap/compile_cache/iseq.rb +51 -11
  11. data/lib/bootsnap/compile_cache/json.rb +79 -0
  12. data/lib/bootsnap/compile_cache/yaml.rb +141 -39
  13. data/lib/bootsnap/compile_cache.rb +14 -4
  14. data/lib/bootsnap/explicit_require.rb +1 -0
  15. data/lib/bootsnap/load_path_cache/cache.rb +47 -26
  16. data/lib/bootsnap/load_path_cache/change_observer.rb +4 -1
  17. data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +18 -20
  18. data/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb +1 -0
  19. data/lib/bootsnap/load_path_cache/loaded_features_index.rb +51 -15
  20. data/lib/bootsnap/load_path_cache/path.rb +3 -2
  21. data/lib/bootsnap/load_path_cache/path_scanner.rb +50 -26
  22. data/lib/bootsnap/load_path_cache/realpath_cache.rb +5 -5
  23. data/lib/bootsnap/load_path_cache/store.rb +39 -15
  24. data/lib/bootsnap/load_path_cache.rb +3 -16
  25. data/lib/bootsnap/setup.rb +2 -36
  26. data/lib/bootsnap/version.rb +2 -1
  27. data/lib/bootsnap.rb +106 -17
  28. metadata +18 -32
  29. data/.github/CODEOWNERS +0 -2
  30. data/.github/probots.yml +0 -2
  31. data/.gitignore +0 -17
  32. data/.rubocop.yml +0 -20
  33. data/.travis.yml +0 -21
  34. data/CODE_OF_CONDUCT.md +0 -74
  35. data/CONTRIBUTING.md +0 -21
  36. data/Gemfile +0 -8
  37. data/README.jp.md +0 -231
  38. data/Rakefile +0 -12
  39. data/bin/ci +0 -10
  40. data/bin/console +0 -14
  41. data/bin/setup +0 -8
  42. data/bin/test-minimal-support +0 -7
  43. data/bin/testunit +0 -8
  44. data/bootsnap.gemspec +0 -45
  45. data/dev.yml +0 -10
  46. data/lib/bootsnap/load_path_cache/core_ext/active_support.rb +0 -106
  47. data/shipit.rubygems.yml +0 -0
@@ -1,61 +1,163 @@
1
+ # frozen_string_literal: true
1
2
  require('bootsnap/bootsnap')
2
3
 
3
4
  module Bootsnap
4
5
  module CompileCache
5
6
  module YAML
6
7
  class << self
7
- attr_accessor(:msgpack_factory)
8
- end
8
+ attr_accessor(:msgpack_factory, :cache_dir, :supported_options)
9
9
 
10
- def self.input_to_storage(contents, _)
11
- raise(Uncompilable) if contents.index("!ruby/object")
12
- obj = ::YAML.load(contents)
13
- msgpack_factory.packer.write(obj).to_s
14
- rescue NoMethodError, RangeError
15
- # if the object included things that we can't serialize, fall back to
16
- # Marshal. It's a bit slower, but can encode anything yaml can.
17
- # NoMethodError is unexpected types; RangeError is Bignums
18
- Marshal.dump(obj)
19
- end
10
+ def input_to_storage(contents, _)
11
+ obj = strict_load(contents)
12
+ msgpack_factory.dump(obj)
13
+ rescue NoMethodError, RangeError
14
+ # The object included things that we can't serialize
15
+ raise(Uncompilable)
16
+ end
20
17
 
21
- def self.storage_to_output(data)
22
- # This could have a meaning in messagepack, and we're being a little lazy
23
- # about it. -- but a leading 0x04 would indicate the contents of the YAML
24
- # is a positive integer, which is rare, to say the least.
25
- if data[0] == 0x04.chr && data[1] == 0x08.chr
26
- Marshal.load(data)
27
- else
28
- msgpack_factory.unpacker.feed(data).read
18
+ def storage_to_output(data, kwargs)
19
+ if kwargs && kwargs.key?(:symbolize_names)
20
+ kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names)
21
+ end
22
+ msgpack_factory.load(data, kwargs)
23
+ end
24
+
25
+ def input_to_output(data, kwargs)
26
+ if ::YAML.respond_to?(:unsafe_load)
27
+ ::YAML.unsafe_load(data, **(kwargs || {}))
28
+ else
29
+ ::YAML.load(data, **(kwargs || {}))
30
+ end
31
+ end
32
+
33
+ def strict_load(payload, *args)
34
+ ast = ::YAML.parse(payload)
35
+ return ast unless ast
36
+ strict_visitor.create(*args).visit(ast)
37
+ end
38
+ ruby2_keywords :strict_load if respond_to?(:ruby2_keywords, true)
39
+
40
+ def precompile(path, cache_dir: YAML.cache_dir)
41
+ Bootsnap::CompileCache::Native.precompile(
42
+ cache_dir,
43
+ path.to_s,
44
+ Bootsnap::CompileCache::YAML,
45
+ )
46
+ end
47
+
48
+ def install!(cache_dir)
49
+ self.cache_dir = cache_dir
50
+ init!
51
+ ::YAML.singleton_class.prepend(Patch)
52
+ end
53
+
54
+ def init!
55
+ require('yaml')
56
+ require('msgpack')
57
+ require('date')
58
+
59
+ if Patch.method_defined?(:unsafe_load_file) && !::YAML.respond_to?(:unsafe_load_file)
60
+ Patch.send(:remove_method, :unsafe_load_file)
61
+ end
62
+ if Patch.method_defined?(:load_file) && ::YAML::VERSION >= '4'
63
+ Patch.send(:remove_method, :load_file)
64
+ end
65
+
66
+ # MessagePack serializes symbols as strings by default.
67
+ # We want them to roundtrip cleanly, so we use a custom factory.
68
+ # see: https://github.com/msgpack/msgpack-ruby/pull/122
69
+ factory = MessagePack::Factory.new
70
+ factory.register_type(0x00, Symbol)
71
+
72
+ if defined? MessagePack::Timestamp
73
+ factory.register_type(
74
+ MessagePack::Timestamp::TYPE, # or just -1
75
+ Time,
76
+ packer: MessagePack::Time::Packer,
77
+ unpacker: MessagePack::Time::Unpacker
78
+ )
79
+
80
+ marshal_fallback = {
81
+ packer: ->(value) { Marshal.dump(value) },
82
+ unpacker: ->(payload) { Marshal.load(payload) },
83
+ }
84
+ {
85
+ Date => 0x01,
86
+ Regexp => 0x02,
87
+ }.each do |type, code|
88
+ factory.register_type(code, type, marshal_fallback)
89
+ end
90
+ end
91
+
92
+ self.msgpack_factory = factory
93
+
94
+ self.supported_options = []
95
+ params = ::YAML.method(:load).parameters
96
+ if params.include?([:key, :symbolize_names])
97
+ self.supported_options << :symbolize_names
98
+ end
99
+ if params.include?([:key, :freeze])
100
+ if factory.load(factory.dump('yaml'), freeze: true).frozen?
101
+ self.supported_options << :freeze
102
+ end
103
+ end
104
+ self.supported_options.freeze
29
105
  end
30
- end
31
106
 
32
- def self.input_to_output(data)
33
- ::YAML.load(data)
107
+ def strict_visitor
108
+ self::NoTagsVisitor ||= Class.new(Psych::Visitors::ToRuby) do
109
+ def visit(target)
110
+ if target.tag
111
+ raise Uncompilable, "YAML tags are not supported: #{target.tag}"
112
+ end
113
+ super
114
+ end
115
+ end
116
+ end
34
117
  end
35
118
 
36
- def self.install!(cache_dir)
37
- require('yaml')
38
- require('msgpack')
119
+ module Patch
120
+ def load_file(path, *args)
121
+ return super if args.size > 1
122
+ if kwargs = args.first
123
+ return super unless kwargs.is_a?(Hash)
124
+ return super unless (kwargs.keys - ::Bootsnap::CompileCache::YAML.supported_options).empty?
125
+ end
126
+
127
+ begin
128
+ ::Bootsnap::CompileCache::Native.fetch(
129
+ Bootsnap::CompileCache::YAML.cache_dir,
130
+ File.realpath(path),
131
+ ::Bootsnap::CompileCache::YAML,
132
+ kwargs,
133
+ )
134
+ rescue Errno::EACCES
135
+ ::Bootsnap::CompileCache.permission_error(path)
136
+ end
137
+ end
39
138
 
40
- # MessagePack serializes symbols as strings by default.
41
- # We want them to roundtrip cleanly, so we use a custom factory.
42
- # see: https://github.com/msgpack/msgpack-ruby/pull/122
43
- factory = MessagePack::Factory.new
44
- factory.register_type(0x00, Symbol)
45
- Bootsnap::CompileCache::YAML.msgpack_factory = factory
139
+ ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true)
140
+
141
+ def unsafe_load_file(path, *args)
142
+ return super if args.size > 1
143
+ if kwargs = args.first
144
+ return super unless kwargs.is_a?(Hash)
145
+ return super unless (kwargs.keys - ::Bootsnap::CompileCache::YAML.supported_options).empty?
146
+ end
46
147
 
47
- klass = class << ::YAML; self; end
48
- klass.send(:define_method, :load_file) do |path|
49
148
  begin
50
- Bootsnap::CompileCache::Native.fetch(
51
- cache_dir,
52
- path,
53
- Bootsnap::CompileCache::YAML
149
+ ::Bootsnap::CompileCache::Native.fetch(
150
+ Bootsnap::CompileCache::YAML.cache_dir,
151
+ File.realpath(path),
152
+ ::Bootsnap::CompileCache::YAML,
153
+ kwargs,
54
154
  )
55
155
  rescue Errno::EACCES
56
- Bootsnap::CompileCache.permission_error(path)
156
+ ::Bootsnap::CompileCache.permission_error(path)
57
157
  end
58
158
  end
159
+
160
+ ruby2_keywords :unsafe_load_file if respond_to?(:ruby2_keywords, true)
59
161
  end
60
162
  end
61
163
  end
@@ -1,9 +1,10 @@
1
+ # frozen_string_literal: true
1
2
  module Bootsnap
2
3
  module CompileCache
3
4
  Error = Class.new(StandardError)
4
5
  PermissionError = Class.new(Error)
5
6
 
6
- def self.setup(cache_dir:, iseq:, yaml:)
7
+ def self.setup(cache_dir:, iseq:, yaml:, json:)
7
8
  if iseq
8
9
  if supported?
9
10
  require_relative('compile_cache/iseq')
@@ -21,6 +22,15 @@ module Bootsnap
21
22
  warn("[bootsnap/setup] YAML parsing caching is not supported on this implementation of Ruby")
22
23
  end
23
24
  end
25
+
26
+ if json
27
+ if supported?
28
+ require_relative('compile_cache/json')
29
+ Bootsnap::CompileCache::JSON.install!(cache_dir)
30
+ elsif $VERBOSE
31
+ warn("[bootsnap/setup] JSON parsing caching is not supported on this implementation of Ruby")
32
+ end
33
+ end
24
34
  end
25
35
 
26
36
  def self.permission_error(path)
@@ -28,14 +38,14 @@ module Bootsnap
28
38
  raise(
29
39
  PermissionError,
30
40
  "bootsnap doesn't have permission to write cache entries in '#{cpath}' " \
31
- "(or, less likely, doesn't have permisison to read '#{path}')",
41
+ "(or, less likely, doesn't have permission to read '#{path}')",
32
42
  )
33
43
  end
34
44
 
35
45
  def self.supported?
36
- # only enable on 'ruby' (MRI), POSIX (darwin, linux, *bsd), and >= 2.3.0
46
+ # only enable on 'ruby' (MRI), POSIX (darwin, linux, *bsd), Windows (RubyInstaller2) and >= 2.3.0
37
47
  RUBY_ENGINE == 'ruby' &&
38
- RUBY_PLATFORM =~ /darwin|linux|bsd/ &&
48
+ RUBY_PLATFORM =~ /darwin|linux|bsd|mswin|mingw|cygwin/ &&
39
49
  Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.3.0")
40
50
  end
41
51
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Bootsnap
2
3
  module ExplicitRequire
3
4
  ARCHDIR = RbConfig::CONFIG['archdir']
@@ -10,8 +10,8 @@ module Bootsnap
10
10
  def initialize(store, path_obj, development_mode: false)
11
11
  @development_mode = development_mode
12
12
  @store = store
13
- @mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new # TODO: Remove once Ruby 2.2 support is dropped.
14
- @path_obj = path_obj.map! { |f| File.exist?(f) ? File.realpath(f) : f }
13
+ @mutex = Mutex.new
14
+ @path_obj = path_obj.map! { |f| PathScanner.os_path(File.exist?(f) ? File.realpath(f) : f.dup) }
15
15
  @has_relative_paths = nil
16
16
  reinitialize
17
17
  end
@@ -44,14 +44,20 @@ module Bootsnap
44
44
 
45
45
  # Try to resolve this feature to an absolute path without traversing the
46
46
  # loadpath.
47
- def find(feature)
47
+ def find(feature, try_extensions: true)
48
48
  reinitialize if (@has_relative_paths && dir_changed?) || stale?
49
- feature = feature.to_s
50
- return feature if absolute_path?(feature)
51
- return expand_path(feature) if feature.start_with?('./')
49
+ feature = feature.to_s.freeze
50
+
51
+ return feature if Bootsnap.absolute_path?(feature)
52
+
53
+ if feature.start_with?('./', '../')
54
+ return try_extensions ? expand_path(feature) : File.expand_path(feature).freeze
55
+ end
56
+
52
57
  @mutex.synchronize do
53
- x = search_index(feature)
58
+ x = search_index(feature, try_extensions: try_extensions)
54
59
  return x if x
60
+ return unless try_extensions
55
61
 
56
62
  # Ruby has some built-in features that require lies about.
57
63
  # For example, 'enumerator' is built in. If you require it, ruby
@@ -67,7 +73,7 @@ module Bootsnap
67
73
  # native dynamic extension, e.g. .bundle or .so), we know it was a
68
74
  # failure and there's nothing more we can do to find the file.
69
75
  # no extension, .rb, (.bundle or .so)
70
- when '', *CACHED_EXTENSIONS # rubocop:disable Performance/CaseWhenSplat
76
+ when '', *CACHED_EXTENSIONS
71
77
  nil
72
78
  # Ruby allows specifying native extensions as '.so' even when DLEXT
73
79
  # is '.bundle'. This is where we handle that case.
@@ -92,16 +98,6 @@ module Bootsnap
92
98
  raise(LoadPathCache::FallbackScan, '', []) if @development_mode
93
99
  end
94
100
 
95
- if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
96
- def absolute_path?(path)
97
- path[1] == ':'
98
- end
99
- else
100
- def absolute_path?(path)
101
- path.start_with?(SLASH)
102
- end
103
- end
104
-
105
101
  def unshift_paths(sender, *paths)
106
102
  return unless sender == @path_obj
107
103
  @mutex.synchronize { unshift_paths_locked(*paths) }
@@ -144,7 +140,7 @@ module Bootsnap
144
140
  expanded_path = p.expanded_path
145
141
  entries, dirs = p.entries_and_dirs(@store)
146
142
  # push -> low precedence -> set only if unset
147
- dirs.each { |dir| @dirs[dir] ||= path }
143
+ dirs.each { |dir| @dirs[dir] ||= path }
148
144
  entries.each { |rel| @index[rel] ||= expanded_path }
149
145
  end
150
146
  end
@@ -177,16 +173,24 @@ module Bootsnap
177
173
  end
178
174
 
179
175
  if DLEXT2
180
- def search_index(f)
181
- try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f + DLEXT2) || try_index(f)
176
+ def search_index(f, try_extensions: true)
177
+ if try_extensions
178
+ try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f + DLEXT2) || try_index(f)
179
+ else
180
+ try_index(f)
181
+ end
182
182
  end
183
183
 
184
184
  def maybe_append_extension(f)
185
185
  try_ext(f + DOT_RB) || try_ext(f + DLEXT) || try_ext(f + DLEXT2) || f
186
186
  end
187
187
  else
188
- def search_index(f)
189
- try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f)
188
+ def search_index(f, try_extensions: true)
189
+ if try_extensions
190
+ try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f)
191
+ else
192
+ try_index(f)
193
+ end
190
194
  end
191
195
 
192
196
  def maybe_append_extension(f)
@@ -194,9 +198,26 @@ module Bootsnap
194
198
  end
195
199
  end
196
200
 
197
- def try_index(f)
198
- if (p = @index[f])
199
- p + '/' + f
201
+ s = rand.to_s.force_encoding(Encoding::US_ASCII).freeze
202
+ if s.respond_to?(:-@)
203
+ if (-s).equal?(s) && (-s.dup).equal?(s) || RUBY_VERSION >= '2.7'
204
+ def try_index(f)
205
+ if (p = @index[f])
206
+ -(File.join(p, f).freeze)
207
+ end
208
+ end
209
+ else
210
+ def try_index(f)
211
+ if (p = @index[f])
212
+ -File.join(p, f).untaint
213
+ end
214
+ end
215
+ end
216
+ else
217
+ def try_index(f)
218
+ if (p = @index[f])
219
+ File.join(p, f)
220
+ end
200
221
  end
201
222
  end
202
223
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Bootsnap
2
3
  module LoadPathCache
3
4
  module ChangeObserver
@@ -14,18 +15,20 @@ module Bootsnap
14
15
  @lpc_observer.push_paths(self, *entries.map(&:to_s))
15
16
  super
16
17
  end
18
+ alias_method :append, :push
17
19
 
18
20
  def unshift(*entries)
19
21
  @lpc_observer.unshift_paths(self, *entries.map(&:to_s))
20
22
  super
21
23
  end
24
+ alias_method :prepend, :unshift
22
25
 
23
26
  def concat(entries)
24
27
  @lpc_observer.push_paths(self, *entries.map(&:to_s))
25
28
  super
26
29
  end
27
30
 
28
- # uniq! keeps the first occurance of each path, otherwise preserving
31
+ # uniq! keeps the first occurrence of each path, otherwise preserving
29
32
  # order, preserving the effective load path
30
33
  def uniq!(*args)
31
34
  ret = super
@@ -1,8 +1,9 @@
1
+ # frozen_string_literal: true
1
2
  module Bootsnap
2
3
  module LoadPathCache
3
4
  module CoreExt
4
5
  def self.make_load_error(path)
5
- err = LoadError.new("cannot load such file -- #{path}")
6
+ err = LoadError.new(+"cannot load such file -- #{path}")
6
7
  err.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
7
8
  err.define_singleton_method(:path) { path }
8
9
  err
@@ -37,36 +38,29 @@ module Kernel
37
38
  rescue Bootsnap::LoadPathCache::ReturnFalse
38
39
  false
39
40
  rescue Bootsnap::LoadPathCache::FallbackScan
40
- require_with_bootsnap_lfi(path)
41
+ fallback = true
42
+ ensure
43
+ if fallback
44
+ require_with_bootsnap_lfi(path)
45
+ end
41
46
  end
42
47
 
43
48
  alias_method(:require_relative_without_bootsnap, :require_relative)
44
49
  def require_relative(path)
50
+ location = caller_locations(1..1).first
45
51
  realpath = Bootsnap::LoadPathCache.realpath_cache.call(
46
- caller_locations(1..1).first.absolute_path, path
52
+ location.absolute_path || location.path, path
47
53
  )
48
54
  require(realpath)
49
55
  end
50
56
 
51
57
  alias_method(:load_without_bootsnap, :load)
52
58
  def load(path, wrap = false)
53
- if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path))
54
- return load_without_bootsnap(resolved, wrap)
55
- end
56
-
57
- # load also allows relative paths from pwd even when not in $:
58
- if File.exist?(relative = File.expand_path(path))
59
- return load_without_bootsnap(relative, wrap)
59
+ if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path, try_extensions: false))
60
+ load_without_bootsnap(resolved, wrap)
61
+ else
62
+ load_without_bootsnap(path, wrap)
60
63
  end
61
-
62
- raise(Bootsnap::LoadPathCache::CoreExt.make_load_error(path))
63
- rescue LoadError => e
64
- e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
65
- raise(e)
66
- rescue Bootsnap::LoadPathCache::ReturnFalse
67
- false
68
- rescue Bootsnap::LoadPathCache::FallbackScan
69
- load_without_bootsnap(path, wrap)
70
64
  end
71
65
  end
72
66
 
@@ -87,6 +81,10 @@ class Module
87
81
  rescue Bootsnap::LoadPathCache::ReturnFalse
88
82
  false
89
83
  rescue Bootsnap::LoadPathCache::FallbackScan
90
- autoload_without_bootsnap(const, path)
84
+ fallback = true
85
+ ensure
86
+ if fallback
87
+ autoload_without_bootsnap(const, path)
88
+ end
91
89
  end
92
90
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  class << $LOADED_FEATURES
2
3
  alias_method(:delete_without_bootsnap, :delete)
3
4
  def delete(key)
@@ -26,7 +26,7 @@ module Bootsnap
26
26
  class LoadedFeaturesIndex
27
27
  def initialize
28
28
  @lfi = {}
29
- @mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new # TODO: Remove once Ruby 2.2 support is dropped.
29
+ @mutex = Mutex.new
30
30
 
31
31
  # In theory the user could mutate $LOADED_FEATURES and invalidate our
32
32
  # cache. If this ever comes up in practice — or if you, the
@@ -40,7 +40,7 @@ module Bootsnap
40
40
  # /a/b/lib/my/foo.rb
41
41
  # ^^^^^^^^^
42
42
  short = feat[(lpe.length + 1)..-1]
43
- stripped = strip_extension(short)
43
+ stripped = strip_extension_if_elidable(short)
44
44
  @lfi[short] = hash
45
45
  @lfi[stripped] = hash
46
46
  end
@@ -58,9 +58,9 @@ module Bootsnap
58
58
  end
59
59
 
60
60
  def purge_multi(features)
61
- rejected_hashes = features.map(&:hash).to_set
61
+ rejected_hashes = features.each_with_object({}) { |f, h| h[f.hash] = true }
62
62
  @mutex.synchronize do
63
- @lfi.reject! { |_, hash| rejected_hashes.include?(hash) }
63
+ @lfi.reject! { |_, hash| rejected_hashes.key?(hash) }
64
64
  end
65
65
  end
66
66
 
@@ -83,24 +83,38 @@ module Bootsnap
83
83
  # 2. Inspect $LOADED_FEATURES upon return from yield to find the matching
84
84
  # entry.
85
85
  def register(short, long = nil)
86
+ # Absolute paths are not a concern.
87
+ if Bootsnap.absolute_path?(short.to_s)
88
+ return yield
89
+ end
90
+
86
91
  if long.nil?
87
- pat = %r{/#{Regexp.escape(short)}(\.[^/]+)?$}
88
92
  len = $LOADED_FEATURES.size
89
93
  ret = yield
90
- long = $LOADED_FEATURES[len..-1].detect { |feat| feat =~ pat }
94
+ long = $LOADED_FEATURES[len..-1].detect do |feat|
95
+ offset = 0
96
+ while offset = feat.index(short, offset)
97
+ if feat.index(".", offset + 1) && !feat.index("/", offset + 2)
98
+ break true
99
+ else
100
+ offset += 1
101
+ end
102
+ end
103
+ end
91
104
  else
92
105
  ret = yield
93
106
  end
94
107
 
95
108
  hash = long.hash
96
109
 
97
- # do we have 'bundler' or 'bundler.rb'?
98
- altname = if File.extname(short) != ''
99
- # strip the path from 'bundler.rb' -> 'bundler'
100
- strip_extension(short)
101
- elsif long && (ext = File.extname(long))
102
- # get the extension from the expanded path if given
103
- # 'bundler' + '.rb'
110
+ # Do we have a filename with an elidable extension, e.g.,
111
+ # 'bundler.rb', or 'libgit2.so'?
112
+ altname = if extension_elidable?(short)
113
+ # Strip the extension off, e.g. 'bundler.rb' -> 'bundler'.
114
+ strip_extension_if_elidable(short)
115
+ elsif long && (ext = File.extname(long.freeze))
116
+ # We already know the extension of the actual file this
117
+ # resolves to, so put that back on.
104
118
  short + ext
105
119
  end
106
120
 
@@ -117,8 +131,30 @@ module Bootsnap
117
131
  STRIP_EXTENSION = /\.[^.]*?$/
118
132
  private_constant(:STRIP_EXTENSION)
119
133
 
120
- def strip_extension(f)
121
- f.sub(STRIP_EXTENSION, '')
134
+ # Might Ruby automatically search for this extension if
135
+ # someone tries to 'require' the file without it? E.g. Ruby
136
+ # will implicitly try 'x.rb' if you ask for 'x'.
137
+ #
138
+ # This is complex and platform-dependent, and the Ruby docs are a little
139
+ # handwavy about what will be tried when and in what order.
140
+ # So optimistically pretend that all known elidable extensions
141
+ # will be tried on all platforms, and that people are unlikely
142
+ # to name files in a way that assumes otherwise.
143
+ # (E.g. It's unlikely that someone will know that their code
144
+ # will _never_ run on MacOS, and therefore think they can get away
145
+ # with calling a Ruby file 'x.dylib.rb' and then requiring it as 'x.dylib'.)
146
+ #
147
+ # See <https://ruby-doc.org/core-2.6.4/Kernel.html#method-i-require>.
148
+ def extension_elidable?(f)
149
+ f.to_s.end_with?('.rb', '.so', '.o', '.dll', '.dylib')
150
+ end
151
+
152
+ def strip_extension_if_elidable(f)
153
+ if extension_elidable?(f)
154
+ f.sub(STRIP_EXTENSION, '')
155
+ else
156
+ f
157
+ end
122
158
  end
123
159
  end
124
160
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require_relative('path_scanner')
2
3
 
3
4
  module Bootsnap
@@ -20,7 +21,7 @@ module Bootsnap
20
21
  attr_reader(:path)
21
22
 
22
23
  def initialize(path)
23
- @path = path.to_s
24
+ @path = path.to_s.freeze
24
25
  end
25
26
 
26
27
  # True if the path exists, but represents a non-directory object
@@ -59,7 +60,7 @@ module Bootsnap
59
60
  end
60
61
 
61
62
  def expanded_path
62
- File.expand_path(path)
63
+ File.expand_path(path).freeze
63
64
  end
64
65
 
65
66
  private