mini-max-rb 0.0.1

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 (30) hide show
  1. checksums.yaml +7 -0
  2. data/bootsnap-1.24.6/CHANGELOG.md +473 -0
  3. data/bootsnap-1.24.6/LICENSE.txt +22 -0
  4. data/bootsnap-1.24.6/README.md +391 -0
  5. data/bootsnap-1.24.6/exe/bootsnap +5 -0
  6. data/bootsnap-1.24.6/ext/bootsnap/bootsnap.c +1235 -0
  7. data/bootsnap-1.24.6/ext/bootsnap/extconf.rb +34 -0
  8. data/bootsnap-1.24.6/lib/bootsnap/bundler.rb +16 -0
  9. data/bootsnap-1.24.6/lib/bootsnap/cli/worker_pool.rb +208 -0
  10. data/bootsnap-1.24.6/lib/bootsnap/cli.rb +258 -0
  11. data/bootsnap-1.24.6/lib/bootsnap/compile_cache/iseq.rb +229 -0
  12. data/bootsnap-1.24.6/lib/bootsnap/compile_cache/ruby_bug_22023_canary.rb +1 -0
  13. data/bootsnap-1.24.6/lib/bootsnap/compile_cache/yaml.rb +344 -0
  14. data/bootsnap-1.24.6/lib/bootsnap/compile_cache.rb +47 -0
  15. data/bootsnap-1.24.6/lib/bootsnap/explicit_require.rb +56 -0
  16. data/bootsnap-1.24.6/lib/bootsnap/load_path_cache/cache.rb +241 -0
  17. data/bootsnap-1.24.6/lib/bootsnap/load_path_cache/change_observer.rb +84 -0
  18. data/bootsnap-1.24.6/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +42 -0
  19. data/bootsnap-1.24.6/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb +19 -0
  20. data/bootsnap-1.24.6/lib/bootsnap/load_path_cache/loaded_features_index.rb +159 -0
  21. data/bootsnap-1.24.6/lib/bootsnap/load_path_cache/path.rb +143 -0
  22. data/bootsnap-1.24.6/lib/bootsnap/load_path_cache/path_scanner.rb +127 -0
  23. data/bootsnap-1.24.6/lib/bootsnap/load_path_cache/store.rb +132 -0
  24. data/bootsnap-1.24.6/lib/bootsnap/load_path_cache.rb +80 -0
  25. data/bootsnap-1.24.6/lib/bootsnap/rake.rb +14 -0
  26. data/bootsnap-1.24.6/lib/bootsnap/setup.rb +5 -0
  27. data/bootsnap-1.24.6/lib/bootsnap/version.rb +5 -0
  28. data/bootsnap-1.24.6/lib/bootsnap.rb +202 -0
  29. data/mini-max-rb.gemspec +12 -0
  30. metadata +69 -0
@@ -0,0 +1,241 @@
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! do |f|
15
+ if File.exist?(f)
16
+ File.realpath(f).freeze
17
+ elsif f.frozen?
18
+ f
19
+ else
20
+ f.dup.freeze
21
+ end
22
+ end
23
+ @has_relative_paths = nil
24
+ reinitialize
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
+ @generated_at = now
128
+ push_paths_locked(*@path_obj)
129
+ end
130
+ end
131
+
132
+ private
133
+
134
+ def dir_changed?
135
+ @prev_dir ||= Dir.pwd
136
+ if @prev_dir == Dir.pwd
137
+ false
138
+ else
139
+ @prev_dir = Dir.pwd
140
+ true
141
+ end
142
+ end
143
+
144
+ def push_paths_locked(*paths)
145
+ @store.transaction do
146
+ paths.map(&:to_s).each do |path|
147
+ p = Path.new(path)
148
+ @has_relative_paths = true if p.relative?
149
+ next if p.non_directory?
150
+
151
+ p = p.to_realpath
152
+
153
+ expanded_path = p.expanded_path
154
+ entries = p.entries(@store)
155
+ # push -> low precedence -> set only if unset
156
+ entries.each { |rel| @index[rel] ||= expanded_path }
157
+ end
158
+ end
159
+ end
160
+
161
+ def unshift_paths_locked(*paths)
162
+ @store.transaction do
163
+ paths.map(&:to_s).reverse_each do |path|
164
+ p = Path.new(path)
165
+ next if p.non_directory?
166
+
167
+ p = p.to_realpath
168
+
169
+ expanded_path = p.expanded_path
170
+ entries = p.entries(@store)
171
+ # unshift -> high precedence -> unconditional set
172
+ entries.each { |rel| @index[rel] = expanded_path }
173
+ end
174
+ end
175
+ end
176
+
177
+ def expand_path(feature)
178
+ maybe_append_extension(File.expand_path(feature))
179
+ end
180
+
181
+ def stale?
182
+ @development_mode && @generated_at + AGE_THRESHOLD < now
183
+ end
184
+
185
+ def now
186
+ Process.clock_gettime(Process::CLOCK_MONOTONIC).to_i
187
+ end
188
+
189
+ if DLEXT2
190
+ def search_index(feature)
191
+ try_index(feature + DOT_RB) ||
192
+ try_index(feature + DLEXT) ||
193
+ try_index(feature + DLEXT2) ||
194
+ try_index(feature)
195
+ end
196
+
197
+ def maybe_append_extension(feature)
198
+ try_ext(feature + DOT_RB) ||
199
+ try_ext(feature + DLEXT) ||
200
+ try_ext(feature + DLEXT2) ||
201
+ feature
202
+ end
203
+ else
204
+ def search_index(feature)
205
+ try_index(feature + DOT_RB) || try_index(feature + DLEXT) || try_index(feature)
206
+ end
207
+
208
+ def maybe_append_extension(feature)
209
+ try_ext(feature + DOT_RB) || try_ext(feature + DLEXT) || feature
210
+ end
211
+ end
212
+
213
+ s = rand.to_s.force_encoding(Encoding::US_ASCII).freeze
214
+ if s.respond_to?(:-@)
215
+ if ((-s).equal?(s) && (-s.dup).equal?(s)) || RUBY_VERSION >= "2.7"
216
+ def try_index(feature)
217
+ if (path = @index[feature])
218
+ -File.join(path, feature).freeze
219
+ end
220
+ end
221
+ else
222
+ def try_index(feature)
223
+ if (path = @index[feature])
224
+ -File.join(path, feature).untaint
225
+ end
226
+ end
227
+ end
228
+ else
229
+ def try_index(feature)
230
+ if (path = @index[feature])
231
+ File.join(path, feature)
232
+ end
233
+ end
234
+ end
235
+
236
+ def try_ext(feature)
237
+ feature if File.exist?(feature)
238
+ end
239
+ end
240
+ end
241
+ 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,42 @@
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)
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
+
19
+ # The file we required may have unloaded the cache
20
+ resolved = Bootsnap::LoadPathCache.loaded_features_index&.identify(string_path, cursor)
21
+ Bootsnap::LoadPathCache.loaded_features_index&.register(string_path, resolved)
22
+
23
+ return ret
24
+ else
25
+ return require_without_bootsnap(path)
26
+ end
27
+ elsif false == resolved
28
+ return false
29
+ elsif resolved.nil?
30
+ return require_without_bootsnap(path)
31
+ else
32
+ # Note that require registers to $LOADED_FEATURES while load does not.
33
+ ret = require_without_bootsnap(resolved)
34
+
35
+ # The file we required may have unloaded the cache
36
+ Bootsnap::LoadPathCache.loaded_features_index&.register(string_path, resolved)
37
+ return ret
38
+ end
39
+ end
40
+
41
+ private :require
42
+ 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.to_h { |f| [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,143 @@
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 of this +Path+.
58
+ def entries(store)
59
+ if stable?
60
+ # the cached_mtime field is unused for 'stable' paths, but is
61
+ # set to zero anyway, just in case we change the stability heuristics.
62
+ _, entries, = store.get(expanded_path)
63
+ return entries if entries # cache hit
64
+
65
+ entries = PathScanner.call(expanded_path)
66
+ store.set(expanded_path, [0, entries])
67
+ return entries
68
+ end
69
+
70
+ cached_mtime, entries = store.get(expanded_path)
71
+
72
+ current_mtime = latest_mtime(expanded_path, entries || [])
73
+ return [] if current_mtime == -1 # path does not exist
74
+ return entries if cached_mtime == current_mtime
75
+
76
+ entries = PathScanner.call(expanded_path)
77
+ store.set(expanded_path, [current_mtime, entries])
78
+ entries
79
+ end
80
+
81
+ def expanded_path
82
+ if @real
83
+ path
84
+ else
85
+ @expanded_path ||= File.expand_path(path).freeze
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ # last time a directory was modified in this subtree. +dirs+ should be a
92
+ # list of relative paths to directories under +path+. e.g. for /a/b and
93
+ # /a/b/c, pass ('/a/b', ['c'])
94
+ def latest_mtime(root_path, entries)
95
+ max = begin
96
+ File.mtime(root_path).to_i
97
+ rescue Errno::ENOENT, Errno::ENOTDIR, Errno::EINVAL
98
+ -1
99
+ end
100
+
101
+ visited = {"." => true}
102
+
103
+ entries.each do |relpath|
104
+ dirname = File.dirname(relpath)
105
+ visited[dirname] ||= begin
106
+ begin
107
+ current = File.mtime(File.join(root_path, dirname)).to_i
108
+ max = current if current > max
109
+ rescue Errno::ENOENT, Errno::ENOTDIR, Errno::EINVAL
110
+ # ignore
111
+ end
112
+ true
113
+ end
114
+ end
115
+
116
+ max
117
+ end
118
+
119
+ # a Path can be either stable of volatile, depending on how frequently we
120
+ # expect its contents may change. Stable paths aren't rescanned nearly as
121
+ # often.
122
+ STABLE = :stable
123
+ VOLATILE = :volatile
124
+
125
+ # Built-in ruby lib stuff doesn't change, but things can occasionally be
126
+ # installed into sitedir, which generally lives under rubylibdir.
127
+ RUBY_LIBDIR = RbConfig::CONFIG["rubylibdir"]
128
+ RUBY_SITEDIR = RbConfig::CONFIG["sitedir"]
129
+
130
+ def stability
131
+ @stability ||= if Gem.path.detect { |p| expanded_path.start_with?(p.to_s) }
132
+ STABLE
133
+ elsif Bootsnap.bundler? && expanded_path.start_with?(Bundler.bundle_path.to_s)
134
+ STABLE
135
+ elsif expanded_path.start_with?(RUBY_LIBDIR) && !expanded_path.start_with?(RUBY_SITEDIR)
136
+ STABLE
137
+ else
138
+ VOLATILE
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end