bootsnap 1.4.1 → 1.10.3

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +189 -0
  3. data/LICENSE.txt +1 -1
  4. data/README.md +67 -18
  5. data/exe/bootsnap +5 -0
  6. data/ext/bootsnap/bootsnap.c +319 -119
  7. data/ext/bootsnap/extconf.rb +22 -14
  8. data/lib/bootsnap/bundler.rb +2 -0
  9. data/lib/bootsnap/cli/worker_pool.rb +136 -0
  10. data/lib/bootsnap/cli.rb +281 -0
  11. data/lib/bootsnap/compile_cache/iseq.rb +65 -18
  12. data/lib/bootsnap/compile_cache/json.rb +88 -0
  13. data/lib/bootsnap/compile_cache/yaml.rb +332 -39
  14. data/lib/bootsnap/compile_cache.rb +35 -7
  15. data/lib/bootsnap/explicit_require.rb +5 -3
  16. data/lib/bootsnap/load_path_cache/cache.rb +83 -32
  17. data/lib/bootsnap/load_path_cache/change_observer.rb +6 -1
  18. data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +39 -47
  19. data/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb +12 -0
  20. data/lib/bootsnap/load_path_cache/loaded_features_index.rb +69 -26
  21. data/lib/bootsnap/load_path_cache/path.rb +8 -5
  22. data/lib/bootsnap/load_path_cache/path_scanner.rb +56 -29
  23. data/lib/bootsnap/load_path_cache/realpath_cache.rb +6 -5
  24. data/lib/bootsnap/load_path_cache/store.rb +49 -18
  25. data/lib/bootsnap/load_path_cache.rb +20 -32
  26. data/lib/bootsnap/setup.rb +3 -33
  27. data/lib/bootsnap/version.rb +3 -1
  28. data/lib/bootsnap.rb +126 -36
  29. metadata +15 -97
  30. data/.gitignore +0 -17
  31. data/.rubocop.yml +0 -20
  32. data/.travis.yml +0 -24
  33. data/CODE_OF_CONDUCT.md +0 -74
  34. data/CONTRIBUTING.md +0 -21
  35. data/Gemfile +0 -8
  36. data/README.jp.md +0 -231
  37. data/Rakefile +0 -12
  38. data/bin/ci +0 -10
  39. data/bin/console +0 -14
  40. data/bin/setup +0 -8
  41. data/bin/test-minimal-support +0 -7
  42. data/bin/testunit +0 -8
  43. data/bootsnap.gemspec +0 -45
  44. data/dev.yml +0 -10
  45. data/lib/bootsnap/load_path_cache/core_ext/active_support.rb +0 -100
  46. data/shipit.rubygems.yml +0 -0
@@ -1,56 +1,349 @@
1
- require('bootsnap/bootsnap')
1
+ # frozen_string_literal: true
2
+
3
+ require("bootsnap/bootsnap")
2
4
 
3
5
  module Bootsnap
4
6
  module CompileCache
5
7
  module YAML
8
+ Uncompilable = Class.new(StandardError)
9
+ UnsupportedTags = Class.new(Uncompilable)
10
+
11
+ SUPPORTED_INTERNAL_ENCODINGS = [
12
+ nil, # UTF-8
13
+ Encoding::UTF_8,
14
+ Encoding::ASCII,
15
+ Encoding::BINARY,
16
+ ].freeze
17
+
6
18
  class << self
7
- attr_accessor(:msgpack_factory)
8
- end
19
+ attr_accessor(:msgpack_factory, :supported_options)
20
+ attr_reader(:implementation, :cache_dir)
9
21
 
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
22
+ def cache_dir=(cache_dir)
23
+ @cache_dir = cache_dir.end_with?("/") ? "#{cache_dir}yaml" : "#{cache_dir}-yaml"
24
+ end
25
+
26
+ def precompile(path)
27
+ return false unless CompileCache::YAML.supported_internal_encoding?
28
+
29
+ CompileCache::Native.precompile(
30
+ cache_dir,
31
+ path.to_s,
32
+ @implementation,
33
+ )
34
+ end
35
+
36
+ def install!(cache_dir)
37
+ self.cache_dir = cache_dir
38
+ init!
39
+ ::YAML.singleton_class.prepend(@implementation::Patch)
40
+ end
41
+
42
+ # Psych coerce strings to `Encoding.default_internal` but Message Pack only support
43
+ # UTF-8, US-ASCII and BINARY. So if Encoding.default_internal is set to anything else
44
+ # we can't safely use the cache
45
+ def supported_internal_encoding?
46
+ SUPPORTED_INTERNAL_ENCODINGS.include?(Encoding.default_internal)
47
+ end
48
+
49
+ module EncodingAwareSymbols
50
+ extend self
51
+
52
+ def unpack(payload)
53
+ (+payload).force_encoding(Encoding::UTF_8).to_sym
54
+ end
55
+ end
56
+
57
+ def init!
58
+ require("yaml")
59
+ require("msgpack")
60
+ require("date")
20
61
 
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
62
+ @implementation = ::YAML::VERSION >= "4" ? Psych4 : Psych3
63
+ if @implementation::Patch.method_defined?(:unsafe_load_file) && !::YAML.respond_to?(:unsafe_load_file)
64
+ @implementation::Patch.send(:remove_method, :unsafe_load_file)
65
+ end
66
+
67
+ # MessagePack serializes symbols as strings by default.
68
+ # We want them to roundtrip cleanly, so we use a custom factory.
69
+ # see: https://github.com/msgpack/msgpack-ruby/pull/122
70
+ factory = MessagePack::Factory.new
71
+ factory.register_type(
72
+ 0x00,
73
+ Symbol,
74
+ packer: :to_msgpack_ext,
75
+ unpacker: EncodingAwareSymbols.method(:unpack).to_proc,
76
+ )
77
+
78
+ if defined? MessagePack::Timestamp
79
+ factory.register_type(
80
+ MessagePack::Timestamp::TYPE, # or just -1
81
+ Time,
82
+ packer: MessagePack::Time::Packer,
83
+ unpacker: MessagePack::Time::Unpacker,
84
+ )
85
+
86
+ marshal_fallback = {
87
+ packer: ->(value) { Marshal.dump(value) },
88
+ unpacker: ->(payload) { Marshal.load(payload) },
89
+ }
90
+ {
91
+ Date => 0x01,
92
+ Regexp => 0x02,
93
+ }.each do |type, code|
94
+ factory.register_type(code, type, marshal_fallback)
95
+ end
96
+ end
97
+
98
+ self.msgpack_factory = factory
99
+
100
+ self.supported_options = []
101
+ params = ::YAML.method(:load).parameters
102
+ if params.include?([:key, :symbolize_names])
103
+ supported_options << :symbolize_names
104
+ end
105
+ if params.include?([:key, :freeze])
106
+ if factory.load(factory.dump("yaml"), freeze: true).frozen?
107
+ supported_options << :freeze
108
+ end
109
+ end
110
+ supported_options.freeze
111
+ end
112
+
113
+ def patch
114
+ @implementation::Patch
115
+ end
116
+
117
+ def strict_load(payload)
118
+ ast = ::YAML.parse(payload)
119
+ return ast unless ast
120
+
121
+ strict_visitor.create.visit(ast)
122
+ end
123
+
124
+ def strict_visitor
125
+ self::NoTagsVisitor ||= Class.new(Psych::Visitors::ToRuby) do
126
+ def visit(target)
127
+ if target.tag
128
+ raise UnsupportedTags, "YAML tags are not supported: #{target.tag}"
129
+ end
130
+
131
+ super
132
+ end
133
+ end
29
134
  end
30
135
  end
31
136
 
32
- def self.input_to_output(data)
33
- ::YAML.load(data)
137
+ module Psych4
138
+ extend self
139
+
140
+ def input_to_storage(contents, _)
141
+ obj = SafeLoad.input_to_storage(contents, nil)
142
+ if UNCOMPILABLE.equal?(obj)
143
+ obj = UnsafeLoad.input_to_storage(contents, nil)
144
+ end
145
+ obj
146
+ end
147
+
148
+ module UnsafeLoad
149
+ extend self
150
+
151
+ def input_to_storage(contents, _)
152
+ obj = ::YAML.unsafe_load(contents)
153
+ packer = CompileCache::YAML.msgpack_factory.packer
154
+ packer.pack(false) # not safe loaded
155
+ begin
156
+ packer.pack(obj)
157
+ rescue NoMethodError, RangeError
158
+ return UNCOMPILABLE # The object included things that we can't serialize
159
+ end
160
+ packer.to_s
161
+ end
162
+
163
+ def storage_to_output(data, kwargs)
164
+ if kwargs&.key?(:symbolize_names)
165
+ kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names)
166
+ end
167
+
168
+ unpacker = CompileCache::YAML.msgpack_factory.unpacker(kwargs)
169
+ unpacker.feed(data)
170
+ _safe_loaded = unpacker.unpack
171
+ unpacker.unpack
172
+ end
173
+
174
+ def input_to_output(data, kwargs)
175
+ ::YAML.unsafe_load(data, **(kwargs || {}))
176
+ end
177
+ end
178
+
179
+ module SafeLoad
180
+ extend self
181
+
182
+ def input_to_storage(contents, _)
183
+ obj = begin
184
+ CompileCache::YAML.strict_load(contents)
185
+ rescue Psych::DisallowedClass, Psych::BadAlias, Uncompilable
186
+ return UNCOMPILABLE
187
+ end
188
+
189
+ packer = CompileCache::YAML.msgpack_factory.packer
190
+ packer.pack(true) # safe loaded
191
+ begin
192
+ packer.pack(obj)
193
+ rescue NoMethodError, RangeError
194
+ return UNCOMPILABLE
195
+ end
196
+ packer.to_s
197
+ end
198
+
199
+ def storage_to_output(data, kwargs)
200
+ if kwargs&.key?(:symbolize_names)
201
+ kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names)
202
+ end
203
+
204
+ unpacker = CompileCache::YAML.msgpack_factory.unpacker(kwargs)
205
+ unpacker.feed(data)
206
+ safe_loaded = unpacker.unpack
207
+ if safe_loaded
208
+ unpacker.unpack
209
+ else
210
+ UNCOMPILABLE
211
+ end
212
+ end
213
+
214
+ def input_to_output(data, kwargs)
215
+ ::YAML.load(data, **(kwargs || {}))
216
+ end
217
+ end
218
+
219
+ module Patch
220
+ def load_file(path, *args)
221
+ return super unless CompileCache::YAML.supported_internal_encoding?
222
+
223
+ return super if args.size > 1
224
+
225
+ if (kwargs = args.first)
226
+ return super unless kwargs.is_a?(Hash)
227
+ return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty?
228
+ end
229
+
230
+ begin
231
+ CompileCache::Native.fetch(
232
+ CompileCache::YAML.cache_dir,
233
+ File.realpath(path),
234
+ CompileCache::YAML::Psych4::SafeLoad,
235
+ kwargs,
236
+ )
237
+ rescue Errno::EACCES
238
+ CompileCache.permission_error(path)
239
+ end
240
+ end
241
+
242
+ ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true)
243
+
244
+ def unsafe_load_file(path, *args)
245
+ return super unless CompileCache::YAML.supported_internal_encoding?
246
+
247
+ return super if args.size > 1
248
+
249
+ if (kwargs = args.first)
250
+ return super unless kwargs.is_a?(Hash)
251
+ return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty?
252
+ end
253
+
254
+ begin
255
+ CompileCache::Native.fetch(
256
+ CompileCache::YAML.cache_dir,
257
+ File.realpath(path),
258
+ CompileCache::YAML::Psych4::UnsafeLoad,
259
+ kwargs,
260
+ )
261
+ rescue Errno::EACCES
262
+ CompileCache.permission_error(path)
263
+ end
264
+ end
265
+
266
+ ruby2_keywords :unsafe_load_file if respond_to?(:ruby2_keywords, true)
267
+ end
34
268
  end
35
269
 
36
- def self.install!(cache_dir)
37
- require('yaml')
38
- require('msgpack')
270
+ module Psych3
271
+ extend self
39
272
 
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
273
+ def input_to_storage(contents, _)
274
+ obj = ::YAML.load(contents)
275
+ packer = CompileCache::YAML.msgpack_factory.packer
276
+ packer.pack(false) # not safe loaded
277
+ begin
278
+ packer.pack(obj)
279
+ rescue NoMethodError, RangeError
280
+ return UNCOMPILABLE # The object included things that we can't serialize
281
+ end
282
+ packer.to_s
283
+ end
46
284
 
47
- klass = class << ::YAML; self; end
48
- klass.send(:define_method, :load_file) do |path|
49
- Bootsnap::CompileCache::Native.fetch(
50
- cache_dir,
51
- path,
52
- Bootsnap::CompileCache::YAML
53
- )
285
+ def storage_to_output(data, kwargs)
286
+ if kwargs&.key?(:symbolize_names)
287
+ kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names)
288
+ end
289
+ unpacker = CompileCache::YAML.msgpack_factory.unpacker(kwargs)
290
+ unpacker.feed(data)
291
+ _safe_loaded = unpacker.unpack
292
+ unpacker.unpack
293
+ end
294
+
295
+ def input_to_output(data, kwargs)
296
+ ::YAML.load(data, **(kwargs || {}))
297
+ end
298
+
299
+ module Patch
300
+ def load_file(path, *args)
301
+ return super unless CompileCache::YAML.supported_internal_encoding?
302
+
303
+ return super if args.size > 1
304
+
305
+ if (kwargs = args.first)
306
+ return super unless kwargs.is_a?(Hash)
307
+ return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty?
308
+ end
309
+
310
+ begin
311
+ CompileCache::Native.fetch(
312
+ CompileCache::YAML.cache_dir,
313
+ File.realpath(path),
314
+ CompileCache::YAML::Psych3,
315
+ kwargs,
316
+ )
317
+ rescue Errno::EACCES
318
+ CompileCache.permission_error(path)
319
+ end
320
+ end
321
+
322
+ ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true)
323
+
324
+ def unsafe_load_file(path, *args)
325
+ return super unless CompileCache::YAML.supported_internal_encoding?
326
+
327
+ return super if args.size > 1
328
+
329
+ if (kwargs = args.first)
330
+ return super unless kwargs.is_a?(Hash)
331
+ return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty?
332
+ end
333
+
334
+ begin
335
+ CompileCache::Native.fetch(
336
+ CompileCache::YAML.cache_dir,
337
+ File.realpath(path),
338
+ CompileCache::YAML::Psych3,
339
+ kwargs,
340
+ )
341
+ rescue Errno::EACCES
342
+ CompileCache.permission_error(path)
343
+ end
344
+ end
345
+
346
+ ruby2_keywords :unsafe_load_file if respond_to?(:ruby2_keywords, true)
54
347
  end
55
348
  end
56
349
  end
@@ -1,9 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Bootsnap
2
4
  module CompileCache
3
- def self.setup(cache_dir:, iseq:, yaml:)
5
+ UNCOMPILABLE = BasicObject.new
6
+ def UNCOMPILABLE.inspect
7
+ "<Bootsnap::UNCOMPILABLE>"
8
+ end
9
+
10
+ Error = Class.new(StandardError)
11
+ PermissionError = Class.new(Error)
12
+
13
+ def self.setup(cache_dir:, iseq:, yaml:, json:)
4
14
  if iseq
5
15
  if supported?
6
- require_relative('compile_cache/iseq')
16
+ require_relative("compile_cache/iseq")
7
17
  Bootsnap::CompileCache::ISeq.install!(cache_dir)
8
18
  elsif $VERBOSE
9
19
  warn("[bootsnap/setup] bytecode caching is not supported on this implementation of Ruby")
@@ -12,19 +22,37 @@ module Bootsnap
12
22
 
13
23
  if yaml
14
24
  if supported?
15
- require_relative('compile_cache/yaml')
25
+ require_relative("compile_cache/yaml")
16
26
  Bootsnap::CompileCache::YAML.install!(cache_dir)
17
27
  elsif $VERBOSE
18
28
  warn("[bootsnap/setup] YAML parsing caching is not supported on this implementation of Ruby")
19
29
  end
20
30
  end
31
+
32
+ if json
33
+ if supported?
34
+ require_relative("compile_cache/json")
35
+ Bootsnap::CompileCache::JSON.install!(cache_dir)
36
+ elsif $VERBOSE
37
+ warn("[bootsnap/setup] JSON parsing caching is not supported on this implementation of Ruby")
38
+ end
39
+ end
40
+ end
41
+
42
+ def self.permission_error(path)
43
+ cpath = Bootsnap::CompileCache::ISeq.cache_dir
44
+ raise(
45
+ PermissionError,
46
+ "bootsnap doesn't have permission to write cache entries in '#{cpath}' " \
47
+ "(or, less likely, doesn't have permission to read '#{path}')",
48
+ )
21
49
  end
22
50
 
23
51
  def self.supported?
24
- # only enable on 'ruby' (MRI), POSIX (darwin, linux, *bsd), and >= 2.3.0
25
- RUBY_ENGINE == 'ruby' &&
26
- RUBY_PLATFORM =~ /darwin|linux|bsd/ &&
27
- Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.3.0")
52
+ # only enable on 'ruby' (MRI), POSIX (darwin, linux, *bsd), Windows (RubyInstaller2) and >= 2.3.0
53
+ RUBY_ENGINE == "ruby" &&
54
+ RUBY_PLATFORM =~ /darwin|linux|bsd|mswin|mingw|cygwin/ &&
55
+ Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.3.0")
28
56
  end
29
57
  end
30
58
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Bootsnap
2
4
  module ExplicitRequire
3
- ARCHDIR = RbConfig::CONFIG['archdir']
4
- RUBYLIBDIR = RbConfig::CONFIG['rubylibdir']
5
- DLEXT = RbConfig::CONFIG['DLEXT']
5
+ ARCHDIR = RbConfig::CONFIG["archdir"]
6
+ RUBYLIBDIR = RbConfig::CONFIG["rubylibdir"]
7
+ DLEXT = RbConfig::CONFIG["DLEXT"]
6
8
 
7
9
  def self.from_self(feature)
8
10
  require_relative("../#{feature}")
@@ -1,4 +1,6 @@
1
- require_relative('../explicit_require')
1
+ # frozen_string_literal: true
2
+
3
+ require_relative("../explicit_require")
2
4
 
3
5
  module Bootsnap
4
6
  module LoadPathCache
@@ -8,8 +10,8 @@ module Bootsnap
8
10
  def initialize(store, path_obj, development_mode: false)
9
11
  @development_mode = development_mode
10
12
  @store = store
11
- @mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new # TODO: Remove once Ruby 2.2 support is dropped.
12
- @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) }
13
15
  @has_relative_paths = nil
14
16
  reinitialize
15
17
  end
@@ -26,15 +28,16 @@ module Bootsnap
26
28
  BUILTIN_FEATURES = $LOADED_FEATURES.each_with_object({}) do |feat, features|
27
29
  # Builtin features are of the form 'enumerator.so'.
28
30
  # All others include paths.
29
- next unless feat.size < 20 && !feat.include?('/')
31
+ next unless feat.size < 20 && !feat.include?("/")
30
32
 
31
- base = File.basename(feat, '.*') # enumerator.so -> enumerator
33
+ base = File.basename(feat, ".*") # enumerator.so -> enumerator
32
34
  ext = File.extname(feat) # .so
33
35
 
34
36
  features[feat] = nil # enumerator.so
35
37
  features[base] = nil # enumerator
36
38
 
37
39
  next unless [DOT_SO, *DL_EXTENSIONS].include?(ext)
40
+
38
41
  DL_EXTENSIONS.each do |dl_ext|
39
42
  features["#{base}#{dl_ext}"] = nil # enumerator.bundle
40
43
  end
@@ -42,21 +45,27 @@ module Bootsnap
42
45
 
43
46
  # Try to resolve this feature to an absolute path without traversing the
44
47
  # loadpath.
45
- def find(feature)
48
+ def find(feature, try_extensions: true)
46
49
  reinitialize if (@has_relative_paths && dir_changed?) || stale?
47
- feature = feature.to_s
48
- return feature if absolute_path?(feature)
49
- return File.expand_path(feature) if feature.start_with?('./')
50
+ feature = feature.to_s.freeze
51
+
52
+ return feature if Bootsnap.absolute_path?(feature)
53
+
54
+ if feature.start_with?("./", "../")
55
+ return try_extensions ? expand_path(feature) : File.expand_path(feature).freeze
56
+ end
57
+
50
58
  @mutex.synchronize do
51
- x = search_index(feature)
59
+ x = search_index(feature, try_extensions: try_extensions)
52
60
  return x if x
61
+ return unless try_extensions
53
62
 
54
63
  # Ruby has some built-in features that require lies about.
55
64
  # For example, 'enumerator' is built in. If you require it, ruby
56
65
  # returns false as if it were already loaded; however, there is no
57
66
  # file to find on disk. We've pre-built a list of these, and we
58
67
  # return false if any of them is loaded.
59
- raise(LoadPathCache::ReturnFalse, '', []) if BUILTIN_FEATURES.key?(feature)
68
+ return false if BUILTIN_FEATURES.key?(feature)
60
69
 
61
70
  # The feature wasn't found on our preliminary search through the index.
62
71
  # We resolve this differently depending on what the extension was.
@@ -65,13 +74,14 @@ module Bootsnap
65
74
  # native dynamic extension, e.g. .bundle or .so), we know it was a
66
75
  # failure and there's nothing more we can do to find the file.
67
76
  # no extension, .rb, (.bundle or .so)
68
- when '', *CACHED_EXTENSIONS # rubocop:disable Performance/CaseWhenSplat
77
+ when "", *CACHED_EXTENSIONS
69
78
  nil
70
79
  # Ruby allows specifying native extensions as '.so' even when DLEXT
71
80
  # is '.bundle'. This is where we handle that case.
72
81
  when DOT_SO
73
82
  x = search_index(feature[0..-4] + DLEXT)
74
83
  return x if x
84
+
75
85
  if DLEXT2
76
86
  x = search_index(feature[0..-4] + DLEXT2)
77
87
  return x if x
@@ -79,7 +89,7 @@ module Bootsnap
79
89
  else
80
90
  # other, unknown extension. For example, `.rake`. Since we haven't
81
91
  # cached these, we legitimately need to run the load path search.
82
- raise(LoadPathCache::FallbackScan, '', [])
92
+ return FALLBACK_SCAN
83
93
  end
84
94
  end
85
95
 
@@ -87,26 +97,18 @@ module Bootsnap
87
97
  # cases where the file doesn't appear to be on the load path. We should
88
98
  # be able to detect newly-created files without rebooting the
89
99
  # application.
90
- raise(LoadPathCache::FallbackScan, '', []) if @development_mode
91
- end
92
-
93
- if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
94
- def absolute_path?(path)
95
- path[1] == ':'
96
- end
97
- else
98
- def absolute_path?(path)
99
- path.start_with?(SLASH)
100
- end
100
+ return FALLBACK_SCAN if @development_mode
101
101
  end
102
102
 
103
103
  def unshift_paths(sender, *paths)
104
104
  return unless sender == @path_obj
105
+
105
106
  @mutex.synchronize { unshift_paths_locked(*paths) }
106
107
  end
107
108
 
108
109
  def push_paths(sender, *paths)
109
110
  return unless sender == @path_obj
111
+
110
112
  @mutex.synchronize { push_paths_locked(*paths) }
111
113
  end
112
114
 
@@ -139,10 +141,11 @@ module Bootsnap
139
141
  p = Path.new(path)
140
142
  @has_relative_paths = true if p.relative?
141
143
  next if p.non_directory?
144
+
142
145
  expanded_path = p.expanded_path
143
146
  entries, dirs = p.entries_and_dirs(@store)
144
147
  # push -> low precedence -> set only if unset
145
- dirs.each { |dir| @dirs[dir] ||= path }
148
+ dirs.each { |dir| @dirs[dir] ||= path }
146
149
  entries.each { |rel| @index[rel] ||= expanded_path }
147
150
  end
148
151
  end
@@ -153,6 +156,7 @@ module Bootsnap
153
156
  paths.map(&:to_s).reverse_each do |path|
154
157
  p = Path.new(path)
155
158
  next if p.non_directory?
159
+
156
160
  expanded_path = p.expanded_path
157
161
  entries, dirs = p.entries_and_dirs(@store)
158
162
  # unshift -> high precedence -> unconditional set
@@ -162,6 +166,10 @@ module Bootsnap
162
166
  end
163
167
  end
164
168
 
169
+ def expand_path(feature)
170
+ maybe_append_extension(File.expand_path(feature))
171
+ end
172
+
165
173
  def stale?
166
174
  @development_mode && @generated_at + AGE_THRESHOLD < now
167
175
  end
@@ -171,19 +179,62 @@ module Bootsnap
171
179
  end
172
180
 
173
181
  if DLEXT2
174
- def search_index(f)
175
- try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f + DLEXT2) || try_index(f)
182
+ def search_index(feature, try_extensions: true)
183
+ if try_extensions
184
+ try_index(feature + DOT_RB) ||
185
+ try_index(feature + DLEXT) ||
186
+ try_index(feature + DLEXT2) ||
187
+ try_index(feature)
188
+ else
189
+ try_index(feature)
190
+ end
191
+ end
192
+
193
+ def maybe_append_extension(feature)
194
+ try_ext(feature + DOT_RB) ||
195
+ try_ext(feature + DLEXT) ||
196
+ try_ext(feature + DLEXT2) ||
197
+ feature
176
198
  end
177
199
  else
178
- def search_index(f)
179
- try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f)
200
+ def search_index(feature, try_extensions: true)
201
+ if try_extensions
202
+ try_index(feature + DOT_RB) || try_index(feature + DLEXT) || try_index(feature)
203
+ else
204
+ try_index(feature)
205
+ end
206
+ end
207
+
208
+ def maybe_append_extension(feature)
209
+ try_ext(feature + DOT_RB) || try_ext(feature + DLEXT) || feature
180
210
  end
181
211
  end
182
212
 
183
- def try_index(f)
184
- if (p = @index[f])
185
- p + '/' + f
213
+ s = rand.to_s.force_encoding(Encoding::US_ASCII).freeze
214
+ if s.respond_to?(:-@)
215
+ if (-s).equal?(s) && (-s.dup).equal?(s) || RUBY_VERSION >= "2.7"
216
+ def try_index(feature)
217
+ if (path = @index[feature])
218
+ -File.join(path, feature).freeze
219
+ end
220
+ end
221
+ else
222
+ def try_index(feature)
223
+ if (path = @index[feature])
224
+ -File.join(path, feature).untaint
225
+ end
226
+ end
186
227
  end
228
+ else
229
+ def try_index(feature)
230
+ if (path = @index[feature])
231
+ File.join(path, feature)
232
+ end
233
+ end
234
+ end
235
+
236
+ def try_ext(feature)
237
+ feature if File.exist?(feature)
187
238
  end
188
239
  end
189
240
  end