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