bootsnap 1.7.5 → 1.11.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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))
@@ -55,6 +58,7 @@ module Bootsnap
55
58
 
56
59
  def self.register(observer, arr)
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
63
  arr.extend(ArrayMixin)
60
64
  end
@@ -1,79 +1,43 @@
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
9
+ string_path = Bootsnap.rb_get_path(path)
10
+ return false if Bootsnap::LoadPathCache.loaded_features_index.key?(string_path)
33
11
 
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
- realpath = Bootsnap::LoadPathCache.realpath_cache.call(
51
- caller_locations(1..1).first.absolute_path, path
52
- )
53
- require(realpath)
54
- end
55
-
56
36
  alias_method(:load_without_bootsnap, :load)
57
37
  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
38
+ if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(Bootsnap.rb_get_path(path), try_extensions: false))
39
+ load_without_bootsnap(resolved, wrap)
40
+ else
77
41
  load_without_bootsnap(path, wrap)
78
42
  end
79
43
  end
@@ -89,17 +53,13 @@ class Module
89
53
  # The challenge is that we don't control the point at which the entry gets
90
54
  # added to $LOADED_FEATURES and won't be able to hook that modification
91
55
  # 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
56
+ resolved = Bootsnap::LoadPathCache.load_path_cache.find(Bootsnap.rb_get_path(path))
57
+ if Bootsnap::LoadPathCache::FALLBACK_SCAN.equal?(resolved)
102
58
  autoload_without_bootsnap(const, path)
59
+ elsif resolved == false
60
+ return false
61
+ else
62
+ autoload_without_bootsnap(const, resolved || path)
103
63
  end
104
64
  end
105
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]
@@ -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..-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,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,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,12 +1,15 @@
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
 
@@ -23,10 +26,11 @@ module Bootsnap
23
26
 
24
27
  def fetch(key)
25
28
  raise(SetOutsideTransactionNotAllowed) unless @txn_mutex.owned?
29
+
26
30
  v = get(key)
27
31
  unless v
28
- @dirty = true
29
32
  v = yield
33
+ mark_for_mutation!
30
34
  @data[key] = v
31
35
  end
32
36
  v
@@ -34,14 +38,16 @@ module Bootsnap
34
38
 
35
39
  def set(key, value)
36
40
  raise(SetOutsideTransactionNotAllowed) unless @txn_mutex.owned?
41
+
37
42
  if value != @data[key]
38
- @dirty = true
43
+ mark_for_mutation!
39
44
  @data[key] = value
40
45
  end
41
46
  end
42
47
 
43
48
  def transaction
44
49
  raise(NestedTransactionError) if @txn_mutex.owned?
50
+
45
51
  @txn_mutex.synchronize do
46
52
  begin
47
53
  yield
@@ -53,6 +59,11 @@ module Bootsnap
53
59
 
54
60
  private
55
61
 
62
+ def mark_for_mutation!
63
+ @dirty = true
64
+ @data = @data.dup if @data.frozen?
65
+ end
66
+
56
67
  def commit_transaction
57
68
  if @dirty
58
69
  dump_data
@@ -62,15 +73,20 @@ module Bootsnap
62
73
 
63
74
  def load_data
64
75
  @data = begin
65
- File.open(@store_path, encoding: Encoding::BINARY) do |io|
66
- MessagePack.load(io)
76
+ data = File.open(@store_path, encoding: Encoding::BINARY) do |io|
77
+ MessagePack.load(io, freeze: true)
78
+ end
79
+ if data.is_a?(Hash) && data[VERSION_KEY] == CURRENT_VERSION
80
+ data
81
+ else
82
+ default_data
67
83
  end
68
84
  # handle malformed data due to upgrade incompatibility
69
85
  rescue Errno::ENOENT, MessagePack::MalformedFormatError, MessagePack::UnknownExtTypeError, EOFError
70
- {}
86
+ default_data
71
87
  rescue ArgumentError => error
72
88
  if error.message =~ /negative array size/
73
- {}
89
+ default_data
74
90
  else
75
91
  raise
76
92
  end
@@ -80,19 +96,38 @@ module Bootsnap
80
96
  def dump_data
81
97
  # Change contents atomically so other processes can't get invalid
82
98
  # caches if they read at an inopportune time.
83
- tmp = "#{@store_path}.#{Process.pid}.#{(rand * 100000).to_i}.tmp"
84
- FileUtils.mkpath(File.dirname(tmp))
99
+ tmp = "#{@store_path}.#{Process.pid}.#{(rand * 100_000).to_i}.tmp"
100
+ mkdir_p(File.dirname(tmp))
85
101
  exclusive_write = File::Constants::CREAT | File::Constants::EXCL | File::Constants::WRONLY
86
102
  # `encoding:` looks redundant wrt `binwrite`, but necessary on windows
87
103
  # because binary is part of mode.
88
104
  File.open(tmp, mode: exclusive_write, encoding: Encoding::BINARY) do |io|
89
- MessagePack.dump(@data, io, freeze: true)
105
+ MessagePack.dump(@data, io)
90
106
  end
91
- FileUtils.mv(tmp, @store_path)
107
+ File.rename(tmp, @store_path)
92
108
  rescue Errno::EEXIST
93
109
  retry
94
110
  rescue SystemCallError
95
111
  end
112
+
113
+ def default_data
114
+ {VERSION_KEY => CURRENT_VERSION}
115
+ end
116
+
117
+ def mkdir_p(path)
118
+ stack = []
119
+ until File.directory?(path)
120
+ stack.push path
121
+ path = File.dirname(path)
122
+ end
123
+ stack.reverse_each do |dir|
124
+ begin
125
+ Dir.mkdir(dir)
126
+ rescue SystemCallError
127
+ raise unless File.directory?(dir)
128
+ end
129
+ end
130
+ end
96
131
  end
97
132
  end
98
133
  end
@@ -2,20 +2,14 @@
2
2
 
3
3
  module Bootsnap
4
4
  module LoadPathCache
5
- ReturnFalse = Class.new(StandardError)
6
- FallbackScan = Class.new(StandardError)
5
+ FALLBACK_SCAN = BasicObject.new
7
6
 
8
- DOT_RB = '.rb'
9
- DOT_SO = '.so'
10
- SLASH = '/'
11
-
12
- # If a NameError happens several levels deep, don't re-handle it
13
- # all the way up the chain: mark it once and bubble it up without
14
- # more retries.
15
- ERROR_TAG_IVAR = :@__bootsnap_rescued
7
+ DOT_RB = ".rb"
8
+ DOT_SO = ".so"
9
+ SLASH = "/"
16
10
 
17
11
  DL_EXTENSIONS = ::RbConfig::CONFIG
18
- .values_at('DLEXT', 'DLEXT2')
12
+ .values_at("DLEXT", "DLEXT2")
19
13
  .reject { |ext| !ext || ext.empty? }
20
14
  .map { |ext| ".#{ext}" }
21
15
  .freeze
@@ -28,7 +22,7 @@ module Bootsnap
28
22
  CACHED_EXTENSIONS = DLEXT2 ? [DOT_RB, DLEXT, DLEXT2] : [DOT_RB, DLEXT]
29
23
 
30
24
  class << self
31
- attr_reader(:load_path_cache, :loaded_features_index, :realpath_cache)
25
+ attr_reader(:load_path_cache, :loaded_features_index)
32
26
 
33
27
  def setup(cache_path:, development_mode:)
34
28
  unless supported?
@@ -39,27 +33,25 @@ module Bootsnap
39
33
  store = Store.new(cache_path)
40
34
 
41
35
  @loaded_features_index = LoadedFeaturesIndex.new
42
- @realpath_cache = RealpathCache.new
43
36
 
44
37
  @load_path_cache = Cache.new(store, $LOAD_PATH, development_mode: development_mode)
45
- require_relative('load_path_cache/core_ext/kernel_require')
46
- require_relative('load_path_cache/core_ext/loaded_features')
38
+ require_relative("load_path_cache/core_ext/kernel_require")
39
+ require_relative("load_path_cache/core_ext/loaded_features")
47
40
  end
48
41
 
49
42
  def supported?
50
- RUBY_ENGINE == 'ruby' &&
51
- RUBY_PLATFORM =~ /darwin|linux|bsd|mswin|mingw|cygwin/
43
+ RUBY_ENGINE == "ruby" &&
44
+ RUBY_PLATFORM =~ /darwin|linux|bsd|mswin|mingw|cygwin/
52
45
  end
53
46
  end
54
47
  end
55
48
  end
56
49
 
57
50
  if Bootsnap::LoadPathCache.supported?
58
- require_relative('load_path_cache/path_scanner')
59
- require_relative('load_path_cache/path')
60
- require_relative('load_path_cache/cache')
61
- require_relative('load_path_cache/store')
62
- require_relative('load_path_cache/change_observer')
63
- require_relative('load_path_cache/loaded_features_index')
64
- require_relative('load_path_cache/realpath_cache')
51
+ require_relative("load_path_cache/path_scanner")
52
+ require_relative("load_path_cache/path")
53
+ require_relative("load_path_cache/cache")
54
+ require_relative("load_path_cache/store")
55
+ require_relative("load_path_cache/change_observer")
56
+ require_relative("load_path_cache/loaded_features_index")
65
57
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
- require_relative('../bootsnap')
2
+
3
+ require_relative("../bootsnap")
3
4
 
4
5
  Bootsnap.default_setup
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Bootsnap
3
- VERSION = "1.7.5"
4
+ VERSION = "1.11.1"
4
5
  end