ed-precompiled_bootsnap 1.18.6-x64-mingw-ucrt

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.
@@ -0,0 +1,244 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../explicit_require"
4
+
5
+ module Bootsnap
6
+ module LoadPathCache
7
+ class Cache
8
+ AGE_THRESHOLD = 30 # seconds
9
+
10
+ def initialize(store, path_obj, development_mode: false)
11
+ @development_mode = development_mode
12
+ @store = store
13
+ @mutex = Mutex.new
14
+ @path_obj = path_obj.map! { |f| PathScanner.os_path(File.exist?(f) ? File.realpath(f) : f.dup) }
15
+ @has_relative_paths = nil
16
+ reinitialize
17
+ end
18
+
19
+ # What is the path item that contains the dir as child?
20
+ # e.g. given "/a/b/c/d" exists, and the path is ["/a/b"], load_dir("c/d")
21
+ # is "/a/b".
22
+ def load_dir(dir)
23
+ reinitialize if stale?
24
+ @mutex.synchronize { @dirs[dir] }
25
+ end
26
+
27
+ TRUFFLERUBY_LIB_DIR_PREFIX = if RUBY_ENGINE == "truffleruby"
28
+ "#{File.join(RbConfig::CONFIG['libdir'], 'truffle')}#{File::SEPARATOR}"
29
+ end
30
+
31
+ # { 'enumerator' => nil, 'enumerator.so' => nil, ... }
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
+
37
+ # Builtin features are of the form 'enumerator.so'.
38
+ # All others include paths.
39
+ next unless feat.size < 20 && !feat.include?("/")
40
+
41
+ base = File.basename(feat, ".*") # enumerator.so -> enumerator
42
+ ext = File.extname(feat) # .so
43
+
44
+ features[feat] = nil # enumerator.so
45
+ features[base] = nil # enumerator
46
+
47
+ next unless [DOT_SO, *DL_EXTENSIONS].include?(ext)
48
+
49
+ DL_EXTENSIONS.each do |dl_ext|
50
+ features["#{base}#{dl_ext}"] = nil # enumerator.bundle
51
+ end
52
+ end.freeze
53
+
54
+ # Try to resolve this feature to an absolute path without traversing the
55
+ # loadpath.
56
+ def find(feature)
57
+ reinitialize if (@has_relative_paths && dir_changed?) || stale?
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
+
66
+ @mutex.synchronize do
67
+ x = search_index(feature)
68
+ return x if x
69
+
70
+ # Ruby has some built-in features that require lies about.
71
+ # For example, 'enumerator' is built in. If you require it, ruby
72
+ # returns false as if it were already loaded; however, there is no
73
+ # file to find on disk. We've pre-built a list of these, and we
74
+ # return false if any of them is loaded.
75
+ return false if BUILTIN_FEATURES.key?(feature)
76
+
77
+ # The feature wasn't found on our preliminary search through the index.
78
+ # We resolve this differently depending on what the extension was.
79
+ case File.extname(feature)
80
+ # If the extension was one of the ones we explicitly cache (.rb and the
81
+ # native dynamic extension, e.g. .bundle or .so), we know it was a
82
+ # failure and there's nothing more we can do to find the file.
83
+ # no extension, .rb, (.bundle or .so)
84
+ when "", *CACHED_EXTENSIONS
85
+ nil
86
+ # Ruby allows specifying native extensions as '.so' even when DLEXT
87
+ # is '.bundle'. This is where we handle that case.
88
+ when DOT_SO
89
+ x = search_index(feature[0..-4] + DLEXT)
90
+ return x if x
91
+
92
+ if DLEXT2
93
+ x = search_index(feature[0..-4] + DLEXT2)
94
+ return x if x
95
+ end
96
+ else
97
+ # other, unknown extension. For example, `.rake`. Since we haven't
98
+ # cached these, we legitimately need to run the load path search.
99
+ return FALLBACK_SCAN
100
+ end
101
+ end
102
+
103
+ # In development mode, we don't want to confidently return failures for
104
+ # cases where the file doesn't appear to be on the load path. We should
105
+ # be able to detect newly-created files without rebooting the
106
+ # application.
107
+ return FALLBACK_SCAN if @development_mode
108
+ end
109
+
110
+ def unshift_paths(sender, *paths)
111
+ return unless sender == @path_obj
112
+
113
+ @mutex.synchronize { unshift_paths_locked(*paths) }
114
+ end
115
+
116
+ def push_paths(sender, *paths)
117
+ return unless sender == @path_obj
118
+
119
+ @mutex.synchronize { push_paths_locked(*paths) }
120
+ end
121
+
122
+ def reinitialize(path_obj = @path_obj)
123
+ @mutex.synchronize do
124
+ @path_obj = path_obj
125
+ ChangeObserver.register(@path_obj, self)
126
+ @index = {}
127
+ @dirs = {}
128
+ @generated_at = now
129
+ push_paths_locked(*@path_obj)
130
+ end
131
+ end
132
+
133
+ private
134
+
135
+ def dir_changed?
136
+ @prev_dir ||= Dir.pwd
137
+ if @prev_dir == Dir.pwd
138
+ false
139
+ else
140
+ @prev_dir = Dir.pwd
141
+ true
142
+ end
143
+ end
144
+
145
+ def push_paths_locked(*paths)
146
+ @store.transaction do
147
+ paths.map(&:to_s).each do |path|
148
+ p = Path.new(path)
149
+ @has_relative_paths = true if p.relative?
150
+ next if p.non_directory?
151
+
152
+ p = p.to_realpath
153
+
154
+ expanded_path = p.expanded_path
155
+ entries, dirs = p.entries_and_dirs(@store)
156
+ # push -> low precedence -> set only if unset
157
+ dirs.each { |dir| @dirs[dir] ||= path }
158
+ entries.each { |rel| @index[rel] ||= expanded_path }
159
+ end
160
+ end
161
+ end
162
+
163
+ def unshift_paths_locked(*paths)
164
+ @store.transaction do
165
+ paths.map(&:to_s).reverse_each do |path|
166
+ p = Path.new(path)
167
+ next if p.non_directory?
168
+
169
+ p = p.to_realpath
170
+
171
+ expanded_path = p.expanded_path
172
+ entries, dirs = p.entries_and_dirs(@store)
173
+ # unshift -> high precedence -> unconditional set
174
+ dirs.each { |dir| @dirs[dir] = path }
175
+ entries.each { |rel| @index[rel] = expanded_path }
176
+ end
177
+ end
178
+ end
179
+
180
+ def expand_path(feature)
181
+ maybe_append_extension(File.expand_path(feature))
182
+ end
183
+
184
+ def stale?
185
+ @development_mode && @generated_at + AGE_THRESHOLD < now
186
+ end
187
+
188
+ def now
189
+ Process.clock_gettime(Process::CLOCK_MONOTONIC).to_i
190
+ end
191
+
192
+ if DLEXT2
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)
198
+ end
199
+
200
+ def maybe_append_extension(feature)
201
+ try_ext(feature + DOT_RB) ||
202
+ try_ext(feature + DLEXT) ||
203
+ try_ext(feature + DLEXT2) ||
204
+ feature
205
+ end
206
+ else
207
+ def search_index(feature)
208
+ try_index(feature + DOT_RB) || try_index(feature + DLEXT) || try_index(feature)
209
+ end
210
+
211
+ def maybe_append_extension(feature)
212
+ try_ext(feature + DOT_RB) || try_ext(feature + DLEXT) || feature
213
+ end
214
+ end
215
+
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
236
+ end
237
+ end
238
+
239
+ def try_ext(feature)
240
+ feature if File.exist?(feature)
241
+ end
242
+ end
243
+ end
244
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bootsnap
4
+ module LoadPathCache
5
+ module ChangeObserver
6
+ module ArrayMixin
7
+ # For each method that adds items to one end or another of the array
8
+ # (<<, push, unshift, concat), override that method to also notify the
9
+ # observer of the change.
10
+ def <<(entry)
11
+ @lpc_observer.push_paths(self, entry.to_s)
12
+ super
13
+ end
14
+
15
+ def push(*entries)
16
+ @lpc_observer.push_paths(self, *entries.map(&:to_s))
17
+ super
18
+ end
19
+ alias_method :append, :push
20
+
21
+ def unshift(*entries)
22
+ @lpc_observer.unshift_paths(self, *entries.map(&:to_s))
23
+ super
24
+ end
25
+ alias_method :prepend, :unshift
26
+
27
+ def concat(entries)
28
+ @lpc_observer.push_paths(self, *entries.map(&:to_s))
29
+ super
30
+ end
31
+
32
+ # uniq! keeps the first occurrence of each path, otherwise preserving
33
+ # order, preserving the effective load path
34
+ def uniq!(*args)
35
+ ret = super
36
+ @lpc_observer.reinitialize if block_given? || !args.empty?
37
+ ret
38
+ end
39
+
40
+ # For each method that modifies the array more aggressively, override
41
+ # the method to also have the observer completely reconstruct its state
42
+ # after the modification. Many of these could be made to modify the
43
+ # internal state of the LoadPathCache::Cache more efficiently, but the
44
+ # accounting cost would be greater than the hit from these, since we
45
+ # actively discourage calling them.
46
+ %i(
47
+ []= clear collect! compact! delete delete_at delete_if fill flatten!
48
+ insert keep_if map! pop reject! replace reverse! rotate! select!
49
+ shift shuffle! slice! sort! sort_by!
50
+ ).each do |method_name|
51
+ define_method(method_name) do |*args, &block|
52
+ ret = super(*args, &block)
53
+ @lpc_observer.reinitialize
54
+ ret
55
+ end
56
+ end
57
+
58
+ def dup
59
+ [] + self
60
+ end
61
+
62
+ alias_method :clone, :dup
63
+ end
64
+
65
+ def self.register(arr, observer)
66
+ return if arr.frozen? # can't register observer, but no need to.
67
+
68
+ arr.instance_variable_set(:@lpc_observer, observer)
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)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kernel
4
+ alias_method :require_without_bootsnap, :require
5
+
6
+ alias_method :require, :require # Avoid method redefinition warnings
7
+
8
+ def require(path) # rubocop:disable Lint/DuplicateMethods
9
+ return require_without_bootsnap(path) unless Bootsnap::LoadPathCache.enabled?
10
+
11
+ string_path = Bootsnap.rb_get_path(path)
12
+ return false if Bootsnap::LoadPathCache.loaded_features_index.key?(string_path)
13
+
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
33
+ end
34
+ end
35
+
36
+ private :require
37
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class << $LOADED_FEATURES
4
+ alias_method(:delete_without_bootsnap, :delete)
5
+ def delete(key)
6
+ Bootsnap::LoadPathCache.loaded_features_index.purge(key)
7
+ delete_without_bootsnap(key)
8
+ end
9
+
10
+ alias_method(:reject_without_bootsnap!, :reject!)
11
+ def reject!(&block)
12
+ backup = dup
13
+
14
+ # FIXME: if no block is passed we'd need to return a decorated iterator
15
+ reject_without_bootsnap!(&block)
16
+
17
+ Bootsnap::LoadPathCache.loaded_features_index.purge_multi(backup - self)
18
+ end
19
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bootsnap
4
+ module LoadPathCache
5
+ # LoadedFeaturesIndex partially mirrors an internal structure in ruby that
6
+ # we can't easily obtain an interface to.
7
+ #
8
+ # This works around an issue where, without bootsnap, *ruby* knows that it
9
+ # has already required a file by its short name (e.g. require 'bundler') if
10
+ # a new instance of bundler is added to the $LOAD_PATH which resolves to a
11
+ # different absolute path. This class makes bootsnap smart enough to
12
+ # realize that it has already loaded 'bundler', and not just
13
+ # '/path/to/bundler'.
14
+ #
15
+ # If you disable LoadedFeaturesIndex, you can see the problem this solves by:
16
+ #
17
+ # 1. `require 'a'`
18
+ # 2. Prepend a new $LOAD_PATH element containing an `a.rb`
19
+ # 3. `require 'a'`
20
+ #
21
+ # Ruby returns false from step 3.
22
+ # With bootsnap but with no LoadedFeaturesIndex, this loads two different
23
+ # `a.rb`s.
24
+ # With bootsnap and with LoadedFeaturesIndex, this skips the second load,
25
+ # returning false like ruby.
26
+ class LoadedFeaturesIndex
27
+ def initialize
28
+ @lfi = {}
29
+ @mutex = Mutex.new
30
+
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
34
+ # parallel the work done with ChangeObserver on $LOAD_PATH to mirror
35
+ # updates to our @lfi.
36
+ $LOADED_FEATURES.each do |feat|
37
+ hash = feat.hash
38
+ $LOAD_PATH.each do |lpe|
39
+ next unless feat.start_with?(lpe)
40
+
41
+ # /a/b/lib/my/foo.rb
42
+ # ^^^^^^^^^
43
+ short = feat[(lpe.length + 1)..]
44
+ stripped = strip_extension_if_elidable(short)
45
+ @lfi[short] = hash
46
+ @lfi[stripped] = hash
47
+ end
48
+ end
49
+ end
50
+
51
+ # We've optimized for initialize and register to be fast, and purge to be tolerable.
52
+ # If access patterns make this not-okay, we can lazy-invert the LFI on
53
+ # first purge and work from there.
54
+ def purge(feature)
55
+ @mutex.synchronize do
56
+ feat_hash = feature.hash
57
+ @lfi.reject! { |_, hash| hash == feat_hash }
58
+ end
59
+ end
60
+
61
+ def purge_multi(features)
62
+ rejected_hashes = features.each_with_object({}) { |f, h| h[f.hash] = true }
63
+ @mutex.synchronize do
64
+ @lfi.reject! { |_, hash| rejected_hashes.key?(hash) }
65
+ end
66
+ end
67
+
68
+ def key?(feature)
69
+ @mutex.synchronize { @lfi.key?(feature) }
70
+ end
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
+
91
+ # There is a relatively uncommon case where we could miss adding an
92
+ # entry:
93
+ #
94
+ # If the user asked for e.g. `require 'bundler'`, and we went through the
95
+ # `FALLBACK_SCAN` pathway in `kernel_require.rb` and therefore did not
96
+ # pass `long` (the full expanded absolute path), then we did are not able
97
+ # to confidently add the `bundler.rb` form to @lfi.
98
+ #
99
+ # We could either:
100
+ #
101
+ # 1. Just add `bundler.rb`, `bundler.so`, and so on, which is close but
102
+ # not quite right; or
103
+ # 2. Inspect $LOADED_FEATURES upon return from yield to find the matching
104
+ # entry.
105
+ def register(short, long)
106
+ return if Bootsnap.absolute_path?(short)
107
+
108
+ hash = long.hash
109
+
110
+ # Do we have a filename with an elidable extension, e.g.,
111
+ # 'bundler.rb', or 'libgit2.so'?
112
+ altname = if extension_elidable?(short)
113
+ # Strip the extension off, e.g. 'bundler.rb' -> 'bundler'.
114
+ strip_extension_if_elidable(short)
115
+ elsif long && (ext = File.extname(long.freeze))
116
+ # We already know the extension of the actual file this
117
+ # resolves to, so put that back on.
118
+ short + ext
119
+ end
120
+
121
+ @mutex.synchronize do
122
+ @lfi[short] = hash
123
+ (@lfi[altname] = hash) if altname
124
+ end
125
+ end
126
+
127
+ private
128
+
129
+ STRIP_EXTENSION = /\.[^.]*?$/.freeze
130
+ private_constant(:STRIP_EXTENSION)
131
+
132
+ # Might Ruby automatically search for this extension if
133
+ # someone tries to 'require' the file without it? E.g. Ruby
134
+ # will implicitly try 'x.rb' if you ask for 'x'.
135
+ #
136
+ # This is complex and platform-dependent, and the Ruby docs are a little
137
+ # handwavy about what will be tried when and in what order.
138
+ # So optimistically pretend that all known elidable extensions
139
+ # will be tried on all platforms, and that people are unlikely
140
+ # to name files in a way that assumes otherwise.
141
+ # (E.g. It's unlikely that someone will know that their code
142
+ # will _never_ run on MacOS, and therefore think they can get away
143
+ # with calling a Ruby file 'x.dylib.rb' and then requiring it as 'x.dylib'.)
144
+ #
145
+ # See <https://docs.ruby-lang.org/en/master/Kernel.html#method-i-require>.
146
+ def extension_elidable?(feature)
147
+ feature.to_s.end_with?(".rb", ".so", ".o", ".dll", ".dylib")
148
+ end
149
+
150
+ def strip_extension_if_elidable(feature)
151
+ if extension_elidable?(feature)
152
+ feature.sub(STRIP_EXTENSION, "")
153
+ else
154
+ feature
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "path_scanner"
4
+
5
+ module Bootsnap
6
+ module LoadPathCache
7
+ class Path
8
+ # A path is considered 'stable' if it is part of a Gem.path or the ruby
9
+ # distribution. When adding or removing files in these paths, the cache
10
+ # must be cleared before the change will be noticed.
11
+ def stable?
12
+ stability == STABLE
13
+ end
14
+
15
+ # A path is considered volatile if it doesn't live under a Gem.path or
16
+ # the ruby distribution root. These paths are scanned for new additions
17
+ # more frequently.
18
+ def volatile?
19
+ stability == VOLATILE
20
+ end
21
+
22
+ attr_reader(:path)
23
+
24
+ def initialize(path, real: false)
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
44
+ end
45
+
46
+ # True if the path exists, but represents a non-directory object
47
+ def non_directory?
48
+ !File.stat(path).directory?
49
+ rescue Errno::ENOENT, Errno::ENOTDIR, Errno::EINVAL
50
+ false
51
+ end
52
+
53
+ def relative?
54
+ !path.start_with?(SLASH)
55
+ end
56
+
57
+ # Return a list of all the requirable files and all of the subdirectories
58
+ # of this +Path+.
59
+ def entries_and_dirs(store)
60
+ if stable?
61
+ # the cached_mtime field is unused for 'stable' paths, but is
62
+ # set to zero anyway, just in case we change the stability heuristics.
63
+ _, entries, dirs = store.get(expanded_path)
64
+ return [entries, dirs] if entries # cache hit
65
+
66
+ entries, dirs = scan!
67
+ store.set(expanded_path, [0, entries, dirs])
68
+ return [entries, dirs]
69
+ end
70
+
71
+ cached_mtime, entries, dirs = store.get(expanded_path)
72
+
73
+ current_mtime = latest_mtime(expanded_path, dirs || [])
74
+ return [[], []] if current_mtime == -1 # path does not exist
75
+ return [entries, dirs] if cached_mtime == current_mtime
76
+
77
+ entries, dirs = scan!
78
+ store.set(expanded_path, [current_mtime, entries, dirs])
79
+ [entries, dirs]
80
+ end
81
+
82
+ def expanded_path
83
+ if @real
84
+ path
85
+ else
86
+ @expanded_path ||= File.expand_path(path).freeze
87
+ end
88
+ end
89
+
90
+ private
91
+
92
+ def scan! # (expensive) returns [entries, dirs]
93
+ PathScanner.call(expanded_path)
94
+ end
95
+
96
+ # last time a directory was modified in this subtree. +dirs+ should be a
97
+ # list of relative paths to directories under +path+. e.g. for /a/b and
98
+ # /a/b/c, pass ('/a/b', ['c'])
99
+ def latest_mtime(path, dirs)
100
+ max = -1
101
+ ["", *dirs].each do |dir|
102
+ curr = begin
103
+ File.mtime("#{path}/#{dir}").to_i
104
+ rescue Errno::ENOENT, Errno::ENOTDIR, Errno::EINVAL
105
+ -1
106
+ end
107
+ max = curr if curr > max
108
+ end
109
+ max
110
+ end
111
+
112
+ # a Path can be either stable of volatile, depending on how frequently we
113
+ # expect its contents may change. Stable paths aren't rescanned nearly as
114
+ # often.
115
+ STABLE = :stable
116
+ VOLATILE = :volatile
117
+
118
+ # Built-in ruby lib stuff doesn't change, but things can occasionally be
119
+ # installed into sitedir, which generally lives under rubylibdir.
120
+ RUBY_LIBDIR = RbConfig::CONFIG["rubylibdir"]
121
+ RUBY_SITEDIR = RbConfig::CONFIG["sitedir"]
122
+
123
+ def stability
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
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end