bootsnap 1.4.5 → 1.11.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +188 -0
  3. data/LICENSE.txt +1 -1
  4. data/README.md +48 -17
  5. data/exe/bootsnap +5 -0
  6. data/ext/bootsnap/bootsnap.c +297 -118
  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 +63 -18
  12. data/lib/bootsnap/compile_cache/json.rb +88 -0
  13. data/lib/bootsnap/compile_cache/yaml.rb +331 -42
  14. data/lib/bootsnap/compile_cache.rb +22 -8
  15. data/lib/bootsnap/explicit_require.rb +5 -3
  16. data/lib/bootsnap/load_path_cache/cache.rb +75 -38
  17. data/lib/bootsnap/load_path_cache/change_observer.rb +6 -1
  18. data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +37 -64
  19. data/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb +2 -0
  20. data/lib/bootsnap/load_path_cache/loaded_features_index.rb +62 -28
  21. data/lib/bootsnap/load_path_cache/path.rb +31 -6
  22. data/lib/bootsnap/load_path_cache/path_scanner.rb +54 -29
  23. data/lib/bootsnap/load_path_cache/store.rb +60 -16
  24. data/lib/bootsnap/load_path_cache.rb +17 -38
  25. data/lib/bootsnap/setup.rb +3 -36
  26. data/lib/bootsnap/version.rb +3 -1
  27. data/lib/bootsnap.rb +137 -36
  28. metadata +15 -99
  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/lib/bootsnap/load_path_cache/realpath_cache.rb +0 -32
  48. data/shipit.rubygems.yml +0 -0
@@ -1,60 +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
20
25
 
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
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
+ )
29
34
  end
30
- end
31
35
 
32
- def self.input_to_output(data)
33
- ::YAML.load(data)
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")
61
+
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
134
+ end
34
135
  end
35
136
 
36
- def self.install!(cache_dir)
37
- require('yaml')
38
- require('msgpack')
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
268
+ end
39
269
 
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
270
+ module Psych3
271
+ extend self
46
272
 
47
- klass = class << ::YAML; self; end
48
- klass.send(:define_method, :load_file) do |path|
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
49
277
  begin
50
- Bootsnap::CompileCache::Native.fetch(
51
- cache_dir,
52
- path,
53
- Bootsnap::CompileCache::YAML
54
- )
55
- rescue Errno::EACCES
56
- Bootsnap::CompileCache.permission_error(path)
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
284
+
285
+ def storage_to_output(data, kwargs)
286
+ if kwargs&.key?(:symbolize_names)
287
+ kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names)
57
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)
58
347
  end
59
348
  end
60
349
  end
@@ -1,12 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Bootsnap
2
4
  module CompileCache
3
- Error = Class.new(StandardError)
5
+ UNCOMPILABLE = BasicObject.new
6
+ def UNCOMPILABLE.inspect
7
+ "<Bootsnap::UNCOMPILABLE>"
8
+ end
9
+
10
+ Error = Class.new(StandardError)
4
11
  PermissionError = Class.new(Error)
5
12
 
6
- def self.setup(cache_dir:, iseq:, yaml:)
13
+ def self.setup(cache_dir:, iseq:, yaml:, json:)
7
14
  if iseq
8
15
  if supported?
9
- require_relative('compile_cache/iseq')
16
+ require_relative("compile_cache/iseq")
10
17
  Bootsnap::CompileCache::ISeq.install!(cache_dir)
11
18
  elsif $VERBOSE
12
19
  warn("[bootsnap/setup] bytecode caching is not supported on this implementation of Ruby")
@@ -15,12 +22,21 @@ module Bootsnap
15
22
 
16
23
  if yaml
17
24
  if supported?
18
- require_relative('compile_cache/yaml')
25
+ require_relative("compile_cache/yaml")
19
26
  Bootsnap::CompileCache::YAML.install!(cache_dir)
20
27
  elsif $VERBOSE
21
28
  warn("[bootsnap/setup] YAML parsing caching is not supported on this implementation of Ruby")
22
29
  end
23
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
24
40
  end
25
41
 
26
42
  def self.permission_error(path)
@@ -33,10 +49,8 @@ module Bootsnap
33
49
  end
34
50
 
35
51
  def self.supported?
36
- # only enable on 'ruby' (MRI), POSIX (darwin, linux, *bsd), and >= 2.3.0
37
- RUBY_ENGINE == 'ruby' &&
38
- RUBY_PLATFORM =~ /darwin|linux|bsd/ &&
39
- 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" && RUBY_PLATFORM.match?(/darwin|linux|bsd|mswin|mingw|cygwin/)
40
54
  end
41
55
  end
42
56
  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,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative('../explicit_require')
3
+ require_relative("../explicit_require")
4
4
 
5
5
  module Bootsnap
6
6
  module LoadPathCache
@@ -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
@@ -28,15 +28,16 @@ module Bootsnap
28
28
  BUILTIN_FEATURES = $LOADED_FEATURES.each_with_object({}) do |feat, features|
29
29
  # Builtin features are of the form 'enumerator.so'.
30
30
  # All others include paths.
31
- next unless feat.size < 20 && !feat.include?('/')
31
+ next unless feat.size < 20 && !feat.include?("/")
32
32
 
33
- base = File.basename(feat, '.*') # enumerator.so -> enumerator
33
+ base = File.basename(feat, ".*") # enumerator.so -> enumerator
34
34
  ext = File.extname(feat) # .so
35
35
 
36
36
  features[feat] = nil # enumerator.so
37
37
  features[base] = nil # enumerator
38
38
 
39
39
  next unless [DOT_SO, *DL_EXTENSIONS].include?(ext)
40
+
40
41
  DL_EXTENSIONS.each do |dl_ext|
41
42
  features["#{base}#{dl_ext}"] = nil # enumerator.bundle
42
43
  end
@@ -44,21 +45,27 @@ module Bootsnap
44
45
 
45
46
  # Try to resolve this feature to an absolute path without traversing the
46
47
  # loadpath.
47
- def find(feature)
48
+ def find(feature, try_extensions: true)
48
49
  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?('./')
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
+
52
58
  @mutex.synchronize do
53
- x = search_index(feature)
59
+ x = search_index(feature, try_extensions: try_extensions)
54
60
  return x if x
61
+ return unless try_extensions
55
62
 
56
63
  # Ruby has some built-in features that require lies about.
57
64
  # For example, 'enumerator' is built in. If you require it, ruby
58
65
  # returns false as if it were already loaded; however, there is no
59
66
  # file to find on disk. We've pre-built a list of these, and we
60
67
  # return false if any of them is loaded.
61
- raise(LoadPathCache::ReturnFalse, '', []) if BUILTIN_FEATURES.key?(feature)
68
+ return false if BUILTIN_FEATURES.key?(feature)
62
69
 
63
70
  # The feature wasn't found on our preliminary search through the index.
64
71
  # We resolve this differently depending on what the extension was.
@@ -67,13 +74,14 @@ module Bootsnap
67
74
  # native dynamic extension, e.g. .bundle or .so), we know it was a
68
75
  # failure and there's nothing more we can do to find the file.
69
76
  # no extension, .rb, (.bundle or .so)
70
- when '', *CACHED_EXTENSIONS # rubocop:disable Performance/CaseWhenSplat
77
+ when "", *CACHED_EXTENSIONS
71
78
  nil
72
79
  # Ruby allows specifying native extensions as '.so' even when DLEXT
73
80
  # is '.bundle'. This is where we handle that case.
74
81
  when DOT_SO
75
82
  x = search_index(feature[0..-4] + DLEXT)
76
83
  return x if x
84
+
77
85
  if DLEXT2
78
86
  x = search_index(feature[0..-4] + DLEXT2)
79
87
  return x if x
@@ -81,7 +89,7 @@ module Bootsnap
81
89
  else
82
90
  # other, unknown extension. For example, `.rake`. Since we haven't
83
91
  # cached these, we legitimately need to run the load path search.
84
- raise(LoadPathCache::FallbackScan, '', [])
92
+ return FALLBACK_SCAN
85
93
  end
86
94
  end
87
95
 
@@ -89,26 +97,18 @@ module Bootsnap
89
97
  # cases where the file doesn't appear to be on the load path. We should
90
98
  # be able to detect newly-created files without rebooting the
91
99
  # application.
92
- raise(LoadPathCache::FallbackScan, '', []) if @development_mode
93
- end
94
-
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
100
+ return FALLBACK_SCAN if @development_mode
103
101
  end
104
102
 
105
103
  def unshift_paths(sender, *paths)
106
104
  return unless sender == @path_obj
105
+
107
106
  @mutex.synchronize { unshift_paths_locked(*paths) }
108
107
  end
109
108
 
110
109
  def push_paths(sender, *paths)
111
110
  return unless sender == @path_obj
111
+
112
112
  @mutex.synchronize { push_paths_locked(*paths) }
113
113
  end
114
114
 
@@ -141,10 +141,13 @@ module Bootsnap
141
141
  p = Path.new(path)
142
142
  @has_relative_paths = true if p.relative?
143
143
  next if p.non_directory?
144
+
145
+ p = p.to_realpath
146
+
144
147
  expanded_path = p.expanded_path
145
148
  entries, dirs = p.entries_and_dirs(@store)
146
149
  # push -> low precedence -> set only if unset
147
- dirs.each { |dir| @dirs[dir] ||= path }
150
+ dirs.each { |dir| @dirs[dir] ||= path }
148
151
  entries.each { |rel| @index[rel] ||= expanded_path }
149
152
  end
150
153
  end
@@ -155,6 +158,9 @@ module Bootsnap
155
158
  paths.map(&:to_s).reverse_each do |path|
156
159
  p = Path.new(path)
157
160
  next if p.non_directory?
161
+
162
+ p = p.to_realpath
163
+
158
164
  expanded_path = p.expanded_path
159
165
  entries, dirs = p.entries_and_dirs(@store)
160
166
  # unshift -> high precedence -> unconditional set
@@ -177,31 +183,62 @@ module Bootsnap
177
183
  end
178
184
 
179
185
  if DLEXT2
180
- def search_index(f)
181
- try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f + DLEXT2) || try_index(f)
186
+ def search_index(feature, try_extensions: true)
187
+ if try_extensions
188
+ try_index(feature + DOT_RB) ||
189
+ try_index(feature + DLEXT) ||
190
+ try_index(feature + DLEXT2) ||
191
+ try_index(feature)
192
+ else
193
+ try_index(feature)
194
+ end
182
195
  end
183
196
 
184
- def maybe_append_extension(f)
185
- try_ext(f + DOT_RB) || try_ext(f + DLEXT) || try_ext(f + DLEXT2) || f
197
+ def maybe_append_extension(feature)
198
+ try_ext(feature + DOT_RB) ||
199
+ try_ext(feature + DLEXT) ||
200
+ try_ext(feature + DLEXT2) ||
201
+ feature
186
202
  end
187
203
  else
188
- def search_index(f)
189
- try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f)
204
+ def search_index(feature, try_extensions: true)
205
+ if try_extensions
206
+ try_index(feature + DOT_RB) || try_index(feature + DLEXT) || try_index(feature)
207
+ else
208
+ try_index(feature)
209
+ end
190
210
  end
191
211
 
192
- def maybe_append_extension(f)
193
- try_ext(f + DOT_RB) || try_ext(f + DLEXT) || f
212
+ def maybe_append_extension(feature)
213
+ try_ext(feature + DOT_RB) || try_ext(feature + DLEXT) || feature
194
214
  end
195
215
  end
196
216
 
197
- def try_index(f)
198
- if (p = @index[f])
199
- p + '/' + f
217
+ s = rand.to_s.force_encoding(Encoding::US_ASCII).freeze
218
+ if s.respond_to?(:-@)
219
+ if (-s).equal?(s) && (-s.dup).equal?(s) || RUBY_VERSION >= "2.7"
220
+ def try_index(feature)
221
+ if (path = @index[feature])
222
+ -File.join(path, feature).freeze
223
+ end
224
+ end
225
+ else
226
+ def try_index(feature)
227
+ if (path = @index[feature])
228
+ -File.join(path, feature).untaint
229
+ end
230
+ end
231
+ end
232
+ else
233
+ def try_index(feature)
234
+ if (path = @index[feature])
235
+ File.join(path, feature)
236
+ end
200
237
  end
201
238
  end
202
239
 
203
- def try_ext(f)
204
- f if File.exist?(f)
240
+ def try_ext(feature)
241
+ feature if File.exist?(feature)
205
242
  end
206
243
  end
207
244
  end