bootsnap 1.4.8 → 1.16.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,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Bootsnap
3
4
  module LoadPathCache
4
5
  module ChangeObserver
@@ -15,11 +16,13 @@ module Bootsnap
15
16
  @lpc_observer.push_paths(self, *entries.map(&:to_s))
16
17
  super
17
18
  end
19
+ alias_method :append, :push
18
20
 
19
21
  def unshift(*entries)
20
22
  @lpc_observer.unshift_paths(self, *entries.map(&:to_s))
21
23
  super
22
24
  end
25
+ alias_method :prepend, :unshift
23
26
 
24
27
  def concat(entries)
25
28
  @lpc_observer.push_paths(self, *entries.map(&:to_s))
@@ -53,10 +56,22 @@ module Bootsnap
53
56
  end
54
57
  end
55
58
 
56
- def self.register(observer, arr)
59
+ def self.register(arr, observer)
57
60
  return if arr.frozen? # can't register observer, but no need to.
61
+
58
62
  arr.instance_variable_set(:@lpc_observer, observer)
59
- arr.extend(ArrayMixin)
63
+ ArrayMixin.instance_methods.each do |method_name|
64
+ arr.singleton_class.send(:define_method, method_name, ArrayMixin.instance_method(method_name))
65
+ end
66
+ end
67
+
68
+ def self.unregister(arr)
69
+ return unless arr.instance_variable_defined?(:@lpc_observer) && arr.instance_variable_get(:@lpc_observer)
70
+
71
+ ArrayMixin.instance_methods.each do |method_name|
72
+ arr.singleton_class.send(:remove_method, method_name)
73
+ end
74
+ arr.instance_variable_set(:@lpc_observer, nil)
60
75
  end
61
76
  end
62
77
  end
@@ -1,105 +1,37 @@
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)
29
-
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)
45
- end
46
- end
47
-
48
- alias_method(:require_relative_without_bootsnap, :require_relative)
49
- def require_relative(path)
50
- realpath = Bootsnap::LoadPathCache.realpath_cache.call(
51
- caller_locations(1..1).first.absolute_path, path
52
- )
53
- require(realpath)
54
- end
55
-
56
- alias_method(:load_without_bootsnap, :load)
57
- def load(path, wrap = false)
58
- if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path))
59
- return load_without_bootsnap(resolved, wrap)
60
- end
61
-
62
- # load also allows relative paths from pwd even when not in $:
63
- if File.exist?(relative = File.expand_path(path).freeze)
64
- return load_without_bootsnap(relative, wrap)
65
- end
66
-
67
- raise(Bootsnap::LoadPathCache::CoreExt.make_load_error(path))
68
- rescue LoadError => e
69
- e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
70
- raise(e)
71
- rescue Bootsnap::LoadPathCache::ReturnFalse
72
- false
73
- rescue Bootsnap::LoadPathCache::FallbackScan
74
- fallback = true
75
- ensure
76
- if fallback
77
- load_without_bootsnap(path, wrap)
78
- end
79
- end
80
- end
81
-
82
- class Module
83
- alias_method(:autoload_without_bootsnap, :autoload)
84
- def autoload(const, path)
85
- # NOTE: This may defeat LoadedFeaturesIndex, but it's not immediately
86
- # obvious how to make it work. This feels like a pretty niche case, unclear
87
- # if it will ever burn anyone.
88
- #
89
- # The challenge is that we don't control the point at which the entry gets
90
- # added to $LOADED_FEATURES and won't be able to hook that modification
91
- # since it's done in C-land.
92
- autoload_without_bootsnap(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
93
- rescue LoadError => e
94
- e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
95
- raise(e)
96
- rescue Bootsnap::LoadPathCache::ReturnFalse
97
- false
98
- rescue Bootsnap::LoadPathCache::FallbackScan
99
- fallback = true
100
- ensure
101
- if fallback
102
- autoload_without_bootsnap(const, path)
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
+ error = LoadError.new(+"cannot load such file -- #{path}")
28
+ error.instance_variable_set(:@path, path)
29
+ raise error
30
+ else
31
+ # Note that require registers to $LOADED_FEATURES while load does not.
32
+ ret = require_without_bootsnap(resolved)
33
+ Bootsnap::LoadPathCache.loaded_features_index.register(string_path, resolved)
34
+ return ret
103
35
  end
104
36
  end
105
37
  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)
@@ -26,20 +26,21 @@ module Bootsnap
26
26
  class LoadedFeaturesIndex
27
27
  def initialize
28
28
  @lfi = {}
29
- @mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new # TODO: Remove once Ruby 2.2 support is dropped.
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
- short = feat[(lpe.length + 1)..-1]
43
+ short = feat[(lpe.length + 1)..]
43
44
  stripped = strip_extension_if_elidable(short)
44
45
  @lfi[short] = hash
45
46
  @lfi[stripped] = hash
@@ -58,9 +59,9 @@ module Bootsnap
58
59
  end
59
60
 
60
61
  def purge_multi(features)
61
- rejected_hashes = features.map(&:hash).to_set
62
+ rejected_hashes = features.each_with_object({}) { |f, h| h[f.hash] = true }
62
63
  @mutex.synchronize do
63
- @lfi.reject! { |_, hash| rejected_hashes.include?(hash) }
64
+ @lfi.reject! { |_, hash| rejected_hashes.key?(hash) }
64
65
  end
65
66
  end
66
67
 
@@ -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..].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,15 +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
- if long.nil?
87
- pat = %r{/#{Regexp.escape(short)}(\.[^/]+)?$}
88
- len = $LOADED_FEATURES.size
89
- ret = yield
90
- long = $LOADED_FEATURES[len..-1].detect { |feat| feat =~ pat }
91
- else
92
- ret = yield
93
- end
105
+ def register(short, long)
106
+ return if Bootsnap.absolute_path?(short)
94
107
 
95
108
  hash = long.hash
96
109
 
@@ -109,13 +122,11 @@ module Bootsnap
109
122
  @lfi[short] = hash
110
123
  (@lfi[altname] = hash) if altname
111
124
  end
112
-
113
- ret
114
125
  end
115
126
 
116
127
  private
117
128
 
118
- STRIP_EXTENSION = /\.[^.]*?$/
129
+ STRIP_EXTENSION = /\.[^.]*?$/.freeze
119
130
  private_constant(:STRIP_EXTENSION)
120
131
 
121
132
  # Might Ruby automatically search for this extension if
@@ -132,15 +143,15 @@ module Bootsnap
132
143
  # with calling a Ruby file 'x.dylib.rb' and then requiring it as 'x.dylib'.)
133
144
  #
134
145
  # 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')
146
+ def extension_elidable?(feature)
147
+ feature.to_s.end_with?(".rb", ".so", ".o", ".dll", ".dylib")
137
148
  end
138
149
 
139
- def strip_extension_if_elidable(f)
140
- if extension_elidable?(f)
141
- f.sub(STRIP_EXTENSION, '')
150
+ def strip_extension_if_elidable(feature)
151
+ if extension_elidable?(feature)
152
+ feature.sub(STRIP_EXTENSION, "")
142
153
  else
143
- f
154
+ feature
144
155
  end
145
156
  end
146
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,14 +21,32 @@ 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
+ @real = true
40
+ self
41
+ else
42
+ Path.new(realpath, real: true)
43
+ end
25
44
  end
26
45
 
27
46
  # True if the path exists, but represents a non-directory object
28
47
  def non_directory?
29
48
  !File.stat(path).directory?
30
- rescue Errno::ENOENT, Errno::ENOTDIR
49
+ rescue Errno::ENOENT, Errno::ENOTDIR, Errno::EINVAL
31
50
  false
32
51
  end
33
52
 
@@ -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
@@ -77,7 +101,7 @@ module Bootsnap
77
101
  ["", *dirs].each do |dir|
78
102
  curr = begin
79
103
  File.mtime("#{path}/#{dir}").to_i
80
- rescue Errno::ENOENT, Errno::ENOTDIR
104
+ rescue Errno::ENOENT, Errno::ENOTDIR, Errno::EINVAL
81
105
  -1
82
106
  end
83
107
  max = curr if curr > max
@@ -92,21 +116,19 @@ module Bootsnap
92
116
  VOLATILE = :volatile
93
117
 
94
118
  # Built-in ruby lib stuff doesn't change, but things can occasionally be
95
- # installed into sitedir, which generally lives under libdir.
96
- RUBY_LIBDIR = RbConfig::CONFIG['libdir']
97
- RUBY_SITEDIR = RbConfig::CONFIG['sitedir']
119
+ # installed into sitedir, which generally lives under rubylibdir.
120
+ RUBY_LIBDIR = RbConfig::CONFIG["rubylibdir"]
121
+ RUBY_SITEDIR = RbConfig::CONFIG["sitedir"]
98
122
 
99
123
  def stability
100
- @stability ||= begin
101
- if Gem.path.detect { |p| expanded_path.start_with?(p.to_s) }
102
- STABLE
103
- elsif Bootsnap.bundler? && expanded_path.start_with?(Bundler.bundle_path.to_s)
104
- STABLE
105
- elsif expanded_path.start_with?(RUBY_LIBDIR) && !expanded_path.start_with?(RUBY_SITEDIR)
106
- STABLE
107
- else
108
- VOLATILE
109
- end
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
110
132
  end
111
133
  end
112
134
  end
@@ -1,21 +1,25 @@
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
+ @ignored_directories = %w(node_modules)
19
+
18
20
  class << self
21
+ attr_accessor :ignored_directories
22
+
19
23
  def call(path)
20
24
  path = File.expand_path(path.to_s).freeze
21
25
  return [[], []] unless File.directory?(path)
@@ -33,10 +37,10 @@ module Bootsnap
33
37
  requirables = []
34
38
  walk(path, nil) do |relative_path, absolute_path, is_directory|
35
39
  if is_directory
36
- dirs << relative_path
40
+ dirs << os_path(relative_path)
37
41
  !contains_bundle_path || !absolute_path.start_with?(BUNDLE_PATH)
38
42
  elsif relative_path.end_with?(*REQUIRABLE_EXTENSIONS)
39
- requirables << relative_path
43
+ requirables << os_path(relative_path)
40
44
  end
41
45
  end
42
46
  [requirables, dirs]
@@ -44,11 +48,14 @@ module Bootsnap
44
48
 
45
49
  def walk(absolute_dir_path, relative_dir_path, &block)
46
50
  Dir.foreach(absolute_dir_path) do |name|
47
- next if name.start_with?('.')
48
- relative_path = relative_dir_path ? "#{relative_dir_path}/#{name}" : name.freeze
51
+ next if name.start_with?(".")
52
+
53
+ relative_path = relative_dir_path ? File.join(relative_dir_path, name) : name
49
54
 
50
55
  absolute_path = "#{absolute_dir_path}/#{name}"
51
56
  if File.directory?(absolute_path)
57
+ next if ignored_directories.include?(name)
58
+
52
59
  if yield relative_path, absolute_path, true
53
60
  walk(absolute_path, relative_path, &block)
54
61
  end
@@ -57,6 +64,17 @@ module Bootsnap
57
64
  end
58
65
  end
59
66
  end
67
+
68
+ if RUBY_VERSION >= "3.1"
69
+ def os_path(path)
70
+ path.freeze
71
+ end
72
+ else
73
+ def os_path(path)
74
+ path.force_encoding(Encoding::US_ASCII) if path.ascii_only?
75
+ path.freeze
76
+ end
77
+ end
60
78
  end
61
79
  end
62
80
  end
@@ -1,20 +1,23 @@
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 # rubocop:disable Style/RedundantFreeze
12
+
10
13
  NestedTransactionError = Class.new(StandardError)
11
14
  SetOutsideTransactionNotAllowed = Class.new(StandardError)
12
15
 
13
- def initialize(store_path)
16
+ def initialize(store_path, readonly: false)
14
17
  @store_path = store_path
15
- # TODO: Remove conditional once Ruby 2.2 support is dropped.
16
- @txn_mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new
18
+ @txn_mutex = Mutex.new
17
19
  @dirty = false
20
+ @readonly = readonly
18
21
  load_data
19
22
  end
20
23
 
@@ -24,10 +27,11 @@ module Bootsnap
24
27
 
25
28
  def fetch(key)
26
29
  raise(SetOutsideTransactionNotAllowed) unless @txn_mutex.owned?
30
+
27
31
  v = get(key)
28
32
  unless v
29
- @dirty = true
30
33
  v = yield
34
+ mark_for_mutation!
31
35
  @data[key] = v
32
36
  end
33
37
  v
@@ -35,27 +39,32 @@ module Bootsnap
35
39
 
36
40
  def set(key, value)
37
41
  raise(SetOutsideTransactionNotAllowed) unless @txn_mutex.owned?
42
+
38
43
  if value != @data[key]
39
- @dirty = true
44
+ mark_for_mutation!
40
45
  @data[key] = value
41
46
  end
42
47
  end
43
48
 
44
49
  def transaction
45
50
  raise(NestedTransactionError) if @txn_mutex.owned?
51
+
46
52
  @txn_mutex.synchronize do
47
- begin
48
- yield
49
- ensure
50
- commit_transaction
51
- end
53
+ yield
54
+ ensure
55
+ commit_transaction
52
56
  end
53
57
  end
54
58
 
55
59
  private
56
60
 
61
+ def mark_for_mutation!
62
+ @dirty = true
63
+ @data = @data.dup if @data.frozen?
64
+ end
65
+
57
66
  def commit_transaction
58
- if @dirty
67
+ if @dirty && !@readonly
59
68
  dump_data
60
69
  @dirty = false
61
70
  end
@@ -63,27 +72,58 @@ module Bootsnap
63
72
 
64
73
  def load_data
65
74
  @data = begin
66
- MessagePack.load(File.binread(@store_path))
67
- # handle malformed data due to upgrade incompatibility
68
- rescue Errno::ENOENT, MessagePack::MalformedFormatError, MessagePack::UnknownExtTypeError, EOFError
69
- {}
70
- rescue ArgumentError => e
71
- e.message =~ /negative array size/ ? {} : raise
75
+ data = File.open(@store_path, encoding: Encoding::BINARY) do |io|
76
+ MessagePack.load(io, freeze: true)
77
+ end
78
+ if data.is_a?(Hash) && data[VERSION_KEY] == CURRENT_VERSION
79
+ data
80
+ else
81
+ default_data
82
+ end
83
+ # handle malformed data due to upgrade incompatibility
84
+ rescue Errno::ENOENT, MessagePack::MalformedFormatError, MessagePack::UnknownExtTypeError, EOFError
85
+ default_data
86
+ rescue ArgumentError => error
87
+ if error.message =~ /negative array size/
88
+ default_data
89
+ else
90
+ raise
91
+ end
72
92
  end
73
93
  end
74
94
 
75
95
  def dump_data
76
96
  # Change contents atomically so other processes can't get invalid
77
97
  # caches if they read at an inopportune time.
78
- tmp = "#{@store_path}.#{Process.pid}.#{(rand * 100000).to_i}.tmp"
79
- FileUtils.mkpath(File.dirname(tmp))
98
+ tmp = "#{@store_path}.#{Process.pid}.#{(rand * 100_000).to_i}.tmp"
99
+ mkdir_p(File.dirname(tmp))
80
100
  exclusive_write = File::Constants::CREAT | File::Constants::EXCL | File::Constants::WRONLY
81
101
  # `encoding:` looks redundant wrt `binwrite`, but necessary on windows
82
102
  # because binary is part of mode.
83
- File.binwrite(tmp, MessagePack.dump(@data), mode: exclusive_write, encoding: Encoding::BINARY)
84
- FileUtils.mv(tmp, @store_path)
103
+ File.open(tmp, mode: exclusive_write, encoding: Encoding::BINARY) do |io|
104
+ MessagePack.dump(@data, io)
105
+ end
106
+ File.rename(tmp, @store_path)
85
107
  rescue Errno::EEXIST
86
108
  retry
109
+ rescue SystemCallError
110
+ end
111
+
112
+ def default_data
113
+ {VERSION_KEY => CURRENT_VERSION}
114
+ end
115
+
116
+ def mkdir_p(path)
117
+ stack = []
118
+ until File.directory?(path)
119
+ stack.push path
120
+ path = File.dirname(path)
121
+ end
122
+ stack.reverse_each do |dir|
123
+ Dir.mkdir(dir)
124
+ rescue SystemCallError
125
+ raise unless File.directory?(dir)
126
+ end
87
127
  end
88
128
  end
89
129
  end