bootsnap 1.10.3 → 1.18.6

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
@@ -24,8 +24,16 @@ module Bootsnap
24
24
  @mutex.synchronize { @dirs[dir] }
25
25
  end
26
26
 
27
+ TRUFFLERUBY_LIB_DIR_PREFIX = if RUBY_ENGINE == "truffleruby"
28
+ "#{File.join(RbConfig::CONFIG['libdir'], 'truffle')}#{File::SEPARATOR}"
29
+ end
30
+
27
31
  # { 'enumerator' => nil, 'enumerator.so' => nil, ... }
28
32
  BUILTIN_FEATURES = $LOADED_FEATURES.each_with_object({}) do |feat, features|
33
+ if TRUFFLERUBY_LIB_DIR_PREFIX && feat.start_with?(TRUFFLERUBY_LIB_DIR_PREFIX)
34
+ feat = feat.byteslice(TRUFFLERUBY_LIB_DIR_PREFIX.bytesize..-1)
35
+ end
36
+
29
37
  # Builtin features are of the form 'enumerator.so'.
30
38
  # All others include paths.
31
39
  next unless feat.size < 20 && !feat.include?("/")
@@ -45,20 +53,19 @@ module Bootsnap
45
53
 
46
54
  # Try to resolve this feature to an absolute path without traversing the
47
55
  # loadpath.
48
- def find(feature, try_extensions: true)
56
+ def find(feature)
49
57
  reinitialize if (@has_relative_paths && dir_changed?) || stale?
50
58
  feature = feature.to_s.freeze
51
59
 
52
60
  return feature if Bootsnap.absolute_path?(feature)
53
61
 
54
62
  if feature.start_with?("./", "../")
55
- return try_extensions ? expand_path(feature) : File.expand_path(feature).freeze
63
+ return expand_path(feature)
56
64
  end
57
65
 
58
66
  @mutex.synchronize do
59
- x = search_index(feature, try_extensions: try_extensions)
67
+ x = search_index(feature)
60
68
  return x if x
61
- return unless try_extensions
62
69
 
63
70
  # Ruby has some built-in features that require lies about.
64
71
  # For example, 'enumerator' is built in. If you require it, ruby
@@ -115,7 +122,7 @@ module Bootsnap
115
122
  def reinitialize(path_obj = @path_obj)
116
123
  @mutex.synchronize do
117
124
  @path_obj = path_obj
118
- ChangeObserver.register(self, @path_obj)
125
+ ChangeObserver.register(@path_obj, self)
119
126
  @index = {}
120
127
  @dirs = {}
121
128
  @generated_at = now
@@ -142,6 +149,8 @@ module Bootsnap
142
149
  @has_relative_paths = true if p.relative?
143
150
  next if p.non_directory?
144
151
 
152
+ p = p.to_realpath
153
+
145
154
  expanded_path = p.expanded_path
146
155
  entries, dirs = p.entries_and_dirs(@store)
147
156
  # push -> low precedence -> set only if unset
@@ -157,6 +166,8 @@ module Bootsnap
157
166
  p = Path.new(path)
158
167
  next if p.non_directory?
159
168
 
169
+ p = p.to_realpath
170
+
160
171
  expanded_path = p.expanded_path
161
172
  entries, dirs = p.entries_and_dirs(@store)
162
173
  # unshift -> high precedence -> unconditional set
@@ -179,15 +190,11 @@ module Bootsnap
179
190
  end
180
191
 
181
192
  if DLEXT2
182
- def search_index(feature, try_extensions: true)
183
- if try_extensions
184
- try_index(feature + DOT_RB) ||
185
- try_index(feature + DLEXT) ||
186
- try_index(feature + DLEXT2) ||
187
- try_index(feature)
188
- else
193
+ def search_index(feature)
194
+ try_index(feature + DOT_RB) ||
195
+ try_index(feature + DLEXT) ||
196
+ try_index(feature + DLEXT2) ||
189
197
  try_index(feature)
190
- end
191
198
  end
192
199
 
193
200
  def maybe_append_extension(feature)
@@ -197,12 +204,8 @@ module Bootsnap
197
204
  feature
198
205
  end
199
206
  else
200
- def search_index(feature, try_extensions: true)
201
- if try_extensions
202
- try_index(feature + DOT_RB) || try_index(feature + DLEXT) || try_index(feature)
203
- else
204
- try_index(feature)
205
- end
207
+ def search_index(feature)
208
+ try_index(feature + DOT_RB) || try_index(feature + DLEXT) || try_index(feature)
206
209
  end
207
210
 
208
211
  def maybe_append_extension(feature)
@@ -212,7 +215,7 @@ module Bootsnap
212
215
 
213
216
  s = rand.to_s.force_encoding(Encoding::US_ASCII).freeze
214
217
  if s.respond_to?(:-@)
215
- if (-s).equal?(s) && (-s.dup).equal?(s) || RUBY_VERSION >= "2.7"
218
+ if ((-s).equal?(s) && (-s.dup).equal?(s)) || RUBY_VERSION >= "2.7"
216
219
  def try_index(feature)
217
220
  if (path = @index[feature])
218
221
  -File.join(path, feature).freeze
@@ -54,13 +54,30 @@ module Bootsnap
54
54
  ret
55
55
  end
56
56
  end
57
+
58
+ def dup
59
+ [] + self
60
+ end
61
+
62
+ alias_method :clone, :dup
57
63
  end
58
64
 
59
- def self.register(observer, arr)
65
+ def self.register(arr, observer)
60
66
  return if arr.frozen? # can't register observer, but no need to.
61
67
 
62
68
  arr.instance_variable_set(:@lpc_observer, observer)
63
- arr.extend(ArrayMixin)
69
+ ArrayMixin.instance_methods.each do |method_name|
70
+ arr.singleton_class.send(:define_method, method_name, ArrayMixin.instance_method(method_name))
71
+ end
72
+ end
73
+
74
+ def self.unregister(arr)
75
+ return unless arr.instance_variable_defined?(:@lpc_observer) && arr.instance_variable_get(:@lpc_observer)
76
+
77
+ ArrayMixin.instance_methods.each do |method_name|
78
+ arr.singleton_class.send(:remove_method, method_name)
79
+ end
80
+ arr.instance_variable_set(:@lpc_observer, nil)
64
81
  end
65
82
  end
66
83
  end
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kernel
4
- module_function
4
+ alias_method :require_without_bootsnap, :require
5
5
 
6
- alias_method(:require_without_bootsnap, :require)
6
+ alias_method :require, :require # Avoid method redefinition warnings
7
7
 
8
- def require(path)
9
- string_path = path.to_s
8
+ def require(path) # rubocop:disable Lint/DuplicateMethods
9
+ return require_without_bootsnap(path) unless Bootsnap::LoadPathCache.enabled?
10
+
11
+ string_path = Bootsnap.rb_get_path(path)
10
12
  return false if Bootsnap::LoadPathCache.loaded_features_index.key?(string_path)
11
13
 
12
14
  resolved = Bootsnap::LoadPathCache.load_path_cache.find(string_path)
@@ -22,9 +24,7 @@ module Kernel
22
24
  elsif false == resolved
23
25
  return false
24
26
  elsif resolved.nil?
25
- error = LoadError.new(+"cannot load such file -- #{path}")
26
- error.instance_variable_set(:@path, path)
27
- raise error
27
+ return require_without_bootsnap(path)
28
28
  else
29
29
  # Note that require registers to $LOADED_FEATURES while load does not.
30
30
  ret = require_without_bootsnap(resolved)
@@ -33,42 +33,5 @@ module Kernel
33
33
  end
34
34
  end
35
35
 
36
- alias_method(:require_relative_without_bootsnap, :require_relative)
37
- def require_relative(path)
38
- location = caller_locations(1..1).first
39
- realpath = Bootsnap::LoadPathCache.realpath_cache.call(
40
- location.absolute_path || location.path, path
41
- )
42
- require(realpath)
43
- end
44
-
45
- alias_method(:load_without_bootsnap, :load)
46
- def load(path, wrap = false)
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)
51
- end
52
- end
53
- end
54
-
55
- class Module
56
- alias_method(:autoload_without_bootsnap, :autoload)
57
- def autoload(const, path)
58
- # NOTE: This may defeat LoadedFeaturesIndex, but it's not immediately
59
- # obvious how to make it work. This feels like a pretty niche case, unclear
60
- # if it will ever burn anyone.
61
- #
62
- # The challenge is that we don't control the point at which the entry gets
63
- # added to $LOADED_FEATURES and won't be able to hook that modification
64
- # since it's done in C-land.
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
73
- end
36
+ private :require
74
37
  end
@@ -40,7 +40,7 @@ module Bootsnap
40
40
 
41
41
  # /a/b/lib/my/foo.rb
42
42
  # ^^^^^^^^^
43
- short = feat[(lpe.length + 1)..-1]
43
+ short = feat[(lpe.length + 1)..]
44
44
  stripped = strip_extension_if_elidable(short)
45
45
  @lfi[short] = hash
46
46
  @lfi[stripped] = hash
@@ -76,7 +76,7 @@ module Bootsnap
76
76
  end
77
77
 
78
78
  def identify(short, cursor)
79
- $LOADED_FEATURES[cursor..-1].detect do |feat|
79
+ $LOADED_FEATURES[cursor..].detect do |feat|
80
80
  offset = 0
81
81
  while (offset = feat.index(short, offset))
82
82
  if feat.index(".", offset + 1) && !feat.index("/", offset + 2)
@@ -142,7 +142,7 @@ module Bootsnap
142
142
  # will _never_ run on MacOS, and therefore think they can get away
143
143
  # with calling a Ruby file 'x.dylib.rb' and then requiring it as 'x.dylib'.)
144
144
  #
145
- # See <https://ruby-doc.org/core-2.6.4/Kernel.html#method-i-require>.
145
+ # See <https://docs.ruby-lang.org/en/master/Kernel.html#method-i-require>.
146
146
  def extension_elidable?(feature)
147
147
  feature.to_s.end_with?(".rb", ".so", ".o", ".dll", ".dylib")
148
148
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative("path_scanner")
3
+ require_relative "path_scanner"
4
4
 
5
5
  module Bootsnap
6
6
  module LoadPathCache
@@ -21,14 +21,32 @@ module Bootsnap
21
21
 
22
22
  attr_reader(:path)
23
23
 
24
- def initialize(path)
24
+ def initialize(path, real: false)
25
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
26
44
  end
27
45
 
28
46
  # True if the path exists, but represents a non-directory object
29
47
  def non_directory?
30
48
  !File.stat(path).directory?
31
- rescue Errno::ENOENT, Errno::ENOTDIR
49
+ rescue Errno::ENOENT, Errno::ENOTDIR, Errno::EINVAL
32
50
  false
33
51
  end
34
52
 
@@ -62,7 +80,11 @@ module Bootsnap
62
80
  end
63
81
 
64
82
  def expanded_path
65
- File.expand_path(path).freeze
83
+ if @real
84
+ path
85
+ else
86
+ @expanded_path ||= File.expand_path(path).freeze
87
+ end
66
88
  end
67
89
 
68
90
  private
@@ -79,7 +101,7 @@ module Bootsnap
79
101
  ["", *dirs].each do |dir|
80
102
  curr = begin
81
103
  File.mtime("#{path}/#{dir}").to_i
82
- rescue Errno::ENOENT, Errno::ENOTDIR
104
+ rescue Errno::ENOENT, Errno::ENOTDIR, Errno::EINVAL
83
105
  -1
84
106
  end
85
107
  max = curr if curr > max
@@ -94,21 +116,19 @@ module Bootsnap
94
116
  VOLATILE = :volatile
95
117
 
96
118
  # Built-in ruby lib stuff doesn't change, but things can occasionally be
97
- # installed into sitedir, which generally lives under libdir.
98
- RUBY_LIBDIR = RbConfig::CONFIG["libdir"]
119
+ # installed into sitedir, which generally lives under rubylibdir.
120
+ RUBY_LIBDIR = RbConfig::CONFIG["rubylibdir"]
99
121
  RUBY_SITEDIR = RbConfig::CONFIG["sitedir"]
100
122
 
101
123
  def stability
102
- @stability ||= begin
103
- if Gem.path.detect { |p| expanded_path.start_with?(p.to_s) }
104
- STABLE
105
- elsif Bootsnap.bundler? && expanded_path.start_with?(Bundler.bundle_path.to_s)
106
- STABLE
107
- elsif expanded_path.start_with?(RUBY_LIBDIR) && !expanded_path.start_with?(RUBY_SITEDIR)
108
- STABLE
109
- else
110
- VOLATILE
111
- 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
112
132
  end
113
133
  end
114
134
  end
@@ -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
@@ -15,7 +15,11 @@ module Bootsnap
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)
@@ -50,6 +54,8 @@ module Bootsnap
50
54
 
51
55
  absolute_path = "#{absolute_dir_path}/#{name}"
52
56
  if File.directory?(absolute_path)
57
+ next if ignored_directories.include?(name) || ignored_directories.include?(absolute_path)
58
+
53
59
  if yield relative_path, absolute_path, true
54
60
  walk(absolute_path, relative_path, &block)
55
61
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative("../explicit_require")
3
+ require_relative "../explicit_require"
4
4
 
5
- Bootsnap::ExplicitRequire.with_gems("msgpack") { require("msgpack") }
5
+ Bootsnap::ExplicitRequire.with_gems("msgpack") { require "msgpack" }
6
6
 
7
7
  module Bootsnap
8
8
  module LoadPathCache
@@ -13,10 +13,11 @@ module Bootsnap
13
13
  NestedTransactionError = Class.new(StandardError)
14
14
  SetOutsideTransactionNotAllowed = Class.new(StandardError)
15
15
 
16
- def initialize(store_path)
16
+ def initialize(store_path, readonly: false)
17
17
  @store_path = store_path
18
18
  @txn_mutex = Mutex.new
19
19
  @dirty = false
20
+ @readonly = readonly
20
21
  load_data
21
22
  end
22
23
 
@@ -29,8 +30,8 @@ module Bootsnap
29
30
 
30
31
  v = get(key)
31
32
  unless v
32
- @dirty = true
33
33
  v = yield
34
+ mark_for_mutation!
34
35
  @data[key] = v
35
36
  end
36
37
  v
@@ -40,7 +41,7 @@ module Bootsnap
40
41
  raise(SetOutsideTransactionNotAllowed) unless @txn_mutex.owned?
41
42
 
42
43
  if value != @data[key]
43
- @dirty = true
44
+ mark_for_mutation!
44
45
  @data[key] = value
45
46
  end
46
47
  end
@@ -49,18 +50,21 @@ module Bootsnap
49
50
  raise(NestedTransactionError) if @txn_mutex.owned?
50
51
 
51
52
  @txn_mutex.synchronize do
52
- begin
53
- yield
54
- ensure
55
- commit_transaction
56
- end
53
+ yield
54
+ ensure
55
+ commit_transaction
57
56
  end
58
57
  end
59
58
 
60
59
  private
61
60
 
61
+ def mark_for_mutation!
62
+ @dirty = true
63
+ @data = @data.dup if @data.frozen?
64
+ end
65
+
62
66
  def commit_transaction
63
- if @dirty
67
+ if @dirty && !@readonly
64
68
  dump_data
65
69
  @dirty = false
66
70
  end
@@ -69,7 +73,7 @@ module Bootsnap
69
73
  def load_data
70
74
  @data = begin
71
75
  data = File.open(@store_path, encoding: Encoding::BINARY) do |io|
72
- MessagePack.load(io)
76
+ MessagePack.load(io, freeze: true)
73
77
  end
74
78
  if data.is_a?(Hash) && data[VERSION_KEY] == CURRENT_VERSION
75
79
  data
@@ -89,19 +93,17 @@ module Bootsnap
89
93
  end
90
94
 
91
95
  def dump_data
92
- require "fileutils" unless defined? FileUtils
93
-
94
96
  # Change contents atomically so other processes can't get invalid
95
97
  # caches if they read at an inopportune time.
96
98
  tmp = "#{@store_path}.#{Process.pid}.#{(rand * 100_000).to_i}.tmp"
97
- FileUtils.mkpath(File.dirname(tmp))
99
+ mkdir_p(File.dirname(tmp))
98
100
  exclusive_write = File::Constants::CREAT | File::Constants::EXCL | File::Constants::WRONLY
99
101
  # `encoding:` looks redundant wrt `binwrite`, but necessary on windows
100
102
  # because binary is part of mode.
101
103
  File.open(tmp, mode: exclusive_write, encoding: Encoding::BINARY) do |io|
102
- MessagePack.dump(@data, io, freeze: true)
104
+ MessagePack.dump(@data, io)
103
105
  end
104
- FileUtils.mv(tmp, @store_path)
106
+ File.rename(tmp, @store_path)
105
107
  rescue Errno::EEXIST
106
108
  retry
107
109
  rescue SystemCallError
@@ -110,6 +112,21 @@ module Bootsnap
110
112
  def default_data
111
113
  {VERSION_KEY => CURRENT_VERSION}
112
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
+ # Check for broken symlinks. Calling File.realpath will raise Errno::ENOENT if that is the case
126
+ File.realpath(dir) if File.symlink?(dir)
127
+ raise unless File.directory?(dir)
128
+ end
129
+ end
113
130
  end
114
131
  end
115
132
  end
@@ -21,39 +21,60 @@ module Bootsnap
21
21
 
22
22
  CACHED_EXTENSIONS = DLEXT2 ? [DOT_RB, DLEXT, DLEXT2] : [DOT_RB, DLEXT]
23
23
 
24
+ @enabled = false
25
+
24
26
  class << self
25
- attr_reader(:load_path_cache, :loaded_features_index, :realpath_cache)
27
+ attr_reader(:load_path_cache, :loaded_features_index, :enabled)
28
+ alias_method :enabled?, :enabled
29
+ remove_method(:enabled)
26
30
 
27
- def setup(cache_path:, development_mode:)
31
+ def setup(cache_path:, development_mode:, ignore_directories:, readonly: false)
28
32
  unless supported?
29
33
  warn("[bootsnap/setup] Load path caching is not supported on this implementation of Ruby") if $VERBOSE
30
34
  return
31
35
  end
32
36
 
33
- store = Store.new(cache_path)
37
+ store = Store.new(cache_path, readonly: readonly)
34
38
 
35
39
  @loaded_features_index = LoadedFeaturesIndex.new
36
- @realpath_cache = RealpathCache.new
37
40
 
41
+ PathScanner.ignored_directories = ignore_directories if ignore_directories
38
42
  @load_path_cache = Cache.new(store, $LOAD_PATH, development_mode: development_mode)
39
- require_relative("load_path_cache/core_ext/kernel_require")
40
- require_relative("load_path_cache/core_ext/loaded_features")
43
+ @enabled = true
44
+ require_relative "load_path_cache/core_ext/kernel_require"
45
+ require_relative "load_path_cache/core_ext/loaded_features"
46
+ end
47
+
48
+ def unload!
49
+ @enabled = false
50
+ @loaded_features_index = nil
51
+ @realpath_cache = nil
52
+ @load_path_cache = nil
53
+ ChangeObserver.unregister($LOAD_PATH) if supported?
41
54
  end
42
55
 
43
56
  def supported?
44
- RUBY_ENGINE == "ruby" &&
45
- RUBY_PLATFORM =~ /darwin|linux|bsd|mswin|mingw|cygwin/
57
+ if RUBY_PLATFORM.match?(/darwin|linux|bsd|mswin|mingw|cygwin/)
58
+ case RUBY_ENGINE
59
+ when "truffleruby"
60
+ # https://github.com/oracle/truffleruby/issues/3131
61
+ RUBY_ENGINE_VERSION >= "23.1.0"
62
+ when "ruby"
63
+ true
64
+ else
65
+ false
66
+ end
67
+ end
46
68
  end
47
69
  end
48
70
  end
49
71
  end
50
72
 
51
73
  if Bootsnap::LoadPathCache.supported?
52
- require_relative("load_path_cache/path_scanner")
53
- require_relative("load_path_cache/path")
54
- require_relative("load_path_cache/cache")
55
- require_relative("load_path_cache/store")
56
- require_relative("load_path_cache/change_observer")
57
- require_relative("load_path_cache/loaded_features_index")
58
- require_relative("load_path_cache/realpath_cache")
74
+ require_relative "load_path_cache/path_scanner"
75
+ require_relative "load_path_cache/path"
76
+ require_relative "load_path_cache/cache"
77
+ require_relative "load_path_cache/store"
78
+ require_relative "load_path_cache/change_observer"
79
+ require_relative "load_path_cache/loaded_features_index"
59
80
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative("../bootsnap")
3
+ require_relative "../bootsnap"
4
4
 
5
5
  Bootsnap.default_setup
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bootsnap
4
- VERSION = "1.10.3"
4
+ VERSION = "1.18.6"
5
5
  end