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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +401 -0
- data/LICENSE.txt +22 -0
- data/README.md +358 -0
- data/exe/bootsnap +5 -0
- data/ext/bootsnap/bootsnap.c +1135 -0
- data/ext/bootsnap/bootsnap.h +6 -0
- data/ext/bootsnap/extconf.rb +33 -0
- data/lib/bootsnap/3.2/bootsnap.so +0 -0
- data/lib/bootsnap/3.3/bootsnap.so +0 -0
- data/lib/bootsnap/3.4/bootsnap.so +0 -0
- data/lib/bootsnap/bootsnap.so +0 -0
- data/lib/bootsnap/bootsnap_ext.rb +7 -0
- data/lib/bootsnap/bundler.rb +16 -0
- data/lib/bootsnap/cli/worker_pool.rb +208 -0
- data/lib/bootsnap/cli.rb +285 -0
- data/lib/bootsnap/compile_cache/iseq.rb +123 -0
- data/lib/bootsnap/compile_cache/json.rb +89 -0
- data/lib/bootsnap/compile_cache/yaml.rb +337 -0
- data/lib/bootsnap/compile_cache.rb +52 -0
- data/lib/bootsnap/explicit_require.rb +56 -0
- data/lib/bootsnap/load_path_cache/cache.rb +244 -0
- data/lib/bootsnap/load_path_cache/change_observer.rb +84 -0
- data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +37 -0
- data/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb +19 -0
- data/lib/bootsnap/load_path_cache/loaded_features_index.rb +159 -0
- data/lib/bootsnap/load_path_cache/path.rb +136 -0
- data/lib/bootsnap/load_path_cache/path_scanner.rb +81 -0
- data/lib/bootsnap/load_path_cache/store.rb +132 -0
- data/lib/bootsnap/load_path_cache.rb +80 -0
- data/lib/bootsnap/setup.rb +5 -0
- data/lib/bootsnap/version.rb +5 -0
- data/lib/bootsnap.rb +164 -0
- metadata +94 -0
@@ -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
|