bootsnap 1.6.0 → 1.18.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -47,8 +56,13 @@ module Bootsnap
47
56
  def find(feature)
48
57
  reinitialize if (@has_relative_paths && dir_changed?) || stale?
49
58
  feature = feature.to_s.freeze
50
- return feature if absolute_path?(feature)
51
- return expand_path(feature) if feature.start_with?('./')
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
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,6 +148,9 @@ 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
@@ -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
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Bootsnap
3
4
  module LoadPathCache
4
5
  module ChangeObserver
@@ -15,11 +16,13 @@ module Bootsnap
15
16
  @lpc_observer.push_paths(self, *entries.map(&:to_s))
16
17
  super
17
18
  end
19
+ alias_method :append, :push
18
20
 
19
21
  def unshift(*entries)
20
22
  @lpc_observer.unshift_paths(self, *entries.map(&:to_s))
21
23
  super
22
24
  end
25
+ alias_method :prepend, :unshift
23
26
 
24
27
  def concat(entries)
25
28
  @lpc_observer.push_paths(self, *entries.map(&:to_s))
@@ -51,12 +54,30 @@ module Bootsnap
51
54
  ret
52
55
  end
53
56
  end
57
+
58
+ def dup
59
+ [] + self
60
+ end
61
+
62
+ alias_method :clone, :dup
54
63
  end
55
64
 
56
- def self.register(observer, arr)
65
+ def self.register(arr, observer)
57
66
  return if arr.frozen? # can't register observer, but no need to.
67
+
58
68
  arr.instance_variable_set(:@lpc_observer, observer)
59
- arr.extend(ArrayMixin)
69
+ ArrayMixin.instance_methods.each do |method_name|
70
+ arr.singleton_class.send(:define_method, method_name, ArrayMixin.instance_method(method_name))
71
+ end
72
+ end
73
+
74
+ def self.unregister(arr)
75
+ return unless arr.instance_variable_defined?(:@lpc_observer) && arr.instance_variable_get(:@lpc_observer)
76
+
77
+ ArrayMixin.instance_methods.each do |method_name|
78
+ arr.singleton_class.send(:remove_method, method_name)
79
+ end
80
+ arr.instance_variable_set(:@lpc_observer, nil)
60
81
  end
61
82
  end
62
83
  end
@@ -1,105 +1,37 @@
1
1
  # frozen_string_literal: true
2
- module Bootsnap
3
- module LoadPathCache
4
- module CoreExt
5
- def self.make_load_error(path)
6
- err = LoadError.new(+"cannot load such file -- #{path}")
7
- err.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
8
- err.define_singleton_method(:path) { path }
9
- err
10
- end
11
- end
12
- end
13
- end
14
2
 
15
3
  module Kernel
16
- module_function # rubocop:disable Style/ModuleFunction
4
+ alias_method :require_without_bootsnap, :require
17
5
 
18
- alias_method(:require_without_bootsnap, :require)
6
+ alias_method :require, :require # Avoid method redefinition warnings
19
7
 
20
- # Note that require registers to $LOADED_FEATURES while load does not.
21
- def require_with_bootsnap_lfi(path, resolved = nil)
22
- Bootsnap::LoadPathCache.loaded_features_index.register(path, resolved) do
23
- require_without_bootsnap(resolved || path)
24
- end
25
- end
8
+ def require(path) # rubocop:disable Lint/DuplicateMethods
9
+ return require_without_bootsnap(path) unless Bootsnap::LoadPathCache.enabled?
26
10
 
27
- def require(path)
28
- return false if Bootsnap::LoadPathCache.loaded_features_index.key?(path)
29
-
30
- if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path))
31
- return require_with_bootsnap_lfi(path, resolved)
32
- end
33
-
34
- raise(Bootsnap::LoadPathCache::CoreExt.make_load_error(path))
35
- rescue LoadError => e
36
- e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
37
- raise(e)
38
- rescue Bootsnap::LoadPathCache::ReturnFalse
39
- false
40
- rescue Bootsnap::LoadPathCache::FallbackScan
41
- fallback = true
42
- ensure
43
- if fallback
44
- require_with_bootsnap_lfi(path)
45
- end
46
- end
11
+ string_path = Bootsnap.rb_get_path(path)
12
+ return false if Bootsnap::LoadPathCache.loaded_features_index.key?(string_path)
47
13
 
48
- alias_method(:require_relative_without_bootsnap, :require_relative)
49
- def require_relative(path)
50
- realpath = Bootsnap::LoadPathCache.realpath_cache.call(
51
- caller_locations(1..1).first.absolute_path, path
52
- )
53
- require(realpath)
54
- end
55
-
56
- alias_method(:load_without_bootsnap, :load)
57
- def load(path, wrap = false)
58
- if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path))
59
- return load_without_bootsnap(resolved, wrap)
60
- end
61
-
62
- # load also allows relative paths from pwd even when not in $:
63
- if File.exist?(relative = File.expand_path(path).freeze)
64
- return load_without_bootsnap(relative, wrap)
65
- end
66
-
67
- raise(Bootsnap::LoadPathCache::CoreExt.make_load_error(path))
68
- rescue LoadError => e
69
- e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
70
- raise(e)
71
- rescue Bootsnap::LoadPathCache::ReturnFalse
72
- false
73
- rescue Bootsnap::LoadPathCache::FallbackScan
74
- fallback = true
75
- ensure
76
- if fallback
77
- load_without_bootsnap(path, wrap)
14
+ resolved = Bootsnap::LoadPathCache.load_path_cache.find(string_path)
15
+ if Bootsnap::LoadPathCache::FALLBACK_SCAN.equal?(resolved)
16
+ if (cursor = Bootsnap::LoadPathCache.loaded_features_index.cursor(string_path))
17
+ ret = require_without_bootsnap(path)
18
+ resolved = Bootsnap::LoadPathCache.loaded_features_index.identify(string_path, cursor)
19
+ Bootsnap::LoadPathCache.loaded_features_index.register(string_path, resolved)
20
+ return ret
21
+ else
22
+ return require_without_bootsnap(path)
23
+ end
24
+ elsif false == resolved
25
+ return false
26
+ elsif resolved.nil?
27
+ return require_without_bootsnap(path)
28
+ else
29
+ # Note that require registers to $LOADED_FEATURES while load does not.
30
+ ret = require_without_bootsnap(resolved)
31
+ Bootsnap::LoadPathCache.loaded_features_index.register(string_path, resolved)
32
+ return ret
78
33
  end
79
34
  end
80
- end
81
35
 
82
- class Module
83
- alias_method(:autoload_without_bootsnap, :autoload)
84
- def autoload(const, path)
85
- # NOTE: This may defeat LoadedFeaturesIndex, but it's not immediately
86
- # obvious how to make it work. This feels like a pretty niche case, unclear
87
- # if it will ever burn anyone.
88
- #
89
- # The challenge is that we don't control the point at which the entry gets
90
- # added to $LOADED_FEATURES and won't be able to hook that modification
91
- # since it's done in C-land.
92
- autoload_without_bootsnap(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
93
- rescue LoadError => e
94
- e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
95
- raise(e)
96
- rescue Bootsnap::LoadPathCache::ReturnFalse
97
- false
98
- rescue Bootsnap::LoadPathCache::FallbackScan
99
- fallback = true
100
- ensure
101
- if fallback
102
- autoload_without_bootsnap(const, path)
103
- end
104
- end
36
+ private :require
105
37
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  class << $LOADED_FEATURES
3
4
  alias_method(:delete_without_bootsnap, :delete)
4
5
  def delete(key)
@@ -26,20 +26,21 @@ module Bootsnap
26
26
  class LoadedFeaturesIndex
27
27
  def initialize
28
28
  @lfi = {}
29
- @mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new # TODO: Remove once Ruby 2.2 support is dropped.
29
+ @mutex = Mutex.new
30
30
 
31
31
  # In theory the user could mutate $LOADED_FEATURES and invalidate our
32
- # cache. If this ever comes up in practice or if you, the
33
- # enterprising reader, feels inclined to solve this problem we could
32
+ # cache. If this ever comes up in practice - or if you, the
33
+ # enterprising reader, feels inclined to solve this problem - we could
34
34
  # parallel the work done with ChangeObserver on $LOAD_PATH to mirror
35
35
  # updates to our @lfi.
36
36
  $LOADED_FEATURES.each do |feat|
37
37
  hash = feat.hash
38
38
  $LOAD_PATH.each do |lpe|
39
39
  next unless feat.start_with?(lpe)
40
+
40
41
  # /a/b/lib/my/foo.rb
41
42
  # ^^^^^^^^^
42
- short = feat[(lpe.length + 1)..-1]
43
+ short = feat[(lpe.length + 1)..]
43
44
  stripped = strip_extension_if_elidable(short)
44
45
  @lfi[short] = hash
45
46
  @lfi[stripped] = hash
@@ -58,9 +59,9 @@ module Bootsnap
58
59
  end
59
60
 
60
61
  def purge_multi(features)
61
- rejected_hashes = features.map(&:hash).to_set
62
+ rejected_hashes = features.each_with_object({}) { |f, h| h[f.hash] = true }
62
63
  @mutex.synchronize do
63
- @lfi.reject! { |_, hash| rejected_hashes.include?(hash) }
64
+ @lfi.reject! { |_, hash| rejected_hashes.key?(hash) }
64
65
  end
65
66
  end
66
67
 
@@ -68,11 +69,30 @@ module Bootsnap
68
69
  @mutex.synchronize { @lfi.key?(feature) }
69
70
  end
70
71
 
72
+ def cursor(short)
73
+ unless Bootsnap.absolute_path?(short.to_s)
74
+ $LOADED_FEATURES.size
75
+ end
76
+ end
77
+
78
+ def identify(short, cursor)
79
+ $LOADED_FEATURES[cursor..].detect do |feat|
80
+ offset = 0
81
+ while (offset = feat.index(short, offset))
82
+ if feat.index(".", offset + 1) && !feat.index("/", offset + 2)
83
+ break true
84
+ else
85
+ offset += 1
86
+ end
87
+ end
88
+ end
89
+ end
90
+
71
91
  # There is a relatively uncommon case where we could miss adding an
72
92
  # entry:
73
93
  #
74
94
  # If the user asked for e.g. `require 'bundler'`, and we went through the
75
- # `FallbackScan` pathway in `kernel_require.rb` and therefore did not
95
+ # `FALLBACK_SCAN` pathway in `kernel_require.rb` and therefore did not
76
96
  # pass `long` (the full expanded absolute path), then we did are not able
77
97
  # to confidently add the `bundler.rb` form to @lfi.
78
98
  #
@@ -82,15 +102,8 @@ module Bootsnap
82
102
  # not quite right; or
83
103
  # 2. Inspect $LOADED_FEATURES upon return from yield to find the matching
84
104
  # entry.
85
- def register(short, long = nil)
86
- if long.nil?
87
- pat = %r{/#{Regexp.escape(short)}(\.[^/]+)?$}
88
- len = $LOADED_FEATURES.size
89
- ret = yield
90
- long = $LOADED_FEATURES[len..-1].detect { |feat| feat =~ pat }
91
- else
92
- ret = yield
93
- end
105
+ def register(short, long)
106
+ return if Bootsnap.absolute_path?(short)
94
107
 
95
108
  hash = long.hash
96
109
 
@@ -109,13 +122,11 @@ module Bootsnap
109
122
  @lfi[short] = hash
110
123
  (@lfi[altname] = hash) if altname
111
124
  end
112
-
113
- ret
114
125
  end
115
126
 
116
127
  private
117
128
 
118
- STRIP_EXTENSION = /\.[^.]*?$/
129
+ STRIP_EXTENSION = /\.[^.]*?$/.freeze
119
130
  private_constant(:STRIP_EXTENSION)
120
131
 
121
132
  # Might Ruby automatically search for this extension if
@@ -132,15 +143,15 @@ module Bootsnap
132
143
  # with calling a Ruby file 'x.dylib.rb' and then requiring it as 'x.dylib'.)
133
144
  #
134
145
  # See <https://ruby-doc.org/core-2.6.4/Kernel.html#method-i-require>.
135
- def extension_elidable?(f)
136
- f.to_s.end_with?('.rb', '.so', '.o', '.dll', '.dylib')
146
+ def extension_elidable?(feature)
147
+ feature.to_s.end_with?(".rb", ".so", ".o", ".dll", ".dylib")
137
148
  end
138
149
 
139
- def strip_extension_if_elidable(f)
140
- if extension_elidable?(f)
141
- f.sub(STRIP_EXTENSION, '')
150
+ def strip_extension_if_elidable(feature)
151
+ if extension_elidable?(feature)
152
+ feature.sub(STRIP_EXTENSION, "")
142
153
  else
143
- f
154
+ feature
144
155
  end
145
156
  end
146
157
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
- require_relative('path_scanner')
2
+
3
+ require_relative "path_scanner"
3
4
 
4
5
  module Bootsnap
5
6
  module LoadPathCache
@@ -20,14 +21,32 @@ module Bootsnap
20
21
 
21
22
  attr_reader(:path)
22
23
 
23
- def initialize(path)
24
+ def initialize(path, real: false)
24
25
  @path = path.to_s.freeze
26
+ @real = real
27
+ end
28
+
29
+ def to_realpath
30
+ return self if @real
31
+
32
+ realpath = begin
33
+ File.realpath(path)
34
+ rescue Errno::ENOENT
35
+ return self
36
+ end
37
+
38
+ if realpath == path
39
+ @real = true
40
+ self
41
+ else
42
+ Path.new(realpath, real: true)
43
+ end
25
44
  end
26
45
 
27
46
  # True if the path exists, but represents a non-directory object
28
47
  def non_directory?
29
48
  !File.stat(path).directory?
30
- rescue Errno::ENOENT, Errno::ENOTDIR
49
+ rescue Errno::ENOENT, Errno::ENOTDIR, Errno::EINVAL
31
50
  false
32
51
  end
33
52
 
@@ -43,6 +62,7 @@ module Bootsnap
43
62
  # set to zero anyway, just in case we change the stability heuristics.
44
63
  _, entries, dirs = store.get(expanded_path)
45
64
  return [entries, dirs] if entries # cache hit
65
+
46
66
  entries, dirs = scan!
47
67
  store.set(expanded_path, [0, entries, dirs])
48
68
  return [entries, dirs]
@@ -60,7 +80,11 @@ module Bootsnap
60
80
  end
61
81
 
62
82
  def expanded_path
63
- File.expand_path(path).freeze
83
+ if @real
84
+ path
85
+ else
86
+ @expanded_path ||= File.expand_path(path).freeze
87
+ end
64
88
  end
65
89
 
66
90
  private
@@ -77,7 +101,7 @@ module Bootsnap
77
101
  ["", *dirs].each do |dir|
78
102
  curr = begin
79
103
  File.mtime("#{path}/#{dir}").to_i
80
- rescue Errno::ENOENT, Errno::ENOTDIR
104
+ rescue Errno::ENOENT, Errno::ENOTDIR, Errno::EINVAL
81
105
  -1
82
106
  end
83
107
  max = curr if curr > max
@@ -92,21 +116,19 @@ module Bootsnap
92
116
  VOLATILE = :volatile
93
117
 
94
118
  # Built-in ruby lib stuff doesn't change, but things can occasionally be
95
- # installed into sitedir, which generally lives under libdir.
96
- RUBY_LIBDIR = RbConfig::CONFIG['libdir']
97
- RUBY_SITEDIR = RbConfig::CONFIG['sitedir']
119
+ # installed into sitedir, which generally lives under rubylibdir.
120
+ RUBY_LIBDIR = RbConfig::CONFIG["rubylibdir"]
121
+ RUBY_SITEDIR = RbConfig::CONFIG["sitedir"]
98
122
 
99
123
  def stability
100
- @stability ||= begin
101
- if Gem.path.detect { |p| expanded_path.start_with?(p.to_s) }
102
- STABLE
103
- elsif Bootsnap.bundler? && expanded_path.start_with?(Bundler.bundle_path.to_s)
104
- STABLE
105
- elsif expanded_path.start_with?(RUBY_LIBDIR) && !expanded_path.start_with?(RUBY_SITEDIR)
106
- STABLE
107
- else
108
- VOLATILE
109
- end
124
+ @stability ||= if Gem.path.detect { |p| expanded_path.start_with?(p.to_s) }
125
+ STABLE
126
+ elsif Bootsnap.bundler? && expanded_path.start_with?(Bundler.bundle_path.to_s)
127
+ STABLE
128
+ elsif expanded_path.start_with?(RUBY_LIBDIR) && !expanded_path.start_with?(RUBY_SITEDIR)
129
+ STABLE
130
+ else
131
+ VOLATILE
110
132
  end
111
133
  end
112
134
  end