bootsnap 1.9.4 → 1.11.0

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.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative('../explicit_require')
3
+ require_relative("../explicit_require")
4
4
 
5
5
  module Bootsnap
6
6
  module LoadPathCache
@@ -28,15 +28,16 @@ module Bootsnap
28
28
  BUILTIN_FEATURES = $LOADED_FEATURES.each_with_object({}) do |feat, features|
29
29
  # Builtin features are of the form 'enumerator.so'.
30
30
  # All others include paths.
31
- next unless feat.size < 20 && !feat.include?('/')
31
+ next unless feat.size < 20 && !feat.include?("/")
32
32
 
33
- base = File.basename(feat, '.*') # enumerator.so -> enumerator
33
+ base = File.basename(feat, ".*") # enumerator.so -> enumerator
34
34
  ext = File.extname(feat) # .so
35
35
 
36
36
  features[feat] = nil # enumerator.so
37
37
  features[base] = nil # enumerator
38
38
 
39
39
  next unless [DOT_SO, *DL_EXTENSIONS].include?(ext)
40
+
40
41
  DL_EXTENSIONS.each do |dl_ext|
41
42
  features["#{base}#{dl_ext}"] = nil # enumerator.bundle
42
43
  end
@@ -50,7 +51,7 @@ module Bootsnap
50
51
 
51
52
  return feature if Bootsnap.absolute_path?(feature)
52
53
 
53
- if feature.start_with?('./', '../')
54
+ if feature.start_with?("./", "../")
54
55
  return try_extensions ? expand_path(feature) : File.expand_path(feature).freeze
55
56
  end
56
57
 
@@ -64,7 +65,7 @@ module Bootsnap
64
65
  # returns false as if it were already loaded; however, there is no
65
66
  # file to find on disk. We've pre-built a list of these, and we
66
67
  # return false if any of them is loaded.
67
- raise(LoadPathCache::ReturnFalse, '', []) if BUILTIN_FEATURES.key?(feature)
68
+ return false if BUILTIN_FEATURES.key?(feature)
68
69
 
69
70
  # The feature wasn't found on our preliminary search through the index.
70
71
  # We resolve this differently depending on what the extension was.
@@ -73,13 +74,14 @@ module Bootsnap
73
74
  # native dynamic extension, e.g. .bundle or .so), we know it was a
74
75
  # failure and there's nothing more we can do to find the file.
75
76
  # no extension, .rb, (.bundle or .so)
76
- when '', *CACHED_EXTENSIONS
77
+ when "", *CACHED_EXTENSIONS
77
78
  nil
78
79
  # Ruby allows specifying native extensions as '.so' even when DLEXT
79
80
  # is '.bundle'. This is where we handle that case.
80
81
  when DOT_SO
81
82
  x = search_index(feature[0..-4] + DLEXT)
82
83
  return x if x
84
+
83
85
  if DLEXT2
84
86
  x = search_index(feature[0..-4] + DLEXT2)
85
87
  return x if x
@@ -87,7 +89,7 @@ module Bootsnap
87
89
  else
88
90
  # other, unknown extension. For example, `.rake`. Since we haven't
89
91
  # cached these, we legitimately need to run the load path search.
90
- raise(LoadPathCache::FallbackScan, '', [])
92
+ return FALLBACK_SCAN
91
93
  end
92
94
  end
93
95
 
@@ -95,16 +97,18 @@ module Bootsnap
95
97
  # cases where the file doesn't appear to be on the load path. We should
96
98
  # be able to detect newly-created files without rebooting the
97
99
  # application.
98
- raise(LoadPathCache::FallbackScan, '', []) if @development_mode
100
+ return FALLBACK_SCAN if @development_mode
99
101
  end
100
102
 
101
103
  def unshift_paths(sender, *paths)
102
104
  return unless sender == @path_obj
105
+
103
106
  @mutex.synchronize { unshift_paths_locked(*paths) }
104
107
  end
105
108
 
106
109
  def push_paths(sender, *paths)
107
110
  return unless sender == @path_obj
111
+
108
112
  @mutex.synchronize { push_paths_locked(*paths) }
109
113
  end
110
114
 
@@ -137,6 +141,9 @@ module Bootsnap
137
141
  p = Path.new(path)
138
142
  @has_relative_paths = true if p.relative?
139
143
  next if p.non_directory?
144
+
145
+ p = p.to_realpath
146
+
140
147
  expanded_path = p.expanded_path
141
148
  entries, dirs = p.entries_and_dirs(@store)
142
149
  # push -> low precedence -> set only if unset
@@ -151,6 +158,9 @@ module Bootsnap
151
158
  paths.map(&:to_s).reverse_each do |path|
152
159
  p = Path.new(path)
153
160
  next if p.non_directory?
161
+
162
+ p = p.to_realpath
163
+
154
164
  expanded_path = p.expanded_path
155
165
  entries, dirs = p.entries_and_dirs(@store)
156
166
  # unshift -> high precedence -> unconditional set
@@ -173,56 +183,62 @@ module Bootsnap
173
183
  end
174
184
 
175
185
  if DLEXT2
176
- def search_index(f, try_extensions: true)
186
+ def search_index(feature, try_extensions: true)
177
187
  if try_extensions
178
- try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f + DLEXT2) || try_index(f)
188
+ try_index(feature + DOT_RB) ||
189
+ try_index(feature + DLEXT) ||
190
+ try_index(feature + DLEXT2) ||
191
+ try_index(feature)
179
192
  else
180
- try_index(f)
193
+ try_index(feature)
181
194
  end
182
195
  end
183
196
 
184
- def maybe_append_extension(f)
185
- try_ext(f + DOT_RB) || try_ext(f + DLEXT) || try_ext(f + DLEXT2) || f
197
+ def maybe_append_extension(feature)
198
+ try_ext(feature + DOT_RB) ||
199
+ try_ext(feature + DLEXT) ||
200
+ try_ext(feature + DLEXT2) ||
201
+ feature
186
202
  end
187
203
  else
188
- def search_index(f, try_extensions: true)
204
+ def search_index(feature, try_extensions: true)
189
205
  if try_extensions
190
- try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f)
206
+ try_index(feature + DOT_RB) || try_index(feature + DLEXT) || try_index(feature)
191
207
  else
192
- try_index(f)
208
+ try_index(feature)
193
209
  end
194
210
  end
195
211
 
196
- def maybe_append_extension(f)
197
- try_ext(f + DOT_RB) || try_ext(f + DLEXT) || f
212
+ def maybe_append_extension(feature)
213
+ try_ext(feature + DOT_RB) || try_ext(feature + DLEXT) || feature
198
214
  end
199
215
  end
200
216
 
201
217
  s = rand.to_s.force_encoding(Encoding::US_ASCII).freeze
202
218
  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)
219
+ if (-s).equal?(s) && (-s.dup).equal?(s) || RUBY_VERSION >= "2.7"
220
+ def try_index(feature)
221
+ if (path = @index[feature])
222
+ -File.join(path, feature).freeze
207
223
  end
208
224
  end
209
225
  else
210
- def try_index(f)
211
- if (p = @index[f])
212
- -File.join(p, f).untaint
226
+ def try_index(feature)
227
+ if (path = @index[feature])
228
+ -File.join(path, feature).untaint
213
229
  end
214
230
  end
215
231
  end
216
232
  else
217
- def try_index(f)
218
- if (p = @index[f])
219
- File.join(p, f)
233
+ def try_index(feature)
234
+ if (path = @index[feature])
235
+ File.join(path, feature)
220
236
  end
221
237
  end
222
238
  end
223
239
 
224
- def try_ext(f)
225
- f if File.exist?(f)
240
+ def try_ext(feature)
241
+ feature if File.exist?(feature)
226
242
  end
227
243
  end
228
244
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Bootsnap
3
4
  module LoadPathCache
4
5
  module ChangeObserver
@@ -57,6 +58,7 @@ module Bootsnap
57
58
 
58
59
  def self.register(observer, arr)
59
60
  return if arr.frozen? # can't register observer, but no need to.
61
+
60
62
  arr.instance_variable_set(:@lpc_observer, observer)
61
63
  arr.extend(ArrayMixin)
62
64
  end
@@ -1,62 +1,41 @@
1
1
  # frozen_string_literal: true
2
- module Bootsnap
3
- module LoadPathCache
4
- module CoreExt
5
- def self.make_load_error(path)
6
- err = LoadError.new(+"cannot load such file -- #{path}")
7
- err.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
8
- err.define_singleton_method(:path) { path }
9
- err
10
- end
11
- end
12
- end
13
- end
14
2
 
15
3
  module Kernel
16
- module_function # rubocop:disable Style/ModuleFunction
4
+ module_function
17
5
 
18
6
  alias_method(:require_without_bootsnap, :require)
19
7
 
20
- # Note that require registers to $LOADED_FEATURES while load does not.
21
- def require_with_bootsnap_lfi(path, resolved = nil)
22
- Bootsnap::LoadPathCache.loaded_features_index.register(path, resolved) do
23
- require_without_bootsnap(resolved || path)
24
- end
25
- end
26
-
27
8
  def require(path)
28
- return false if Bootsnap::LoadPathCache.loaded_features_index.key?(path)
9
+ string_path = Bootsnap.rb_get_path(path)
10
+ return false if Bootsnap::LoadPathCache.loaded_features_index.key?(string_path)
29
11
 
30
- if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path))
31
- return require_with_bootsnap_lfi(path, resolved)
32
- end
33
-
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)
38
- rescue Bootsnap::LoadPathCache::ReturnFalse
39
- false
40
- rescue Bootsnap::LoadPathCache::FallbackScan
41
- fallback = true
42
- ensure
43
- if fallback
44
- require_with_bootsnap_lfi(path)
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
45
33
  end
46
34
  end
47
35
 
48
- alias_method(:require_relative_without_bootsnap, :require_relative)
49
- def require_relative(path)
50
- location = caller_locations(1..1).first
51
- realpath = Bootsnap::LoadPathCache.realpath_cache.call(
52
- location.absolute_path || location.path, path
53
- )
54
- require(realpath)
55
- end
56
-
57
36
  alias_method(:load_without_bootsnap, :load)
58
37
  def load(path, wrap = false)
59
- if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path, try_extensions: false))
38
+ if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(Bootsnap.rb_get_path(path), try_extensions: false))
60
39
  load_without_bootsnap(resolved, wrap)
61
40
  else
62
41
  load_without_bootsnap(path, wrap)
@@ -74,17 +53,13 @@ class Module
74
53
  # The challenge is that we don't control the point at which the entry gets
75
54
  # added to $LOADED_FEATURES and won't be able to hook that modification
76
55
  # since it's done in C-land.
77
- autoload_without_bootsnap(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
78
- rescue LoadError => e
79
- e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
80
- raise(e)
81
- rescue Bootsnap::LoadPathCache::ReturnFalse
82
- false
83
- rescue Bootsnap::LoadPathCache::FallbackScan
84
- fallback = true
85
- ensure
86
- if fallback
56
+ resolved = Bootsnap::LoadPathCache.load_path_cache.find(Bootsnap.rb_get_path(path))
57
+ if Bootsnap::LoadPathCache::FALLBACK_SCAN.equal?(resolved)
87
58
  autoload_without_bootsnap(const, path)
59
+ elsif resolved == false
60
+ return false
61
+ else
62
+ autoload_without_bootsnap(const, resolved || path)
88
63
  end
89
64
  end
90
65
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  class << $LOADED_FEATURES
3
4
  alias_method(:delete_without_bootsnap, :delete)
4
5
  def delete(key)
@@ -29,14 +29,15 @@ module Bootsnap
29
29
  @mutex = Mutex.new
30
30
 
31
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
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
34
  # parallel the work done with ChangeObserver on $LOAD_PATH to mirror
35
35
  # updates to our @lfi.
36
36
  $LOADED_FEATURES.each do |feat|
37
37
  hash = feat.hash
38
38
  $LOAD_PATH.each do |lpe|
39
39
  next unless feat.start_with?(lpe)
40
+
40
41
  # /a/b/lib/my/foo.rb
41
42
  # ^^^^^^^^^
42
43
  short = feat[(lpe.length + 1)..-1]
@@ -68,11 +69,30 @@ module Bootsnap
68
69
  @mutex.synchronize { @lfi.key?(feature) }
69
70
  end
70
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
+
71
91
  # There is a relatively uncommon case where we could miss adding an
72
92
  # entry:
73
93
  #
74
94
  # If the user asked for e.g. `require 'bundler'`, and we went through the
75
- # `FallbackScan` pathway in `kernel_require.rb` and therefore did not
95
+ # `FALLBACK_SCAN` pathway in `kernel_require.rb` and therefore did not
76
96
  # pass `long` (the full expanded absolute path), then we did are not able
77
97
  # to confidently add the `bundler.rb` form to @lfi.
78
98
  #
@@ -82,28 +102,8 @@ module Bootsnap
82
102
  # not quite right; or
83
103
  # 2. Inspect $LOADED_FEATURES upon return from yield to find the matching
84
104
  # entry.
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
-
91
- if long.nil?
92
- len = $LOADED_FEATURES.size
93
- ret = yield
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
104
- else
105
- ret = yield
106
- end
105
+ def register(short, long)
106
+ return if Bootsnap.absolute_path?(short)
107
107
 
108
108
  hash = long.hash
109
109
 
@@ -122,13 +122,11 @@ module Bootsnap
122
122
  @lfi[short] = hash
123
123
  (@lfi[altname] = hash) if altname
124
124
  end
125
-
126
- ret
127
125
  end
128
126
 
129
127
  private
130
128
 
131
- STRIP_EXTENSION = /\.[^.]*?$/
129
+ STRIP_EXTENSION = /\.[^.]*?$/.freeze
132
130
  private_constant(:STRIP_EXTENSION)
133
131
 
134
132
  # Might Ruby automatically search for this extension if
@@ -145,15 +143,15 @@ module Bootsnap
145
143
  # with calling a Ruby file 'x.dylib.rb' and then requiring it as 'x.dylib'.)
146
144
  #
147
145
  # 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')
146
+ def extension_elidable?(feature)
147
+ feature.to_s.end_with?(".rb", ".so", ".o", ".dll", ".dylib")
150
148
  end
151
149
 
152
- def strip_extension_if_elidable(f)
153
- if extension_elidable?(f)
154
- f.sub(STRIP_EXTENSION, '')
150
+ def strip_extension_if_elidable(feature)
151
+ if extension_elidable?(feature)
152
+ feature.sub(STRIP_EXTENSION, "")
155
153
  else
156
- f
154
+ feature
157
155
  end
158
156
  end
159
157
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
- require_relative('path_scanner')
2
+
3
+ require_relative("path_scanner")
3
4
 
4
5
  module Bootsnap
5
6
  module LoadPathCache
@@ -20,8 +21,26 @@ module Bootsnap
20
21
 
21
22
  attr_reader(:path)
22
23
 
23
- def initialize(path)
24
+ def initialize(path, real: false)
24
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
+ Path.new(realpath, real: true)
40
+ else
41
+ @real = true
42
+ self
43
+ end
25
44
  end
26
45
 
27
46
  # True if the path exists, but represents a non-directory object
@@ -43,6 +62,7 @@ module Bootsnap
43
62
  # set to zero anyway, just in case we change the stability heuristics.
44
63
  _, entries, dirs = store.get(expanded_path)
45
64
  return [entries, dirs] if entries # cache hit
65
+
46
66
  entries, dirs = scan!
47
67
  store.set(expanded_path, [0, entries, dirs])
48
68
  return [entries, dirs]
@@ -60,7 +80,11 @@ module Bootsnap
60
80
  end
61
81
 
62
82
  def expanded_path
63
- File.expand_path(path).freeze
83
+ if @real
84
+ path
85
+ else
86
+ @expanded_path ||= File.expand_path(path).freeze
87
+ end
64
88
  end
65
89
 
66
90
  private
@@ -93,8 +117,8 @@ module Bootsnap
93
117
 
94
118
  # Built-in ruby lib stuff doesn't change, but things can occasionally be
95
119
  # installed into sitedir, which generally lives under libdir.
96
- RUBY_LIBDIR = RbConfig::CONFIG['libdir']
97
- RUBY_SITEDIR = RbConfig::CONFIG['sitedir']
120
+ RUBY_LIBDIR = RbConfig::CONFIG["libdir"]
121
+ RUBY_SITEDIR = RbConfig::CONFIG["sitedir"]
98
122
 
99
123
  def stability
100
124
  @stability ||= begin
@@ -1,18 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative('../explicit_require')
3
+ require_relative("../explicit_require")
4
4
 
5
5
  module Bootsnap
6
6
  module LoadPathCache
7
7
  module PathScanner
8
8
  REQUIRABLE_EXTENSIONS = [DOT_RB] + DL_EXTENSIONS
9
9
  NORMALIZE_NATIVE_EXTENSIONS = !DL_EXTENSIONS.include?(LoadPathCache::DOT_SO)
10
- ALTERNATIVE_NATIVE_EXTENSIONS_PATTERN = /\.(o|bundle|dylib)\z/
10
+ ALTERNATIVE_NATIVE_EXTENSIONS_PATTERN = /\.(o|bundle|dylib)\z/.freeze
11
11
 
12
12
  BUNDLE_PATH = if Bootsnap.bundler?
13
13
  (Bundler.bundle_path.cleanpath.to_s << LoadPathCache::SLASH).freeze
14
14
  else
15
- ''
15
+ ""
16
16
  end
17
17
 
18
18
  class << self
@@ -44,7 +44,8 @@ module Bootsnap
44
44
 
45
45
  def walk(absolute_dir_path, relative_dir_path, &block)
46
46
  Dir.foreach(absolute_dir_path) do |name|
47
- next if name.start_with?('.')
47
+ next if name.start_with?(".")
48
+
48
49
  relative_path = relative_dir_path ? File.join(relative_dir_path, name) : name
49
50
 
50
51
  absolute_path = "#{absolute_dir_path}/#{name}"
@@ -58,7 +59,7 @@ module Bootsnap
58
59
  end
59
60
  end
60
61
 
61
- if RUBY_VERSION >= '3.1'
62
+ if RUBY_VERSION >= "3.1"
62
63
  def os_path(path)
63
64
  path.freeze
64
65
  end
@@ -1,14 +1,14 @@
1
1
  # frozen_string_literal: true
2
- require_relative('../explicit_require')
3
2
 
4
- Bootsnap::ExplicitRequire.with_gems('msgpack') { require('msgpack') }
5
- Bootsnap::ExplicitRequire.from_rubylibdir('fileutils')
3
+ require_relative("../explicit_require")
4
+
5
+ Bootsnap::ExplicitRequire.with_gems("msgpack") { require("msgpack") }
6
6
 
7
7
  module Bootsnap
8
8
  module LoadPathCache
9
9
  class Store
10
- VERSION_KEY = '__bootsnap_ruby_version__'
11
- CURRENT_VERSION = "#{RUBY_REVISION}-#{RUBY_PLATFORM}".freeze
10
+ VERSION_KEY = "__bootsnap_ruby_version__"
11
+ CURRENT_VERSION = "#{RUBY_REVISION}-#{RUBY_PLATFORM}".freeze # rubocop:disable Style/RedundantFreeze
12
12
 
13
13
  NestedTransactionError = Class.new(StandardError)
14
14
  SetOutsideTransactionNotAllowed = Class.new(StandardError)
@@ -26,6 +26,7 @@ module Bootsnap
26
26
 
27
27
  def fetch(key)
28
28
  raise(SetOutsideTransactionNotAllowed) unless @txn_mutex.owned?
29
+
29
30
  v = get(key)
30
31
  unless v
31
32
  @dirty = true
@@ -37,6 +38,7 @@ module Bootsnap
37
38
 
38
39
  def set(key, value)
39
40
  raise(SetOutsideTransactionNotAllowed) unless @txn_mutex.owned?
41
+
40
42
  if value != @data[key]
41
43
  @dirty = true
42
44
  @data[key] = value
@@ -45,6 +47,7 @@ module Bootsnap
45
47
 
46
48
  def transaction
47
49
  raise(NestedTransactionError) if @txn_mutex.owned?
50
+
48
51
  @txn_mutex.synchronize do
49
52
  begin
50
53
  yield
@@ -66,7 +69,7 @@ module Bootsnap
66
69
  def load_data
67
70
  @data = begin
68
71
  data = File.open(@store_path, encoding: Encoding::BINARY) do |io|
69
- MessagePack.load(io)
72
+ MessagePack.load(io, freeze: true)
70
73
  end
71
74
  if data.is_a?(Hash) && data[VERSION_KEY] == CURRENT_VERSION
72
75
  data
@@ -88,22 +91,37 @@ module Bootsnap
88
91
  def dump_data
89
92
  # Change contents atomically so other processes can't get invalid
90
93
  # caches if they read at an inopportune time.
91
- tmp = "#{@store_path}.#{Process.pid}.#{(rand * 100000).to_i}.tmp"
92
- FileUtils.mkpath(File.dirname(tmp))
94
+ tmp = "#{@store_path}.#{Process.pid}.#{(rand * 100_000).to_i}.tmp"
95
+ mkdir_p(File.dirname(tmp))
93
96
  exclusive_write = File::Constants::CREAT | File::Constants::EXCL | File::Constants::WRONLY
94
97
  # `encoding:` looks redundant wrt `binwrite`, but necessary on windows
95
98
  # because binary is part of mode.
96
99
  File.open(tmp, mode: exclusive_write, encoding: Encoding::BINARY) do |io|
97
- MessagePack.dump(@data, io, freeze: true)
100
+ MessagePack.dump(@data, io)
98
101
  end
99
- FileUtils.mv(tmp, @store_path)
102
+ File.rename(tmp, @store_path)
100
103
  rescue Errno::EEXIST
101
104
  retry
102
105
  rescue SystemCallError
103
106
  end
104
107
 
105
108
  def default_data
106
- { VERSION_KEY => CURRENT_VERSION }
109
+ {VERSION_KEY => CURRENT_VERSION}
110
+ end
111
+
112
+ def mkdir_p(path)
113
+ stack = []
114
+ until File.directory?(path)
115
+ stack.push path
116
+ path = File.dirname(path)
117
+ end
118
+ stack.reverse_each do |dir|
119
+ begin
120
+ Dir.mkdir(dir)
121
+ rescue SystemCallError
122
+ raise unless File.directory?(dir)
123
+ end
124
+ end
107
125
  end
108
126
  end
109
127
  end