bootsnap 1.4.4 → 1.9.4
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 +129 -0
- data/README.md +46 -15
- data/exe/bootsnap +5 -0
- data/ext/bootsnap/bootsnap.c +276 -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 +51 -11
- data/lib/bootsnap/compile_cache/json.rb +79 -0
- data/lib/bootsnap/compile_cache/yaml.rb +141 -39
- data/lib/bootsnap/compile_cache.rb +14 -4
- data/lib/bootsnap/explicit_require.rb +1 -0
- data/lib/bootsnap/load_path_cache/cache.rb +47 -26
- data/lib/bootsnap/load_path_cache/change_observer.rb +4 -1
- data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +18 -20
- data/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb +1 -0
- data/lib/bootsnap/load_path_cache/loaded_features_index.rb +51 -15
- data/lib/bootsnap/load_path_cache/path.rb +3 -2
- data/lib/bootsnap/load_path_cache/path_scanner.rb +50 -26
- data/lib/bootsnap/load_path_cache/realpath_cache.rb +5 -5
- data/lib/bootsnap/load_path_cache/store.rb +39 -15
- data/lib/bootsnap/load_path_cache.rb +3 -16
- data/lib/bootsnap/setup.rb +2 -36
- data/lib/bootsnap/version.rb +2 -1
- data/lib/bootsnap.rb +106 -17
- metadata +18 -32
- data/.github/CODEOWNERS +0 -2
- data/.github/probots.yml +0 -2
- data/.gitignore +0 -17
- data/.rubocop.yml +0 -20
- data/.travis.yml +0 -21
- 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 -106
- data/shipit.rubygems.yml +0 -0
@@ -1,61 +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
|
8
|
+
attr_accessor(:msgpack_factory, :cache_dir, :supported_options)
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
# NoMethodError is unexpected types; RangeError is Bignums
|
18
|
-
Marshal.dump(obj)
|
19
|
-
end
|
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)
|
16
|
+
end
|
20
17
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
24
|
+
|
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
|
32
|
+
|
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)
|
39
|
+
|
40
|
+
def precompile(path, cache_dir: YAML.cache_dir)
|
41
|
+
Bootsnap::CompileCache::Native.precompile(
|
42
|
+
cache_dir,
|
43
|
+
path.to_s,
|
44
|
+
Bootsnap::CompileCache::YAML,
|
45
|
+
)
|
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
|
29
105
|
end
|
30
|
-
end
|
31
106
|
|
32
|
-
|
33
|
-
|
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
|
34
117
|
end
|
35
118
|
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
39
138
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
46
147
|
|
47
|
-
klass = class << ::YAML; self; end
|
48
|
-
klass.send(:define_method, :load_file) do |path|
|
49
148
|
begin
|
50
|
-
Bootsnap::CompileCache::Native.fetch(
|
51
|
-
cache_dir,
|
52
|
-
path,
|
53
|
-
Bootsnap::CompileCache::YAML
|
149
|
+
::Bootsnap::CompileCache::Native.fetch(
|
150
|
+
Bootsnap::CompileCache::YAML.cache_dir,
|
151
|
+
File.realpath(path),
|
152
|
+
::Bootsnap::CompileCache::YAML,
|
153
|
+
kwargs,
|
54
154
|
)
|
55
155
|
rescue Errno::EACCES
|
56
|
-
Bootsnap::CompileCache.permission_error(path)
|
156
|
+
::Bootsnap::CompileCache.permission_error(path)
|
57
157
|
end
|
58
158
|
end
|
159
|
+
|
160
|
+
ruby2_keywords :unsafe_load_file if respond_to?(:ruby2_keywords, true)
|
59
161
|
end
|
60
162
|
end
|
61
163
|
end
|
@@ -1,9 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module Bootsnap
|
2
3
|
module CompileCache
|
3
4
|
Error = Class.new(StandardError)
|
4
5
|
PermissionError = Class.new(Error)
|
5
6
|
|
6
|
-
def self.setup(cache_dir:, iseq:, yaml:)
|
7
|
+
def self.setup(cache_dir:, iseq:, yaml:, json:)
|
7
8
|
if iseq
|
8
9
|
if supported?
|
9
10
|
require_relative('compile_cache/iseq')
|
@@ -21,6 +22,15 @@ module Bootsnap
|
|
21
22
|
warn("[bootsnap/setup] YAML parsing caching is not supported on this implementation of Ruby")
|
22
23
|
end
|
23
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
|
24
34
|
end
|
25
35
|
|
26
36
|
def self.permission_error(path)
|
@@ -28,14 +38,14 @@ module Bootsnap
|
|
28
38
|
raise(
|
29
39
|
PermissionError,
|
30
40
|
"bootsnap doesn't have permission to write cache entries in '#{cpath}' " \
|
31
|
-
"(or, less likely, doesn't have
|
41
|
+
"(or, less likely, doesn't have permission to read '#{path}')",
|
32
42
|
)
|
33
43
|
end
|
34
44
|
|
35
45
|
def self.supported?
|
36
|
-
# 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
|
37
47
|
RUBY_ENGINE == 'ruby' &&
|
38
|
-
RUBY_PLATFORM =~ /darwin|linux|bsd/ &&
|
48
|
+
RUBY_PLATFORM =~ /darwin|linux|bsd|mswin|mingw|cygwin/ &&
|
39
49
|
Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.3.0")
|
40
50
|
end
|
41
51
|
end
|
@@ -10,8 +10,8 @@ module Bootsnap
|
|
10
10
|
def initialize(store, path_obj, development_mode: false)
|
11
11
|
@development_mode = development_mode
|
12
12
|
@store = store
|
13
|
-
@mutex =
|
14
|
-
@path_obj = path_obj.map! { |f| File.exist?(f) ? File.realpath(f) : f }
|
13
|
+
@mutex = Mutex.new
|
14
|
+
@path_obj = path_obj.map! { |f| PathScanner.os_path(File.exist?(f) ? File.realpath(f) : f.dup) }
|
15
15
|
@has_relative_paths = nil
|
16
16
|
reinitialize
|
17
17
|
end
|
@@ -44,14 +44,20 @@ module Bootsnap
|
|
44
44
|
|
45
45
|
# Try to resolve this feature to an absolute path without traversing the
|
46
46
|
# loadpath.
|
47
|
-
def find(feature)
|
47
|
+
def find(feature, try_extensions: true)
|
48
48
|
reinitialize if (@has_relative_paths && dir_changed?) || stale?
|
49
|
-
feature = feature.to_s
|
50
|
-
|
51
|
-
return
|
49
|
+
feature = feature.to_s.freeze
|
50
|
+
|
51
|
+
return feature if Bootsnap.absolute_path?(feature)
|
52
|
+
|
53
|
+
if feature.start_with?('./', '../')
|
54
|
+
return try_extensions ? expand_path(feature) : File.expand_path(feature).freeze
|
55
|
+
end
|
56
|
+
|
52
57
|
@mutex.synchronize do
|
53
|
-
x = search_index(feature)
|
58
|
+
x = search_index(feature, try_extensions: try_extensions)
|
54
59
|
return x if x
|
60
|
+
return unless try_extensions
|
55
61
|
|
56
62
|
# Ruby has some built-in features that require lies about.
|
57
63
|
# For example, 'enumerator' is built in. If you require it, ruby
|
@@ -67,7 +73,7 @@ module Bootsnap
|
|
67
73
|
# native dynamic extension, e.g. .bundle or .so), we know it was a
|
68
74
|
# failure and there's nothing more we can do to find the file.
|
69
75
|
# no extension, .rb, (.bundle or .so)
|
70
|
-
when '', *CACHED_EXTENSIONS
|
76
|
+
when '', *CACHED_EXTENSIONS
|
71
77
|
nil
|
72
78
|
# Ruby allows specifying native extensions as '.so' even when DLEXT
|
73
79
|
# is '.bundle'. This is where we handle that case.
|
@@ -92,16 +98,6 @@ module Bootsnap
|
|
92
98
|
raise(LoadPathCache::FallbackScan, '', []) if @development_mode
|
93
99
|
end
|
94
100
|
|
95
|
-
if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
|
96
|
-
def absolute_path?(path)
|
97
|
-
path[1] == ':'
|
98
|
-
end
|
99
|
-
else
|
100
|
-
def absolute_path?(path)
|
101
|
-
path.start_with?(SLASH)
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
101
|
def unshift_paths(sender, *paths)
|
106
102
|
return unless sender == @path_obj
|
107
103
|
@mutex.synchronize { unshift_paths_locked(*paths) }
|
@@ -144,7 +140,7 @@ module Bootsnap
|
|
144
140
|
expanded_path = p.expanded_path
|
145
141
|
entries, dirs = p.entries_and_dirs(@store)
|
146
142
|
# push -> low precedence -> set only if unset
|
147
|
-
dirs.each { |dir| @dirs[dir]
|
143
|
+
dirs.each { |dir| @dirs[dir] ||= path }
|
148
144
|
entries.each { |rel| @index[rel] ||= expanded_path }
|
149
145
|
end
|
150
146
|
end
|
@@ -177,16 +173,24 @@ module Bootsnap
|
|
177
173
|
end
|
178
174
|
|
179
175
|
if DLEXT2
|
180
|
-
def search_index(f)
|
181
|
-
|
176
|
+
def search_index(f, try_extensions: true)
|
177
|
+
if try_extensions
|
178
|
+
try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f + DLEXT2) || try_index(f)
|
179
|
+
else
|
180
|
+
try_index(f)
|
181
|
+
end
|
182
182
|
end
|
183
183
|
|
184
184
|
def maybe_append_extension(f)
|
185
185
|
try_ext(f + DOT_RB) || try_ext(f + DLEXT) || try_ext(f + DLEXT2) || f
|
186
186
|
end
|
187
187
|
else
|
188
|
-
def search_index(f)
|
189
|
-
|
188
|
+
def search_index(f, try_extensions: true)
|
189
|
+
if try_extensions
|
190
|
+
try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f)
|
191
|
+
else
|
192
|
+
try_index(f)
|
193
|
+
end
|
190
194
|
end
|
191
195
|
|
192
196
|
def maybe_append_extension(f)
|
@@ -194,9 +198,26 @@ module Bootsnap
|
|
194
198
|
end
|
195
199
|
end
|
196
200
|
|
197
|
-
|
198
|
-
|
199
|
-
|
201
|
+
s = rand.to_s.force_encoding(Encoding::US_ASCII).freeze
|
202
|
+
if s.respond_to?(:-@)
|
203
|
+
if (-s).equal?(s) && (-s.dup).equal?(s) || RUBY_VERSION >= '2.7'
|
204
|
+
def try_index(f)
|
205
|
+
if (p = @index[f])
|
206
|
+
-(File.join(p, f).freeze)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
else
|
210
|
+
def try_index(f)
|
211
|
+
if (p = @index[f])
|
212
|
+
-File.join(p, f).untaint
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
else
|
217
|
+
def try_index(f)
|
218
|
+
if (p = @index[f])
|
219
|
+
File.join(p, f)
|
220
|
+
end
|
200
221
|
end
|
201
222
|
end
|
202
223
|
|
@@ -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
|
@@ -1,8 +1,9 @@
|
|
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}")
|
6
7
|
err.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
|
7
8
|
err.define_singleton_method(:path) { path }
|
8
9
|
err
|
@@ -37,36 +38,29 @@ module Kernel
|
|
37
38
|
rescue Bootsnap::LoadPathCache::ReturnFalse
|
38
39
|
false
|
39
40
|
rescue Bootsnap::LoadPathCache::FallbackScan
|
40
|
-
|
41
|
+
fallback = true
|
42
|
+
ensure
|
43
|
+
if fallback
|
44
|
+
require_with_bootsnap_lfi(path)
|
45
|
+
end
|
41
46
|
end
|
42
47
|
|
43
48
|
alias_method(:require_relative_without_bootsnap, :require_relative)
|
44
49
|
def require_relative(path)
|
50
|
+
location = caller_locations(1..1).first
|
45
51
|
realpath = Bootsnap::LoadPathCache.realpath_cache.call(
|
46
|
-
|
52
|
+
location.absolute_path || location.path, path
|
47
53
|
)
|
48
54
|
require(realpath)
|
49
55
|
end
|
50
56
|
|
51
57
|
alias_method(:load_without_bootsnap, :load)
|
52
58
|
def load(path, wrap = false)
|
53
|
-
if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path))
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
# load also allows relative paths from pwd even when not in $:
|
58
|
-
if File.exist?(relative = File.expand_path(path))
|
59
|
-
return load_without_bootsnap(relative, wrap)
|
59
|
+
if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path, try_extensions: false))
|
60
|
+
load_without_bootsnap(resolved, wrap)
|
61
|
+
else
|
62
|
+
load_without_bootsnap(path, wrap)
|
60
63
|
end
|
61
|
-
|
62
|
-
raise(Bootsnap::LoadPathCache::CoreExt.make_load_error(path))
|
63
|
-
rescue LoadError => e
|
64
|
-
e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
|
65
|
-
raise(e)
|
66
|
-
rescue Bootsnap::LoadPathCache::ReturnFalse
|
67
|
-
false
|
68
|
-
rescue Bootsnap::LoadPathCache::FallbackScan
|
69
|
-
load_without_bootsnap(path, wrap)
|
70
64
|
end
|
71
65
|
end
|
72
66
|
|
@@ -87,6 +81,10 @@ class Module
|
|
87
81
|
rescue Bootsnap::LoadPathCache::ReturnFalse
|
88
82
|
false
|
89
83
|
rescue Bootsnap::LoadPathCache::FallbackScan
|
90
|
-
|
84
|
+
fallback = true
|
85
|
+
ensure
|
86
|
+
if fallback
|
87
|
+
autoload_without_bootsnap(const, path)
|
88
|
+
end
|
91
89
|
end
|
92
90
|
end
|
@@ -26,7 +26,7 @@ module Bootsnap
|
|
26
26
|
class LoadedFeaturesIndex
|
27
27
|
def initialize
|
28
28
|
@lfi = {}
|
29
|
-
@mutex =
|
29
|
+
@mutex = Mutex.new
|
30
30
|
|
31
31
|
# In theory the user could mutate $LOADED_FEATURES and invalidate our
|
32
32
|
# cache. If this ever comes up in practice — or if you, the
|
@@ -40,7 +40,7 @@ module Bootsnap
|
|
40
40
|
# /a/b/lib/my/foo.rb
|
41
41
|
# ^^^^^^^^^
|
42
42
|
short = feat[(lpe.length + 1)..-1]
|
43
|
-
stripped =
|
43
|
+
stripped = strip_extension_if_elidable(short)
|
44
44
|
@lfi[short] = hash
|
45
45
|
@lfi[stripped] = hash
|
46
46
|
end
|
@@ -58,9 +58,9 @@ module Bootsnap
|
|
58
58
|
end
|
59
59
|
|
60
60
|
def purge_multi(features)
|
61
|
-
rejected_hashes = features.
|
61
|
+
rejected_hashes = features.each_with_object({}) { |f, h| h[f.hash] = true }
|
62
62
|
@mutex.synchronize do
|
63
|
-
@lfi.reject! { |_, hash| rejected_hashes.
|
63
|
+
@lfi.reject! { |_, hash| rejected_hashes.key?(hash) }
|
64
64
|
end
|
65
65
|
end
|
66
66
|
|
@@ -83,24 +83,38 @@ module Bootsnap
|
|
83
83
|
# 2. Inspect $LOADED_FEATURES upon return from yield to find the matching
|
84
84
|
# entry.
|
85
85
|
def register(short, long = nil)
|
86
|
+
# Absolute paths are not a concern.
|
87
|
+
if Bootsnap.absolute_path?(short.to_s)
|
88
|
+
return yield
|
89
|
+
end
|
90
|
+
|
86
91
|
if long.nil?
|
87
|
-
pat = %r{/#{Regexp.escape(short)}(\.[^/]+)?$}
|
88
92
|
len = $LOADED_FEATURES.size
|
89
93
|
ret = yield
|
90
|
-
long = $LOADED_FEATURES[len..-1].detect
|
94
|
+
long = $LOADED_FEATURES[len..-1].detect do |feat|
|
95
|
+
offset = 0
|
96
|
+
while offset = feat.index(short, offset)
|
97
|
+
if feat.index(".", offset + 1) && !feat.index("/", offset + 2)
|
98
|
+
break true
|
99
|
+
else
|
100
|
+
offset += 1
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
91
104
|
else
|
92
105
|
ret = yield
|
93
106
|
end
|
94
107
|
|
95
108
|
hash = long.hash
|
96
109
|
|
97
|
-
#
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
#
|
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.
|
104
118
|
short + ext
|
105
119
|
end
|
106
120
|
|
@@ -117,8 +131,30 @@ module Bootsnap
|
|
117
131
|
STRIP_EXTENSION = /\.[^.]*?$/
|
118
132
|
private_constant(:STRIP_EXTENSION)
|
119
133
|
|
120
|
-
|
121
|
-
|
134
|
+
# Might Ruby automatically search for this extension if
|
135
|
+
# someone tries to 'require' the file without it? E.g. Ruby
|
136
|
+
# will implicitly try 'x.rb' if you ask for 'x'.
|
137
|
+
#
|
138
|
+
# This is complex and platform-dependent, and the Ruby docs are a little
|
139
|
+
# handwavy about what will be tried when and in what order.
|
140
|
+
# So optimistically pretend that all known elidable extensions
|
141
|
+
# will be tried on all platforms, and that people are unlikely
|
142
|
+
# to name files in a way that assumes otherwise.
|
143
|
+
# (E.g. It's unlikely that someone will know that their code
|
144
|
+
# will _never_ run on MacOS, and therefore think they can get away
|
145
|
+
# with calling a Ruby file 'x.dylib.rb' and then requiring it as 'x.dylib'.)
|
146
|
+
#
|
147
|
+
# See <https://ruby-doc.org/core-2.6.4/Kernel.html#method-i-require>.
|
148
|
+
def extension_elidable?(f)
|
149
|
+
f.to_s.end_with?('.rb', '.so', '.o', '.dll', '.dylib')
|
150
|
+
end
|
151
|
+
|
152
|
+
def strip_extension_if_elidable(f)
|
153
|
+
if extension_elidable?(f)
|
154
|
+
f.sub(STRIP_EXTENSION, '')
|
155
|
+
else
|
156
|
+
f
|
157
|
+
end
|
122
158
|
end
|
123
159
|
end
|
124
160
|
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
|