bootsnap 1.4.1 → 1.10.3
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 +189 -0
- data/LICENSE.txt +1 -1
- data/README.md +67 -18
- data/exe/bootsnap +5 -0
- data/ext/bootsnap/bootsnap.c +319 -119
- data/ext/bootsnap/extconf.rb +22 -14
- data/lib/bootsnap/bundler.rb +2 -0
- data/lib/bootsnap/cli/worker_pool.rb +136 -0
- data/lib/bootsnap/cli.rb +281 -0
- data/lib/bootsnap/compile_cache/iseq.rb +65 -18
- data/lib/bootsnap/compile_cache/json.rb +88 -0
- data/lib/bootsnap/compile_cache/yaml.rb +332 -39
- data/lib/bootsnap/compile_cache.rb +35 -7
- data/lib/bootsnap/explicit_require.rb +5 -3
- data/lib/bootsnap/load_path_cache/cache.rb +83 -32
- data/lib/bootsnap/load_path_cache/change_observer.rb +6 -1
- data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +39 -47
- data/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb +12 -0
- data/lib/bootsnap/load_path_cache/loaded_features_index.rb +69 -26
- data/lib/bootsnap/load_path_cache/path.rb +8 -5
- data/lib/bootsnap/load_path_cache/path_scanner.rb +56 -29
- data/lib/bootsnap/load_path_cache/realpath_cache.rb +6 -5
- data/lib/bootsnap/load_path_cache/store.rb +49 -18
- data/lib/bootsnap/load_path_cache.rb +20 -32
- data/lib/bootsnap/setup.rb +3 -33
- data/lib/bootsnap/version.rb +3 -1
- data/lib/bootsnap.rb +126 -36
- metadata +15 -97
- 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 -0
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Bootsnap
|
2
4
|
module LoadPathCache
|
3
5
|
module ChangeObserver
|
@@ -14,18 +16,20 @@ module Bootsnap
|
|
14
16
|
@lpc_observer.push_paths(self, *entries.map(&:to_s))
|
15
17
|
super
|
16
18
|
end
|
19
|
+
alias_method :append, :push
|
17
20
|
|
18
21
|
def unshift(*entries)
|
19
22
|
@lpc_observer.unshift_paths(self, *entries.map(&:to_s))
|
20
23
|
super
|
21
24
|
end
|
25
|
+
alias_method :prepend, :unshift
|
22
26
|
|
23
27
|
def concat(entries)
|
24
28
|
@lpc_observer.push_paths(self, *entries.map(&:to_s))
|
25
29
|
super
|
26
30
|
end
|
27
31
|
|
28
|
-
# uniq! keeps the first
|
32
|
+
# uniq! keeps the first occurrence of each path, otherwise preserving
|
29
33
|
# order, preserving the effective load path
|
30
34
|
def uniq!(*args)
|
31
35
|
ret = super
|
@@ -54,6 +58,7 @@ module Bootsnap
|
|
54
58
|
|
55
59
|
def self.register(observer, arr)
|
56
60
|
return if arr.frozen? # can't register observer, but no need to.
|
61
|
+
|
57
62
|
arr.instance_variable_set(:@lpc_observer, observer)
|
58
63
|
arr.extend(ArrayMixin)
|
59
64
|
end
|
@@ -1,65 +1,54 @@
|
|
1
|
-
|
2
|
-
module LoadPathCache
|
3
|
-
module CoreExt
|
4
|
-
def self.make_load_error(path)
|
5
|
-
err = LoadError.new("cannot load such file -- #{path}")
|
6
|
-
err.define_singleton_method(:path) { path }
|
7
|
-
err
|
8
|
-
end
|
9
|
-
end
|
10
|
-
end
|
11
|
-
end
|
1
|
+
# frozen_string_literal: true
|
12
2
|
|
13
3
|
module Kernel
|
14
|
-
module_function
|
4
|
+
module_function
|
15
5
|
|
16
6
|
alias_method(:require_without_bootsnap, :require)
|
17
7
|
|
18
|
-
# Note that require registers to $LOADED_FEATURES while load does not.
|
19
|
-
def require_with_bootsnap_lfi(path, resolved = nil)
|
20
|
-
Bootsnap::LoadPathCache.loaded_features_index.register(path, resolved) do
|
21
|
-
require_without_bootsnap(resolved || path)
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
8
|
def require(path)
|
26
|
-
|
9
|
+
string_path = path.to_s
|
10
|
+
return false if Bootsnap::LoadPathCache.loaded_features_index.key?(string_path)
|
27
11
|
|
28
|
-
|
29
|
-
|
12
|
+
resolved = Bootsnap::LoadPathCache.load_path_cache.find(string_path)
|
13
|
+
if Bootsnap::LoadPathCache::FALLBACK_SCAN.equal?(resolved)
|
14
|
+
if (cursor = Bootsnap::LoadPathCache.loaded_features_index.cursor(string_path))
|
15
|
+
ret = require_without_bootsnap(path)
|
16
|
+
resolved = Bootsnap::LoadPathCache.loaded_features_index.identify(string_path, cursor)
|
17
|
+
Bootsnap::LoadPathCache.loaded_features_index.register(string_path, resolved)
|
18
|
+
return ret
|
19
|
+
else
|
20
|
+
return require_without_bootsnap(path)
|
21
|
+
end
|
22
|
+
elsif false == resolved
|
23
|
+
return false
|
24
|
+
elsif resolved.nil?
|
25
|
+
error = LoadError.new(+"cannot load such file -- #{path}")
|
26
|
+
error.instance_variable_set(:@path, path)
|
27
|
+
raise error
|
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
|
30
33
|
end
|
31
|
-
|
32
|
-
raise(Bootsnap::LoadPathCache::CoreExt.make_load_error(path))
|
33
|
-
rescue Bootsnap::LoadPathCache::ReturnFalse
|
34
|
-
false
|
35
|
-
rescue Bootsnap::LoadPathCache::FallbackScan
|
36
|
-
require_with_bootsnap_lfi(path)
|
37
34
|
end
|
38
35
|
|
39
36
|
alias_method(:require_relative_without_bootsnap, :require_relative)
|
40
37
|
def require_relative(path)
|
38
|
+
location = caller_locations(1..1).first
|
41
39
|
realpath = Bootsnap::LoadPathCache.realpath_cache.call(
|
42
|
-
|
40
|
+
location.absolute_path || location.path, path
|
43
41
|
)
|
44
42
|
require(realpath)
|
45
43
|
end
|
46
44
|
|
47
45
|
alias_method(:load_without_bootsnap, :load)
|
48
46
|
def load(path, wrap = false)
|
49
|
-
if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path))
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
# load also allows relative paths from pwd even when not in $:
|
54
|
-
if File.exist?(relative = File.expand_path(path))
|
55
|
-
return load_without_bootsnap(relative, wrap)
|
47
|
+
if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path, try_extensions: false))
|
48
|
+
load_without_bootsnap(resolved, wrap)
|
49
|
+
else
|
50
|
+
load_without_bootsnap(path, wrap)
|
56
51
|
end
|
57
|
-
|
58
|
-
raise(Bootsnap::LoadPathCache::CoreExt.make_load_error(path))
|
59
|
-
rescue Bootsnap::LoadPathCache::ReturnFalse
|
60
|
-
false
|
61
|
-
rescue Bootsnap::LoadPathCache::FallbackScan
|
62
|
-
load_without_bootsnap(path, wrap)
|
63
52
|
end
|
64
53
|
end
|
65
54
|
|
@@ -73,10 +62,13 @@ class Module
|
|
73
62
|
# The challenge is that we don't control the point at which the entry gets
|
74
63
|
# added to $LOADED_FEATURES and won't be able to hook that modification
|
75
64
|
# since it's done in C-land.
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
65
|
+
resolved = Bootsnap::LoadPathCache.load_path_cache.find(path)
|
66
|
+
if Bootsnap::LoadPathCache::FALLBACK_SCAN.equal?(resolved)
|
67
|
+
autoload_without_bootsnap(const, path)
|
68
|
+
elsif resolved == false
|
69
|
+
return false
|
70
|
+
else
|
71
|
+
autoload_without_bootsnap(const, resolved || path)
|
72
|
+
end
|
81
73
|
end
|
82
74
|
end
|
@@ -1,7 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class << $LOADED_FEATURES
|
2
4
|
alias_method(:delete_without_bootsnap, :delete)
|
3
5
|
def delete(key)
|
4
6
|
Bootsnap::LoadPathCache.loaded_features_index.purge(key)
|
5
7
|
delete_without_bootsnap(key)
|
6
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
|
7
19
|
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,21 +26,22 @@ 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
|
-
# cache. If this ever comes up in practice
|
31
|
-
# enterprising reader, feels inclined to solve this problem
|
32
|
+
# cache. If this ever comes up in practice - or if you, the
|
33
|
+
# enterprising reader, feels inclined to solve this problem - we could
|
32
34
|
# parallel the work done with ChangeObserver on $LOAD_PATH to mirror
|
33
35
|
# updates to our @lfi.
|
34
36
|
$LOADED_FEATURES.each do |feat|
|
35
37
|
hash = feat.hash
|
36
38
|
$LOAD_PATH.each do |lpe|
|
37
39
|
next unless feat.start_with?(lpe)
|
40
|
+
|
38
41
|
# /a/b/lib/my/foo.rb
|
39
42
|
# ^^^^^^^^^
|
40
43
|
short = feat[(lpe.length + 1)..-1]
|
41
|
-
stripped =
|
44
|
+
stripped = strip_extension_if_elidable(short)
|
42
45
|
@lfi[short] = hash
|
43
46
|
@lfi[stripped] = hash
|
44
47
|
end
|
@@ -55,15 +58,41 @@ module Bootsnap
|
|
55
58
|
end
|
56
59
|
end
|
57
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
|
+
|
58
68
|
def key?(feature)
|
59
69
|
@mutex.synchronize { @lfi.key?(feature) }
|
60
70
|
end
|
61
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..-1].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
|
+
|
62
91
|
# There is a relatively uncommon case where we could miss adding an
|
63
92
|
# entry:
|
64
93
|
#
|
65
94
|
# If the user asked for e.g. `require 'bundler'`, and we went through the
|
66
|
-
# `
|
95
|
+
# `FALLBACK_SCAN` pathway in `kernel_require.rb` and therefore did not
|
67
96
|
# pass `long` (the full expanded absolute path), then we did are not able
|
68
97
|
# to confidently add the `bundler.rb` form to @lfi.
|
69
98
|
#
|
@@ -73,25 +102,19 @@ module Bootsnap
|
|
73
102
|
# not quite right; or
|
74
103
|
# 2. Inspect $LOADED_FEATURES upon return from yield to find the matching
|
75
104
|
# entry.
|
76
|
-
def register(short, long
|
77
|
-
if
|
78
|
-
pat = %r{/#{Regexp.escape(short)}(\.[^/]+)?$}
|
79
|
-
len = $LOADED_FEATURES.size
|
80
|
-
ret = yield
|
81
|
-
long = $LOADED_FEATURES[len..-1].detect { |feat| feat =~ pat }
|
82
|
-
else
|
83
|
-
ret = yield
|
84
|
-
end
|
105
|
+
def register(short, long)
|
106
|
+
return if Bootsnap.absolute_path?(short)
|
85
107
|
|
86
108
|
hash = long.hash
|
87
109
|
|
88
|
-
#
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
#
|
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.
|
95
118
|
short + ext
|
96
119
|
end
|
97
120
|
|
@@ -99,17 +122,37 @@ module Bootsnap
|
|
99
122
|
@lfi[short] = hash
|
100
123
|
(@lfi[altname] = hash) if altname
|
101
124
|
end
|
102
|
-
|
103
|
-
ret
|
104
125
|
end
|
105
126
|
|
106
127
|
private
|
107
128
|
|
108
|
-
STRIP_EXTENSION = /\.[^.]
|
129
|
+
STRIP_EXTENSION = /\.[^.]*?$/.freeze
|
109
130
|
private_constant(:STRIP_EXTENSION)
|
110
131
|
|
111
|
-
|
112
|
-
|
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://ruby-doc.org/core-2.6.4/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
|
113
156
|
end
|
114
157
|
end
|
115
158
|
end
|
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative("path_scanner")
|
2
4
|
|
3
5
|
module Bootsnap
|
4
6
|
module LoadPathCache
|
@@ -20,7 +22,7 @@ module Bootsnap
|
|
20
22
|
attr_reader(:path)
|
21
23
|
|
22
24
|
def initialize(path)
|
23
|
-
@path = path.to_s
|
25
|
+
@path = path.to_s.freeze
|
24
26
|
end
|
25
27
|
|
26
28
|
# True if the path exists, but represents a non-directory object
|
@@ -42,6 +44,7 @@ module Bootsnap
|
|
42
44
|
# set to zero anyway, just in case we change the stability heuristics.
|
43
45
|
_, entries, dirs = store.get(expanded_path)
|
44
46
|
return [entries, dirs] if entries # cache hit
|
47
|
+
|
45
48
|
entries, dirs = scan!
|
46
49
|
store.set(expanded_path, [0, entries, dirs])
|
47
50
|
return [entries, dirs]
|
@@ -59,7 +62,7 @@ module Bootsnap
|
|
59
62
|
end
|
60
63
|
|
61
64
|
def expanded_path
|
62
|
-
File.expand_path(path)
|
65
|
+
File.expand_path(path).freeze
|
63
66
|
end
|
64
67
|
|
65
68
|
private
|
@@ -92,8 +95,8 @@ module Bootsnap
|
|
92
95
|
|
93
96
|
# Built-in ruby lib stuff doesn't change, but things can occasionally be
|
94
97
|
# installed into sitedir, which generally lives under libdir.
|
95
|
-
RUBY_LIBDIR = RbConfig::CONFIG[
|
96
|
-
RUBY_SITEDIR = RbConfig::CONFIG[
|
98
|
+
RUBY_LIBDIR = RbConfig::CONFIG["libdir"]
|
99
|
+
RUBY_SITEDIR = RbConfig::CONFIG["sitedir"]
|
97
100
|
|
98
101
|
def stability
|
99
102
|
@stability ||= begin
|
@@ -1,47 +1,74 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative("../explicit_require")
|
2
4
|
|
3
5
|
module Bootsnap
|
4
6
|
module LoadPathCache
|
5
7
|
module PathScanner
|
6
|
-
ALL_FILES = "/{,**/*/**/}*"
|
7
8
|
REQUIRABLE_EXTENSIONS = [DOT_RB] + DL_EXTENSIONS
|
8
9
|
NORMALIZE_NATIVE_EXTENSIONS = !DL_EXTENSIONS.include?(LoadPathCache::DOT_SO)
|
9
|
-
ALTERNATIVE_NATIVE_EXTENSIONS_PATTERN = /\.(o|bundle|dylib)\z
|
10
|
+
ALTERNATIVE_NATIVE_EXTENSIONS_PATTERN = /\.(o|bundle|dylib)\z/.freeze
|
10
11
|
|
11
12
|
BUNDLE_PATH = if Bootsnap.bundler?
|
12
13
|
(Bundler.bundle_path.cleanpath.to_s << LoadPathCache::SLASH).freeze
|
13
14
|
else
|
14
|
-
|
15
|
+
""
|
15
16
|
end
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
requirables << relative_path
|
18
|
+
class << self
|
19
|
+
def call(path)
|
20
|
+
path = File.expand_path(path.to_s).freeze
|
21
|
+
return [[], []] unless File.directory?(path)
|
22
|
+
|
23
|
+
# If the bundle path is a descendent of this path, we do additional
|
24
|
+
# checks to prevent recursing into the bundle path as we recurse
|
25
|
+
# through this path. We don't want to scan the bundle path because
|
26
|
+
# anything useful in it will be present on other load path items.
|
27
|
+
#
|
28
|
+
# This can happen if, for example, the user adds '.' to the load path,
|
29
|
+
# and the bundle path is '.bundle'.
|
30
|
+
contains_bundle_path = BUNDLE_PATH.start_with?(path)
|
31
|
+
|
32
|
+
dirs = []
|
33
|
+
requirables = []
|
34
|
+
walk(path, nil) do |relative_path, absolute_path, is_directory|
|
35
|
+
if is_directory
|
36
|
+
dirs << os_path(relative_path)
|
37
|
+
!contains_bundle_path || !absolute_path.start_with?(BUNDLE_PATH)
|
38
|
+
elsif relative_path.end_with?(*REQUIRABLE_EXTENSIONS)
|
39
|
+
requirables << os_path(relative_path)
|
40
|
+
end
|
41
41
|
end
|
42
|
+
[requirables, dirs]
|
42
43
|
end
|
43
44
|
|
44
|
-
|
45
|
+
def walk(absolute_dir_path, relative_dir_path, &block)
|
46
|
+
Dir.foreach(absolute_dir_path) do |name|
|
47
|
+
next if name.start_with?(".")
|
48
|
+
|
49
|
+
relative_path = relative_dir_path ? File.join(relative_dir_path, name) : name
|
50
|
+
|
51
|
+
absolute_path = "#{absolute_dir_path}/#{name}"
|
52
|
+
if File.directory?(absolute_path)
|
53
|
+
if yield relative_path, absolute_path, true
|
54
|
+
walk(absolute_path, relative_path, &block)
|
55
|
+
end
|
56
|
+
else
|
57
|
+
yield relative_path, absolute_path, false
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
if RUBY_VERSION >= "3.1"
|
63
|
+
def os_path(path)
|
64
|
+
path.freeze
|
65
|
+
end
|
66
|
+
else
|
67
|
+
def os_path(path)
|
68
|
+
path.force_encoding(Encoding::US_ASCII) if path.ascii_only?
|
69
|
+
path.freeze
|
70
|
+
end
|
71
|
+
end
|
45
72
|
end
|
46
73
|
end
|
47
74
|
end
|
@@ -15,15 +15,16 @@ module Bootsnap
|
|
15
15
|
|
16
16
|
def realpath(caller_location, path)
|
17
17
|
base = File.dirname(caller_location)
|
18
|
-
|
19
|
-
|
20
|
-
File.join(dir, File.basename(file))
|
18
|
+
abspath = File.expand_path(path, base).freeze
|
19
|
+
find_file(abspath)
|
21
20
|
end
|
22
21
|
|
23
22
|
def find_file(name)
|
24
|
-
|
23
|
+
return File.realpath(name).freeze if File.exist?(name)
|
24
|
+
|
25
|
+
CACHED_EXTENSIONS.each do |ext|
|
25
26
|
filename = "#{name}#{ext}"
|
26
|
-
return File.realpath(filename) if File.exist?(filename)
|
27
|
+
return File.realpath(filename).freeze if File.exist?(filename)
|
27
28
|
end
|
28
29
|
name
|
29
30
|
end
|
@@ -1,17 +1,21 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
require_relative("../explicit_require")
|
4
|
+
|
5
|
+
Bootsnap::ExplicitRequire.with_gems("msgpack") { require("msgpack") }
|
5
6
|
|
6
7
|
module Bootsnap
|
7
8
|
module LoadPathCache
|
8
9
|
class Store
|
10
|
+
VERSION_KEY = "__bootsnap_ruby_version__"
|
11
|
+
CURRENT_VERSION = "#{RUBY_REVISION}-#{RUBY_PLATFORM}".freeze # rubocop:disable Style/RedundantFreeze
|
12
|
+
|
9
13
|
NestedTransactionError = Class.new(StandardError)
|
10
14
|
SetOutsideTransactionNotAllowed = Class.new(StandardError)
|
11
15
|
|
12
16
|
def initialize(store_path)
|
13
17
|
@store_path = store_path
|
14
|
-
@
|
18
|
+
@txn_mutex = Mutex.new
|
15
19
|
@dirty = false
|
16
20
|
load_data
|
17
21
|
end
|
@@ -21,7 +25,8 @@ module Bootsnap
|
|
21
25
|
end
|
22
26
|
|
23
27
|
def fetch(key)
|
24
|
-
raise(SetOutsideTransactionNotAllowed) unless @
|
28
|
+
raise(SetOutsideTransactionNotAllowed) unless @txn_mutex.owned?
|
29
|
+
|
25
30
|
v = get(key)
|
26
31
|
unless v
|
27
32
|
@dirty = true
|
@@ -32,7 +37,8 @@ module Bootsnap
|
|
32
37
|
end
|
33
38
|
|
34
39
|
def set(key, value)
|
35
|
-
raise(SetOutsideTransactionNotAllowed) unless @
|
40
|
+
raise(SetOutsideTransactionNotAllowed) unless @txn_mutex.owned?
|
41
|
+
|
36
42
|
if value != @data[key]
|
37
43
|
@dirty = true
|
38
44
|
@data[key] = value
|
@@ -40,12 +46,15 @@ module Bootsnap
|
|
40
46
|
end
|
41
47
|
|
42
48
|
def transaction
|
43
|
-
raise(NestedTransactionError) if @
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
+
raise(NestedTransactionError) if @txn_mutex.owned?
|
50
|
+
|
51
|
+
@txn_mutex.synchronize do
|
52
|
+
begin
|
53
|
+
yield
|
54
|
+
ensure
|
55
|
+
commit_transaction
|
56
|
+
end
|
57
|
+
end
|
49
58
|
end
|
50
59
|
|
51
60
|
private
|
@@ -59,25 +68,47 @@ module Bootsnap
|
|
59
68
|
|
60
69
|
def load_data
|
61
70
|
@data = begin
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
71
|
+
data = File.open(@store_path, encoding: Encoding::BINARY) do |io|
|
72
|
+
MessagePack.load(io)
|
73
|
+
end
|
74
|
+
if data.is_a?(Hash) && data[VERSION_KEY] == CURRENT_VERSION
|
75
|
+
data
|
76
|
+
else
|
77
|
+
default_data
|
78
|
+
end
|
79
|
+
# handle malformed data due to upgrade incompatibility
|
80
|
+
rescue Errno::ENOENT, MessagePack::MalformedFormatError, MessagePack::UnknownExtTypeError, EOFError
|
81
|
+
default_data
|
82
|
+
rescue ArgumentError => error
|
83
|
+
if error.message =~ /negative array size/
|
84
|
+
default_data
|
85
|
+
else
|
86
|
+
raise
|
87
|
+
end
|
66
88
|
end
|
67
89
|
end
|
68
90
|
|
69
91
|
def dump_data
|
92
|
+
require "fileutils" unless defined? FileUtils
|
93
|
+
|
70
94
|
# Change contents atomically so other processes can't get invalid
|
71
95
|
# caches if they read at an inopportune time.
|
72
|
-
tmp = "#{@store_path}.#{Process.pid}.#{(rand *
|
96
|
+
tmp = "#{@store_path}.#{Process.pid}.#{(rand * 100_000).to_i}.tmp"
|
73
97
|
FileUtils.mkpath(File.dirname(tmp))
|
74
98
|
exclusive_write = File::Constants::CREAT | File::Constants::EXCL | File::Constants::WRONLY
|
75
99
|
# `encoding:` looks redundant wrt `binwrite`, but necessary on windows
|
76
100
|
# because binary is part of mode.
|
77
|
-
File.
|
101
|
+
File.open(tmp, mode: exclusive_write, encoding: Encoding::BINARY) do |io|
|
102
|
+
MessagePack.dump(@data, io, freeze: true)
|
103
|
+
end
|
78
104
|
FileUtils.mv(tmp, @store_path)
|
79
105
|
rescue Errno::EEXIST
|
80
106
|
retry
|
107
|
+
rescue SystemCallError
|
108
|
+
end
|
109
|
+
|
110
|
+
def default_data
|
111
|
+
{VERSION_KEY => CURRENT_VERSION}
|
81
112
|
end
|
82
113
|
end
|
83
114
|
end
|