bootsnap 1.4.5 → 1.18.3

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 +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