bootsnap 1.4.0 → 1.9.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +126 -0
- data/README.md +68 -13
- data/exe/bootsnap +5 -0
- data/ext/bootsnap/bootsnap.c +285 -87
- data/ext/bootsnap/extconf.rb +20 -14
- data/lib/bootsnap/bundler.rb +1 -0
- data/lib/bootsnap/cli/worker_pool.rb +135 -0
- data/lib/bootsnap/cli.rb +281 -0
- data/lib/bootsnap/compile_cache/iseq.rb +24 -7
- data/lib/bootsnap/compile_cache/json.rb +79 -0
- data/lib/bootsnap/compile_cache/yaml.rb +145 -39
- data/lib/bootsnap/compile_cache.rb +25 -3
- data/lib/bootsnap/explicit_require.rb +1 -0
- data/lib/bootsnap/load_path_cache/cache.rb +44 -9
- data/lib/bootsnap/load_path_cache/change_observer.rb +5 -1
- data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +30 -6
- data/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb +11 -0
- data/lib/bootsnap/load_path_cache/loaded_features_index.rb +43 -11
- data/lib/bootsnap/load_path_cache/path.rb +3 -2
- data/lib/bootsnap/load_path_cache/path_scanner.rb +53 -27
- data/lib/bootsnap/load_path_cache/realpath_cache.rb +5 -5
- data/lib/bootsnap/load_path_cache/store.rb +28 -14
- data/lib/bootsnap/load_path_cache.rb +10 -16
- data/lib/bootsnap/setup.rb +2 -33
- data/lib/bootsnap/version.rb +2 -1
- data/lib/bootsnap.rb +96 -17
- metadata +18 -29
- data/.gitignore +0 -17
- data/.rubocop.yml +0 -20
- data/.travis.yml +0 -24
- data/CODE_OF_CONDUCT.md +0 -74
- data/CONTRIBUTING.md +0 -21
- data/Gemfile +0 -8
- data/README.jp.md +0 -231
- data/Rakefile +0 -12
- data/bin/ci +0 -10
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/bin/test-minimal-support +0 -7
- data/bin/testunit +0 -8
- data/bootsnap.gemspec +0 -45
- data/dev.yml +0 -10
- data/lib/bootsnap/load_path_cache/core_ext/active_support.rb +0 -100
- data/shipit.rubygems.yml +0 -4
@@ -1,57 +1,163 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require('bootsnap/bootsnap')
|
2
3
|
|
3
4
|
module Bootsnap
|
4
5
|
module CompileCache
|
5
6
|
module YAML
|
6
7
|
class << self
|
7
|
-
attr_accessor(:msgpack_factory)
|
8
|
-
end
|
9
|
-
|
10
|
-
def self.input_to_storage(contents, _)
|
11
|
-
raise(Uncompilable) if contents.index("!ruby/object")
|
12
|
-
obj = ::YAML.load(contents)
|
13
|
-
msgpack_factory.packer.write(obj).to_s
|
14
|
-
rescue NoMethodError, RangeError
|
15
|
-
# if the object included things that we can't serialize, fall back to
|
16
|
-
# Marshal. It's a bit slower, but can encode anything yaml can.
|
17
|
-
# NoMethodError is unexpected types; RangeError is Bignums
|
18
|
-
Marshal.dump(obj)
|
19
|
-
end
|
8
|
+
attr_accessor(:msgpack_factory, :cache_dir, :supported_options)
|
20
9
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
else
|
28
|
-
msgpack_factory.unpacker.feed(data).read
|
10
|
+
def input_to_storage(contents, _)
|
11
|
+
obj = strict_load(contents)
|
12
|
+
msgpack_factory.dump(obj)
|
13
|
+
rescue NoMethodError, RangeError
|
14
|
+
# The object included things that we can't serialize
|
15
|
+
raise(Uncompilable)
|
29
16
|
end
|
30
|
-
end
|
31
17
|
|
32
|
-
|
33
|
-
|
34
|
-
|
18
|
+
def storage_to_output(data, kwargs)
|
19
|
+
if kwargs && kwargs.key?(:symbolize_names)
|
20
|
+
kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names)
|
21
|
+
end
|
22
|
+
msgpack_factory.load(data, kwargs)
|
23
|
+
end
|
35
24
|
|
36
|
-
|
37
|
-
|
38
|
-
|
25
|
+
def input_to_output(data, kwargs)
|
26
|
+
if ::YAML.respond_to?(:unsafe_load)
|
27
|
+
::YAML.unsafe_load(data, **(kwargs || {}))
|
28
|
+
else
|
29
|
+
::YAML.load(data, **(kwargs || {}))
|
30
|
+
end
|
31
|
+
end
|
39
32
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
33
|
+
def strict_load(payload, *args)
|
34
|
+
ast = ::YAML.parse(payload)
|
35
|
+
return ast unless ast
|
36
|
+
strict_visitor.create(*args).visit(ast)
|
37
|
+
end
|
38
|
+
ruby2_keywords :strict_load if respond_to?(:ruby2_keywords, true)
|
46
39
|
|
47
|
-
|
48
|
-
|
49
|
-
Bootsnap::CompileCache::Native.fetch(
|
40
|
+
def precompile(path, cache_dir: YAML.cache_dir)
|
41
|
+
Bootsnap::CompileCache::Native.precompile(
|
50
42
|
cache_dir,
|
51
|
-
path,
|
52
|
-
Bootsnap::CompileCache::YAML
|
43
|
+
path.to_s,
|
44
|
+
Bootsnap::CompileCache::YAML,
|
53
45
|
)
|
54
46
|
end
|
47
|
+
|
48
|
+
def install!(cache_dir)
|
49
|
+
self.cache_dir = cache_dir
|
50
|
+
init!
|
51
|
+
::YAML.singleton_class.prepend(Patch)
|
52
|
+
end
|
53
|
+
|
54
|
+
def init!
|
55
|
+
require('yaml')
|
56
|
+
require('msgpack')
|
57
|
+
require('date')
|
58
|
+
|
59
|
+
if Patch.method_defined?(:unsafe_load_file) && !::YAML.respond_to?(:unsafe_load_file)
|
60
|
+
Patch.send(:remove_method, :unsafe_load_file)
|
61
|
+
end
|
62
|
+
if Patch.method_defined?(:load_file) && ::YAML::VERSION >= '4'
|
63
|
+
Patch.send(:remove_method, :load_file)
|
64
|
+
end
|
65
|
+
|
66
|
+
# MessagePack serializes symbols as strings by default.
|
67
|
+
# We want them to roundtrip cleanly, so we use a custom factory.
|
68
|
+
# see: https://github.com/msgpack/msgpack-ruby/pull/122
|
69
|
+
factory = MessagePack::Factory.new
|
70
|
+
factory.register_type(0x00, Symbol)
|
71
|
+
|
72
|
+
if defined? MessagePack::Timestamp
|
73
|
+
factory.register_type(
|
74
|
+
MessagePack::Timestamp::TYPE, # or just -1
|
75
|
+
Time,
|
76
|
+
packer: MessagePack::Time::Packer,
|
77
|
+
unpacker: MessagePack::Time::Unpacker
|
78
|
+
)
|
79
|
+
|
80
|
+
marshal_fallback = {
|
81
|
+
packer: ->(value) { Marshal.dump(value) },
|
82
|
+
unpacker: ->(payload) { Marshal.load(payload) },
|
83
|
+
}
|
84
|
+
{
|
85
|
+
Date => 0x01,
|
86
|
+
Regexp => 0x02,
|
87
|
+
}.each do |type, code|
|
88
|
+
factory.register_type(code, type, marshal_fallback)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
self.msgpack_factory = factory
|
93
|
+
|
94
|
+
self.supported_options = []
|
95
|
+
params = ::YAML.method(:load).parameters
|
96
|
+
if params.include?([:key, :symbolize_names])
|
97
|
+
self.supported_options << :symbolize_names
|
98
|
+
end
|
99
|
+
if params.include?([:key, :freeze])
|
100
|
+
if factory.load(factory.dump('yaml'), freeze: true).frozen?
|
101
|
+
self.supported_options << :freeze
|
102
|
+
end
|
103
|
+
end
|
104
|
+
self.supported_options.freeze
|
105
|
+
end
|
106
|
+
|
107
|
+
def strict_visitor
|
108
|
+
self::NoTagsVisitor ||= Class.new(Psych::Visitors::ToRuby) do
|
109
|
+
def visit(target)
|
110
|
+
if target.tag
|
111
|
+
raise Uncompilable, "YAML tags are not supported: #{target.tag}"
|
112
|
+
end
|
113
|
+
super
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
module Patch
|
120
|
+
def load_file(path, *args)
|
121
|
+
return super if args.size > 1
|
122
|
+
if kwargs = args.first
|
123
|
+
return super unless kwargs.is_a?(Hash)
|
124
|
+
return super unless (kwargs.keys - ::Bootsnap::CompileCache::YAML.supported_options).empty?
|
125
|
+
end
|
126
|
+
|
127
|
+
begin
|
128
|
+
::Bootsnap::CompileCache::Native.fetch(
|
129
|
+
Bootsnap::CompileCache::YAML.cache_dir,
|
130
|
+
File.realpath(path),
|
131
|
+
::Bootsnap::CompileCache::YAML,
|
132
|
+
kwargs,
|
133
|
+
)
|
134
|
+
rescue Errno::EACCES
|
135
|
+
::Bootsnap::CompileCache.permission_error(path)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true)
|
140
|
+
|
141
|
+
def unsafe_load_file(path, *args)
|
142
|
+
return super if args.size > 1
|
143
|
+
if kwargs = args.first
|
144
|
+
return super unless kwargs.is_a?(Hash)
|
145
|
+
return super unless (kwargs.keys - ::Bootsnap::CompileCache::YAML.supported_options).empty?
|
146
|
+
end
|
147
|
+
|
148
|
+
begin
|
149
|
+
::Bootsnap::CompileCache::Native.fetch(
|
150
|
+
Bootsnap::CompileCache::YAML.cache_dir,
|
151
|
+
File.realpath(path),
|
152
|
+
::Bootsnap::CompileCache::YAML,
|
153
|
+
kwargs,
|
154
|
+
)
|
155
|
+
rescue Errno::EACCES
|
156
|
+
::Bootsnap::CompileCache.permission_error(path)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
ruby2_keywords :unsafe_load_file if respond_to?(:ruby2_keywords, true)
|
55
161
|
end
|
56
162
|
end
|
57
163
|
end
|
@@ -1,6 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module Bootsnap
|
2
3
|
module CompileCache
|
3
|
-
|
4
|
+
Error = Class.new(StandardError)
|
5
|
+
PermissionError = Class.new(Error)
|
6
|
+
|
7
|
+
def self.setup(cache_dir:, iseq:, yaml:, json:)
|
4
8
|
if iseq
|
5
9
|
if supported?
|
6
10
|
require_relative('compile_cache/iseq')
|
@@ -18,12 +22,30 @@ module Bootsnap
|
|
18
22
|
warn("[bootsnap/setup] YAML parsing caching is not supported on this implementation of Ruby")
|
19
23
|
end
|
20
24
|
end
|
25
|
+
|
26
|
+
if json
|
27
|
+
if supported?
|
28
|
+
require_relative('compile_cache/json')
|
29
|
+
Bootsnap::CompileCache::JSON.install!(cache_dir)
|
30
|
+
elsif $VERBOSE
|
31
|
+
warn("[bootsnap/setup] JSON parsing caching is not supported on this implementation of Ruby")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.permission_error(path)
|
37
|
+
cpath = Bootsnap::CompileCache::ISeq.cache_dir
|
38
|
+
raise(
|
39
|
+
PermissionError,
|
40
|
+
"bootsnap doesn't have permission to write cache entries in '#{cpath}' " \
|
41
|
+
"(or, less likely, doesn't have permission to read '#{path}')",
|
42
|
+
)
|
21
43
|
end
|
22
44
|
|
23
45
|
def self.supported?
|
24
|
-
# only enable on 'ruby' (MRI), POSIX (darwin, linux, *bsd), and >= 2.3.0
|
46
|
+
# only enable on 'ruby' (MRI), POSIX (darwin, linux, *bsd), Windows (RubyInstaller2) and >= 2.3.0
|
25
47
|
RUBY_ENGINE == 'ruby' &&
|
26
|
-
RUBY_PLATFORM =~ /darwin|linux|bsd/ &&
|
48
|
+
RUBY_PLATFORM =~ /darwin|linux|bsd|mswin|mingw|cygwin/ &&
|
27
49
|
Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.3.0")
|
28
50
|
end
|
29
51
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative('../explicit_require')
|
2
4
|
|
3
5
|
module Bootsnap
|
@@ -8,8 +10,8 @@ module Bootsnap
|
|
8
10
|
def initialize(store, path_obj, development_mode: false)
|
9
11
|
@development_mode = development_mode
|
10
12
|
@store = store
|
11
|
-
@mutex =
|
12
|
-
@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) }
|
13
15
|
@has_relative_paths = nil
|
14
16
|
reinitialize
|
15
17
|
end
|
@@ -44,9 +46,9 @@ module Bootsnap
|
|
44
46
|
# loadpath.
|
45
47
|
def find(feature)
|
46
48
|
reinitialize if (@has_relative_paths && dir_changed?) || stale?
|
47
|
-
feature = feature.to_s
|
49
|
+
feature = feature.to_s.freeze
|
48
50
|
return feature if absolute_path?(feature)
|
49
|
-
return
|
51
|
+
return expand_path(feature) if feature.start_with?('./')
|
50
52
|
@mutex.synchronize do
|
51
53
|
x = search_index(feature)
|
52
54
|
return x if x
|
@@ -65,7 +67,7 @@ module Bootsnap
|
|
65
67
|
# native dynamic extension, e.g. .bundle or .so), we know it was a
|
66
68
|
# failure and there's nothing more we can do to find the file.
|
67
69
|
# no extension, .rb, (.bundle or .so)
|
68
|
-
when '', *CACHED_EXTENSIONS
|
70
|
+
when '', *CACHED_EXTENSIONS
|
69
71
|
nil
|
70
72
|
# Ruby allows specifying native extensions as '.so' even when DLEXT
|
71
73
|
# is '.bundle'. This is where we handle that case.
|
@@ -142,7 +144,7 @@ module Bootsnap
|
|
142
144
|
expanded_path = p.expanded_path
|
143
145
|
entries, dirs = p.entries_and_dirs(@store)
|
144
146
|
# push -> low precedence -> set only if unset
|
145
|
-
dirs.each { |dir| @dirs[dir]
|
147
|
+
dirs.each { |dir| @dirs[dir] ||= path }
|
146
148
|
entries.each { |rel| @index[rel] ||= expanded_path }
|
147
149
|
end
|
148
150
|
end
|
@@ -162,6 +164,10 @@ module Bootsnap
|
|
162
164
|
end
|
163
165
|
end
|
164
166
|
|
167
|
+
def expand_path(feature)
|
168
|
+
maybe_append_extension(File.expand_path(feature))
|
169
|
+
end
|
170
|
+
|
165
171
|
def stale?
|
166
172
|
@development_mode && @generated_at + AGE_THRESHOLD < now
|
167
173
|
end
|
@@ -174,17 +180,46 @@ module Bootsnap
|
|
174
180
|
def search_index(f)
|
175
181
|
try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f + DLEXT2) || try_index(f)
|
176
182
|
end
|
183
|
+
|
184
|
+
def maybe_append_extension(f)
|
185
|
+
try_ext(f + DOT_RB) || try_ext(f + DLEXT) || try_ext(f + DLEXT2) || f
|
186
|
+
end
|
177
187
|
else
|
178
188
|
def search_index(f)
|
179
189
|
try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f)
|
180
190
|
end
|
191
|
+
|
192
|
+
def maybe_append_extension(f)
|
193
|
+
try_ext(f + DOT_RB) || try_ext(f + DLEXT) || f
|
194
|
+
end
|
181
195
|
end
|
182
196
|
|
183
|
-
|
184
|
-
|
185
|
-
|
197
|
+
s = rand.to_s.force_encoding(Encoding::US_ASCII).freeze
|
198
|
+
if s.respond_to?(:-@)
|
199
|
+
if (-s).equal?(s) && (-s.dup).equal?(s) || RUBY_VERSION >= '2.7'
|
200
|
+
def try_index(f)
|
201
|
+
if (p = @index[f])
|
202
|
+
-(File.join(p, f).freeze)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
else
|
206
|
+
def try_index(f)
|
207
|
+
if (p = @index[f])
|
208
|
+
-File.join(p, f).untaint
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
else
|
213
|
+
def try_index(f)
|
214
|
+
if (p = @index[f])
|
215
|
+
File.join(p, f)
|
216
|
+
end
|
186
217
|
end
|
187
218
|
end
|
219
|
+
|
220
|
+
def try_ext(f)
|
221
|
+
f if File.exist?(f)
|
222
|
+
end
|
188
223
|
end
|
189
224
|
end
|
190
225
|
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module Bootsnap
|
2
3
|
module LoadPathCache
|
3
4
|
module ChangeObserver
|
@@ -14,18 +15,20 @@ module Bootsnap
|
|
14
15
|
@lpc_observer.push_paths(self, *entries.map(&:to_s))
|
15
16
|
super
|
16
17
|
end
|
18
|
+
alias_method :append, :push
|
17
19
|
|
18
20
|
def unshift(*entries)
|
19
21
|
@lpc_observer.unshift_paths(self, *entries.map(&:to_s))
|
20
22
|
super
|
21
23
|
end
|
24
|
+
alias_method :prepend, :unshift
|
22
25
|
|
23
26
|
def concat(entries)
|
24
27
|
@lpc_observer.push_paths(self, *entries.map(&:to_s))
|
25
28
|
super
|
26
29
|
end
|
27
30
|
|
28
|
-
# uniq! keeps the first
|
31
|
+
# uniq! keeps the first occurrence of each path, otherwise preserving
|
29
32
|
# order, preserving the effective load path
|
30
33
|
def uniq!(*args)
|
31
34
|
ret = super
|
@@ -53,6 +56,7 @@ module Bootsnap
|
|
53
56
|
end
|
54
57
|
|
55
58
|
def self.register(observer, arr)
|
59
|
+
return if arr.frozen? # can't register observer, but no need to.
|
56
60
|
arr.instance_variable_set(:@lpc_observer, observer)
|
57
61
|
arr.extend(ArrayMixin)
|
58
62
|
end
|
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module Bootsnap
|
2
3
|
module LoadPathCache
|
3
4
|
module CoreExt
|
4
5
|
def self.make_load_error(path)
|
5
|
-
err = LoadError.new("cannot load such file -- #{path}")
|
6
|
+
err = LoadError.new(+"cannot load such file -- #{path}")
|
7
|
+
err.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
|
6
8
|
err.define_singleton_method(:path) { path }
|
7
9
|
err
|
8
10
|
end
|
@@ -30,16 +32,24 @@ module Kernel
|
|
30
32
|
end
|
31
33
|
|
32
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)
|
33
38
|
rescue Bootsnap::LoadPathCache::ReturnFalse
|
34
39
|
false
|
35
40
|
rescue Bootsnap::LoadPathCache::FallbackScan
|
36
|
-
|
41
|
+
fallback = true
|
42
|
+
ensure
|
43
|
+
if fallback
|
44
|
+
require_with_bootsnap_lfi(path)
|
45
|
+
end
|
37
46
|
end
|
38
47
|
|
39
48
|
alias_method(:require_relative_without_bootsnap, :require_relative)
|
40
49
|
def require_relative(path)
|
50
|
+
location = caller_locations(1..1).first
|
41
51
|
realpath = Bootsnap::LoadPathCache.realpath_cache.call(
|
42
|
-
|
52
|
+
location.absolute_path || location.path, path
|
43
53
|
)
|
44
54
|
require(realpath)
|
45
55
|
end
|
@@ -51,15 +61,22 @@ module Kernel
|
|
51
61
|
end
|
52
62
|
|
53
63
|
# load also allows relative paths from pwd even when not in $:
|
54
|
-
if File.exist?(relative = File.expand_path(path))
|
64
|
+
if File.exist?(relative = File.expand_path(path).freeze)
|
55
65
|
return load_without_bootsnap(relative, wrap)
|
56
66
|
end
|
57
67
|
|
58
68
|
raise(Bootsnap::LoadPathCache::CoreExt.make_load_error(path))
|
69
|
+
rescue LoadError => e
|
70
|
+
e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
|
71
|
+
raise(e)
|
59
72
|
rescue Bootsnap::LoadPathCache::ReturnFalse
|
60
73
|
false
|
61
74
|
rescue Bootsnap::LoadPathCache::FallbackScan
|
62
|
-
|
75
|
+
fallback = true
|
76
|
+
ensure
|
77
|
+
if fallback
|
78
|
+
load_without_bootsnap(path, wrap)
|
79
|
+
end
|
63
80
|
end
|
64
81
|
end
|
65
82
|
|
@@ -74,9 +91,16 @@ class Module
|
|
74
91
|
# added to $LOADED_FEATURES and won't be able to hook that modification
|
75
92
|
# since it's done in C-land.
|
76
93
|
autoload_without_bootsnap(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
|
94
|
+
rescue LoadError => e
|
95
|
+
e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
|
96
|
+
raise(e)
|
77
97
|
rescue Bootsnap::LoadPathCache::ReturnFalse
|
78
98
|
false
|
79
99
|
rescue Bootsnap::LoadPathCache::FallbackScan
|
80
|
-
|
100
|
+
fallback = true
|
101
|
+
ensure
|
102
|
+
if fallback
|
103
|
+
autoload_without_bootsnap(const, path)
|
104
|
+
end
|
81
105
|
end
|
82
106
|
end
|
@@ -1,7 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
class << $LOADED_FEATURES
|
2
3
|
alias_method(:delete_without_bootsnap, :delete)
|
3
4
|
def delete(key)
|
4
5
|
Bootsnap::LoadPathCache.loaded_features_index.purge(key)
|
5
6
|
delete_without_bootsnap(key)
|
6
7
|
end
|
8
|
+
|
9
|
+
alias_method(:reject_without_bootsnap!, :reject!)
|
10
|
+
def reject!(&block)
|
11
|
+
backup = dup
|
12
|
+
|
13
|
+
# FIXME: if no block is passed we'd need to return a decorated iterator
|
14
|
+
reject_without_bootsnap!(&block)
|
15
|
+
|
16
|
+
Bootsnap::LoadPathCache.loaded_features_index.purge_multi(backup - self)
|
17
|
+
end
|
7
18
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Bootsnap
|
2
4
|
module LoadPathCache
|
3
5
|
# LoadedFeaturesIndex partially mirrors an internal structure in ruby that
|
@@ -24,7 +26,7 @@ module Bootsnap
|
|
24
26
|
class LoadedFeaturesIndex
|
25
27
|
def initialize
|
26
28
|
@lfi = {}
|
27
|
-
@mutex =
|
29
|
+
@mutex = Mutex.new
|
28
30
|
|
29
31
|
# In theory the user could mutate $LOADED_FEATURES and invalidate our
|
30
32
|
# cache. If this ever comes up in practice — or if you, the
|
@@ -38,7 +40,7 @@ module Bootsnap
|
|
38
40
|
# /a/b/lib/my/foo.rb
|
39
41
|
# ^^^^^^^^^
|
40
42
|
short = feat[(lpe.length + 1)..-1]
|
41
|
-
stripped =
|
43
|
+
stripped = strip_extension_if_elidable(short)
|
42
44
|
@lfi[short] = hash
|
43
45
|
@lfi[stripped] = hash
|
44
46
|
end
|
@@ -55,6 +57,13 @@ module Bootsnap
|
|
55
57
|
end
|
56
58
|
end
|
57
59
|
|
60
|
+
def purge_multi(features)
|
61
|
+
rejected_hashes = features.each_with_object({}) { |f, h| h[f.hash] = true }
|
62
|
+
@mutex.synchronize do
|
63
|
+
@lfi.reject! { |_, hash| rejected_hashes.key?(hash) }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
58
67
|
def key?(feature)
|
59
68
|
@mutex.synchronize { @lfi.key?(feature) }
|
60
69
|
end
|
@@ -85,13 +94,14 @@ module Bootsnap
|
|
85
94
|
|
86
95
|
hash = long.hash
|
87
96
|
|
88
|
-
#
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
#
|
97
|
+
# Do we have a filename with an elidable extension, e.g.,
|
98
|
+
# 'bundler.rb', or 'libgit2.so'?
|
99
|
+
altname = if extension_elidable?(short)
|
100
|
+
# Strip the extension off, e.g. 'bundler.rb' -> 'bundler'.
|
101
|
+
strip_extension_if_elidable(short)
|
102
|
+
elsif long && (ext = File.extname(long.freeze))
|
103
|
+
# We already know the extension of the actual file this
|
104
|
+
# resolves to, so put that back on.
|
95
105
|
short + ext
|
96
106
|
end
|
97
107
|
|
@@ -108,8 +118,30 @@ module Bootsnap
|
|
108
118
|
STRIP_EXTENSION = /\.[^.]*?$/
|
109
119
|
private_constant(:STRIP_EXTENSION)
|
110
120
|
|
111
|
-
|
112
|
-
|
121
|
+
# Might Ruby automatically search for this extension if
|
122
|
+
# someone tries to 'require' the file without it? E.g. Ruby
|
123
|
+
# will implicitly try 'x.rb' if you ask for 'x'.
|
124
|
+
#
|
125
|
+
# This is complex and platform-dependent, and the Ruby docs are a little
|
126
|
+
# handwavy about what will be tried when and in what order.
|
127
|
+
# So optimistically pretend that all known elidable extensions
|
128
|
+
# will be tried on all platforms, and that people are unlikely
|
129
|
+
# to name files in a way that assumes otherwise.
|
130
|
+
# (E.g. It's unlikely that someone will know that their code
|
131
|
+
# will _never_ run on MacOS, and therefore think they can get away
|
132
|
+
# with calling a Ruby file 'x.dylib.rb' and then requiring it as 'x.dylib'.)
|
133
|
+
#
|
134
|
+
# 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')
|
137
|
+
end
|
138
|
+
|
139
|
+
def strip_extension_if_elidable(f)
|
140
|
+
if extension_elidable?(f)
|
141
|
+
f.sub(STRIP_EXTENSION, '')
|
142
|
+
else
|
143
|
+
f
|
144
|
+
end
|
113
145
|
end
|
114
146
|
end
|
115
147
|
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require_relative('path_scanner')
|
2
3
|
|
3
4
|
module Bootsnap
|
@@ -20,7 +21,7 @@ module Bootsnap
|
|
20
21
|
attr_reader(:path)
|
21
22
|
|
22
23
|
def initialize(path)
|
23
|
-
@path = path.to_s
|
24
|
+
@path = path.to_s.freeze
|
24
25
|
end
|
25
26
|
|
26
27
|
# True if the path exists, but represents a non-directory object
|
@@ -59,7 +60,7 @@ module Bootsnap
|
|
59
60
|
end
|
60
61
|
|
61
62
|
def expanded_path
|
62
|
-
File.expand_path(path)
|
63
|
+
File.expand_path(path).freeze
|
63
64
|
end
|
64
65
|
|
65
66
|
private
|