bootsnap 1.4.4 → 1.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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