bootsnap 1.10.1 → 1.11.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7c66adff2d954aee5f0f0b6beae724c2ec1a2abedb6cc6ffd1b3eb84a0481acf
4
- data.tar.gz: 91a512ba361da721632679a2942d84128fe301bfc0750b48ec3c98989433072b
3
+ metadata.gz: af127c58fb31e53309de406862116407d9f57d43514937d400a0f87c13080447
4
+ data.tar.gz: 497f3a471eed46b68e624f8cda87bc6d5e97d5a17bdf4cbeb2221019fb21d531
5
5
  SHA512:
6
- metadata.gz: 74613cfa0f4b1b337cf60c2e60f8f51c8726321953addfc1166708af567bdae486ea38b5ff9f1ad63ec13bee2c7142be0cde9fd9d68bf2eca4c044936b575f17
7
- data.tar.gz: 9449f4e75ebe813e5df1fd2c71c1e334853c67feaa0a4e5351a0bcc53d6479993ffc59b37412f81853dce6154e36a165e5e5fb6c311c2ef5cf561afc889a51e2
6
+ metadata.gz: 32c3bba78bf66bb71b47d78e06592e4a670735d581c84043b4c125704fb91d4cff900da0e2884c910f3c66ddf9e31f4860a33718da978cc9f2719e32a9f1f617
7
+ data.tar.gz: 11a5bdee97b35fc99d346ae2cd44e95e8fc3cfd949bddc06a7561b7329fd42a0e63ef5e747a90fa599125100cc368d9ef6c9ad207a1a69fe51f6c32e71992611
data/CHANGELOG.md CHANGED
@@ -1,8 +1,47 @@
1
1
  # Unreleased
2
2
 
3
+ # 1.11.0
4
+
5
+ * Drop dependency on `fileutils`.
6
+
7
+ * Better respect `Kernel#require` duck typing. While it almost never comes up in practice, `Kernel#require`
8
+ follow a fairly intricate duck-typing protocol on its argument implemented as `rb_get_path(VALUE)` in MRI.
9
+ So when applicable we bind `rb_get_path` and use it for improved compatibility. See #396 and #406.
10
+
11
+ * Get rid of the `Kernel.require_relative` decorator by resolving `$LOAD_PATH` members to their real path.
12
+ This way we handle symlinks in `$LOAD_PATH` much more efficiently. See #402 for the detailed explanation.
13
+
14
+ * Drop support for Ruby 2.3 (to allow getting rid of the `Kernel.require_relative` decorator).
15
+
16
+ # 1.10.3
17
+
18
+ * Fix Regexp and Date type support in YAML compile cache. (#400)
19
+
20
+ * Improve the YAML compile cache to support `UTF-8` symbols. (#398, #399)
21
+ [The default `MessagePack` symbol serializer assumes all symbols are ASCII](https://github.com/msgpack/msgpack-ruby/pull/211),
22
+ because of this, non-ASCII compatible symbol would be restored with `ASCII_8BIT` encoding (AKA `BINARY`).
23
+ Bootsnap now properly cache them in `UTF-8`.
24
+
25
+ Note that the above only apply for actual YAML symbols (e..g `--- :foo`).
26
+ The issue is still present for string keys parsed with `YAML.load_file(..., symbolize_names: true)`, that is a bug
27
+ in `msgpack` that will hopefully be solved soon, see: https://github.com/msgpack/msgpack-ruby/pull/246
28
+
29
+ * Entirely disable the YAML compile cache if `Encoding.default_internal` is set to an encoding not supported by `msgpack`. (#398)
30
+ `Psych` coerce strings to `Encoding.default_internal`, but `MessagePack` doesn't. So in this scenario we can't provide
31
+ YAML caching at all without returning the strings in the wrong encoding.
32
+ This never came up in practice but might as well be safe.
33
+
34
+ # 1.10.2
35
+
36
+ * Reduce the `Kernel.require` extra stack frames some more. Now bootsnap should only add one extra frame per `require` call.
37
+
38
+ * Better check `freeze` option support in JSON compile cache.
39
+ Previously `JSON.load_file(..., freeze: true)` would be cached even when the msgpack version is missing support for it.
40
+
3
41
  # 1.10.1
4
42
 
5
- * Fix `Kernel#autoload`'s fallback path always bing executed.
43
+ * Fix `Kernel#autoload`'s fallback path always being executed.
44
+
6
45
  * Consider `unlink` failing with `ENOENT` as a success.
7
46
 
8
47
  # 1.10.0
@@ -14,7 +53,7 @@
14
53
  Since `1.8.0`, `YAML.load_file` was no longer cached when Psych 4 was used. This is because `load_file` loads
15
54
  in safe mode by default, so the Bootsnap cache could defeat that safety.
16
55
  Now when precompiling YAML files, Bootsnap first try to parse them in safe mode, and if it can't fallback to unsafe mode,
17
- and the cache contains a flag that records wether it was generated in safe mode or not.
56
+ and the cache contains a flag that records whether it was generated in safe mode or not.
18
57
  `YAML.unsafe_load_file` will use safe caches just fine, but `YAML.load_file` will fallback to uncached YAML parsing
19
58
  if the cache was generated using unsafe parsing.
20
59
 
@@ -55,7 +94,7 @@
55
94
 
56
95
  # 1.8.0
57
96
 
58
- * Improve support for Pysch 4. (#368)
97
+ * Improve support for Psych 4. (#368)
59
98
 
60
99
  # 1.7.7
61
100
 
@@ -73,8 +112,8 @@
73
112
 
74
113
  # 1.7.4
75
114
 
76
- * Stop raising errors when encoutering various file system errors. The cache is now best effort,
77
- if somehow it can't be saved, bootsnapp will gracefully fallback to the original operation (e.g. `Kernel.require`).
115
+ * Stop raising errors when encountering various file system errors. The cache is now best effort,
116
+ if somehow it can't be saved, bootsnap will gracefully fallback to the original operation (e.g. `Kernel.require`).
78
117
  (#353, #177, #262)
79
118
 
80
119
  # 1.7.3
@@ -135,6 +135,12 @@ bs_rb_coverage_running(VALUE self)
135
135
  return RTEST(cov) ? Qtrue : Qfalse;
136
136
  }
137
137
 
138
+ static VALUE
139
+ bs_rb_get_path(VALUE self, VALUE fname)
140
+ {
141
+ return rb_get_path(fname);
142
+ }
143
+
138
144
  /*
139
145
  * Ruby C extensions are initialized by calling Init_<extname>.
140
146
  *
@@ -146,6 +152,9 @@ void
146
152
  Init_bootsnap(void)
147
153
  {
148
154
  rb_mBootsnap = rb_define_module("Bootsnap");
155
+
156
+ rb_define_singleton_method(rb_mBootsnap, "rb_get_path", bs_rb_get_path, 1);
157
+
149
158
  rb_mBootsnap_CompileCache = rb_define_module_under(rb_mBootsnap, "CompileCache");
150
159
  rb_mBootsnap_CompileCache_Native = rb_define_module_under(rb_mBootsnap_CompileCache, "Native");
151
160
  rb_cBootsnap_CompileCache_UNCOMPILABLE = rb_const_get(rb_mBootsnap_CompileCache, rb_intern("UNCOMPILABLE"));
@@ -52,7 +52,9 @@ module Bootsnap
52
52
  self.msgpack_factory = MessagePack::Factory.new
53
53
  self.supported_options = [:symbolize_names]
54
54
  if ::JSON.parse('["foo"]', freeze: true).first.frozen?
55
- self.supported_options = [:freeze]
55
+ if MessagePack.load(MessagePack.dump("foo"), freeze: true).frozen?
56
+ self.supported_options = [:freeze]
57
+ end
56
58
  end
57
59
  supported_options.freeze
58
60
  end
@@ -5,7 +5,15 @@ require("bootsnap/bootsnap")
5
5
  module Bootsnap
6
6
  module CompileCache
7
7
  module YAML
8
- UnsupportedTags = Class.new(StandardError)
8
+ Uncompilable = Class.new(StandardError)
9
+ UnsupportedTags = Class.new(Uncompilable)
10
+
11
+ SUPPORTED_INTERNAL_ENCODINGS = [
12
+ nil, # UTF-8
13
+ Encoding::UTF_8,
14
+ Encoding::ASCII,
15
+ Encoding::BINARY,
16
+ ].freeze
9
17
 
10
18
  class << self
11
19
  attr_accessor(:msgpack_factory, :supported_options)
@@ -16,7 +24,9 @@ module Bootsnap
16
24
  end
17
25
 
18
26
  def precompile(path)
19
- Bootsnap::CompileCache::Native.precompile(
27
+ return false unless CompileCache::YAML.supported_internal_encoding?
28
+
29
+ CompileCache::Native.precompile(
20
30
  cache_dir,
21
31
  path.to_s,
22
32
  @implementation,
@@ -29,6 +39,21 @@ module Bootsnap
29
39
  ::YAML.singleton_class.prepend(@implementation::Patch)
30
40
  end
31
41
 
42
+ # Psych coerce strings to `Encoding.default_internal` but Message Pack only support
43
+ # UTF-8, US-ASCII and BINARY. So if Encoding.default_internal is set to anything else
44
+ # we can't safely use the cache
45
+ def supported_internal_encoding?
46
+ SUPPORTED_INTERNAL_ENCODINGS.include?(Encoding.default_internal)
47
+ end
48
+
49
+ module EncodingAwareSymbols
50
+ extend self
51
+
52
+ def unpack(payload)
53
+ (+payload).force_encoding(Encoding::UTF_8).to_sym
54
+ end
55
+ end
56
+
32
57
  def init!
33
58
  require("yaml")
34
59
  require("msgpack")
@@ -43,7 +68,12 @@ module Bootsnap
43
68
  # We want them to roundtrip cleanly, so we use a custom factory.
44
69
  # see: https://github.com/msgpack/msgpack-ruby/pull/122
45
70
  factory = MessagePack::Factory.new
46
- factory.register_type(0x00, Symbol)
71
+ factory.register_type(
72
+ 0x00,
73
+ Symbol,
74
+ packer: :to_msgpack_ext,
75
+ unpacker: EncodingAwareSymbols.method(:unpack).to_proc,
76
+ )
47
77
 
48
78
  if defined? MessagePack::Timestamp
49
79
  factory.register_type(
@@ -119,13 +149,15 @@ module Bootsnap
119
149
  extend self
120
150
 
121
151
  def input_to_storage(contents, _)
122
- obj = CompileCache::YAML.strict_load(contents)
152
+ obj = ::YAML.unsafe_load(contents)
123
153
  packer = CompileCache::YAML.msgpack_factory.packer
124
154
  packer.pack(false) # not safe loaded
125
- packer.pack(obj)
155
+ begin
156
+ packer.pack(obj)
157
+ rescue NoMethodError, RangeError
158
+ return UNCOMPILABLE # The object included things that we can't serialize
159
+ end
126
160
  packer.to_s
127
- rescue NoMethodError, RangeError, UnsupportedTags
128
- UNCOMPILABLE # The object included things that we can't serialize
129
161
  end
130
162
 
131
163
  def storage_to_output(data, kwargs)
@@ -148,13 +180,20 @@ module Bootsnap
148
180
  extend self
149
181
 
150
182
  def input_to_storage(contents, _)
151
- obj = ::YAML.load(contents)
183
+ obj = begin
184
+ CompileCache::YAML.strict_load(contents)
185
+ rescue Psych::DisallowedClass, Psych::BadAlias, Uncompilable
186
+ return UNCOMPILABLE
187
+ end
188
+
152
189
  packer = CompileCache::YAML.msgpack_factory.packer
153
190
  packer.pack(true) # safe loaded
154
- packer.pack(obj)
191
+ begin
192
+ packer.pack(obj)
193
+ rescue NoMethodError, RangeError
194
+ return UNCOMPILABLE
195
+ end
155
196
  packer.to_s
156
- rescue NoMethodError, RangeError, Psych::DisallowedClass, Psych::BadAlias
157
- UNCOMPILABLE # The object included things that we can't serialize
158
197
  end
159
198
 
160
199
  def storage_to_output(data, kwargs)
@@ -179,44 +218,48 @@ module Bootsnap
179
218
 
180
219
  module Patch
181
220
  def load_file(path, *args)
221
+ return super unless CompileCache::YAML.supported_internal_encoding?
222
+
182
223
  return super if args.size > 1
183
224
 
184
225
  if (kwargs = args.first)
185
226
  return super unless kwargs.is_a?(Hash)
186
- return super unless (kwargs.keys - ::Bootsnap::CompileCache::YAML.supported_options).empty?
227
+ return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty?
187
228
  end
188
229
 
189
230
  begin
190
- ::Bootsnap::CompileCache::Native.fetch(
191
- Bootsnap::CompileCache::YAML.cache_dir,
231
+ CompileCache::Native.fetch(
232
+ CompileCache::YAML.cache_dir,
192
233
  File.realpath(path),
193
- ::Bootsnap::CompileCache::YAML::Psych4::SafeLoad,
234
+ CompileCache::YAML::Psych4::SafeLoad,
194
235
  kwargs,
195
236
  )
196
237
  rescue Errno::EACCES
197
- ::Bootsnap::CompileCache.permission_error(path)
238
+ CompileCache.permission_error(path)
198
239
  end
199
240
  end
200
241
 
201
242
  ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true)
202
243
 
203
244
  def unsafe_load_file(path, *args)
245
+ return super unless CompileCache::YAML.supported_internal_encoding?
246
+
204
247
  return super if args.size > 1
205
248
 
206
249
  if (kwargs = args.first)
207
250
  return super unless kwargs.is_a?(Hash)
208
- return super unless (kwargs.keys - ::Bootsnap::CompileCache::YAML.supported_options).empty?
251
+ return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty?
209
252
  end
210
253
 
211
254
  begin
212
- ::Bootsnap::CompileCache::Native.fetch(
213
- Bootsnap::CompileCache::YAML.cache_dir,
255
+ CompileCache::Native.fetch(
256
+ CompileCache::YAML.cache_dir,
214
257
  File.realpath(path),
215
- ::Bootsnap::CompileCache::YAML::Psych4::UnsafeLoad,
258
+ CompileCache::YAML::Psych4::UnsafeLoad,
216
259
  kwargs,
217
260
  )
218
261
  rescue Errno::EACCES
219
- ::Bootsnap::CompileCache.permission_error(path)
262
+ CompileCache.permission_error(path)
220
263
  end
221
264
  end
222
265
 
@@ -228,13 +271,15 @@ module Bootsnap
228
271
  extend self
229
272
 
230
273
  def input_to_storage(contents, _)
231
- obj = CompileCache::YAML.strict_load(contents)
274
+ obj = ::YAML.load(contents)
232
275
  packer = CompileCache::YAML.msgpack_factory.packer
233
276
  packer.pack(false) # not safe loaded
234
- packer.pack(obj)
277
+ begin
278
+ packer.pack(obj)
279
+ rescue NoMethodError, RangeError
280
+ return UNCOMPILABLE # The object included things that we can't serialize
281
+ end
235
282
  packer.to_s
236
- rescue NoMethodError, RangeError, UnsupportedTags
237
- UNCOMPILABLE # The object included things that we can't serialize
238
283
  end
239
284
 
240
285
  def storage_to_output(data, kwargs)
@@ -253,44 +298,48 @@ module Bootsnap
253
298
 
254
299
  module Patch
255
300
  def load_file(path, *args)
301
+ return super unless CompileCache::YAML.supported_internal_encoding?
302
+
256
303
  return super if args.size > 1
257
304
 
258
305
  if (kwargs = args.first)
259
306
  return super unless kwargs.is_a?(Hash)
260
- return super unless (kwargs.keys - ::Bootsnap::CompileCache::YAML.supported_options).empty?
307
+ return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty?
261
308
  end
262
309
 
263
310
  begin
264
- ::Bootsnap::CompileCache::Native.fetch(
265
- Bootsnap::CompileCache::YAML.cache_dir,
311
+ CompileCache::Native.fetch(
312
+ CompileCache::YAML.cache_dir,
266
313
  File.realpath(path),
267
- ::Bootsnap::CompileCache::YAML::Psych3,
314
+ CompileCache::YAML::Psych3,
268
315
  kwargs,
269
316
  )
270
317
  rescue Errno::EACCES
271
- ::Bootsnap::CompileCache.permission_error(path)
318
+ CompileCache.permission_error(path)
272
319
  end
273
320
  end
274
321
 
275
322
  ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true)
276
323
 
277
324
  def unsafe_load_file(path, *args)
325
+ return super unless CompileCache::YAML.supported_internal_encoding?
326
+
278
327
  return super if args.size > 1
279
328
 
280
329
  if (kwargs = args.first)
281
330
  return super unless kwargs.is_a?(Hash)
282
- return super unless (kwargs.keys - ::Bootsnap::CompileCache::YAML.supported_options).empty?
331
+ return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty?
283
332
  end
284
333
 
285
334
  begin
286
- ::Bootsnap::CompileCache::Native.fetch(
287
- Bootsnap::CompileCache::YAML.cache_dir,
335
+ CompileCache::Native.fetch(
336
+ CompileCache::YAML.cache_dir,
288
337
  File.realpath(path),
289
- ::Bootsnap::CompileCache::YAML::Psych3,
338
+ CompileCache::YAML::Psych3,
290
339
  kwargs,
291
340
  )
292
341
  rescue Errno::EACCES
293
- ::Bootsnap::CompileCache.permission_error(path)
342
+ CompileCache.permission_error(path)
294
343
  end
295
344
  end
296
345
 
@@ -3,6 +3,9 @@
3
3
  module Bootsnap
4
4
  module CompileCache
5
5
  UNCOMPILABLE = BasicObject.new
6
+ def UNCOMPILABLE.inspect
7
+ "<Bootsnap::UNCOMPILABLE>"
8
+ end
6
9
 
7
10
  Error = Class.new(StandardError)
8
11
  PermissionError = Class.new(Error)
@@ -47,9 +50,7 @@ module Bootsnap
47
50
 
48
51
  def self.supported?
49
52
  # only enable on 'ruby' (MRI), POSIX (darwin, linux, *bsd), Windows (RubyInstaller2) and >= 2.3.0
50
- RUBY_ENGINE == "ruby" &&
51
- RUBY_PLATFORM =~ /darwin|linux|bsd|mswin|mingw|cygwin/ &&
52
- Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.3.0")
53
+ RUBY_ENGINE == "ruby" && RUBY_PLATFORM.match?(/darwin|linux|bsd|mswin|mingw|cygwin/)
53
54
  end
54
55
  end
55
56
  end
@@ -65,7 +65,7 @@ module Bootsnap
65
65
  # returns false as if it were already loaded; however, there is no
66
66
  # file to find on disk. We've pre-built a list of these, and we
67
67
  # return false if any of them is loaded.
68
- raise(LoadPathCache::ReturnFalse, "", []) if BUILTIN_FEATURES.key?(feature)
68
+ return false if BUILTIN_FEATURES.key?(feature)
69
69
 
70
70
  # The feature wasn't found on our preliminary search through the index.
71
71
  # We resolve this differently depending on what the extension was.
@@ -89,7 +89,7 @@ module Bootsnap
89
89
  else
90
90
  # other, unknown extension. For example, `.rake`. Since we haven't
91
91
  # cached these, we legitimately need to run the load path search.
92
- raise(LoadPathCache::FallbackScan, "", [])
92
+ return FALLBACK_SCAN
93
93
  end
94
94
  end
95
95
 
@@ -97,7 +97,7 @@ module Bootsnap
97
97
  # cases where the file doesn't appear to be on the load path. We should
98
98
  # be able to detect newly-created files without rebooting the
99
99
  # application.
100
- raise(LoadPathCache::FallbackScan, "", []) if @development_mode
100
+ return FALLBACK_SCAN if @development_mode
101
101
  end
102
102
 
103
103
  def unshift_paths(sender, *paths)
@@ -142,6 +142,8 @@ module Bootsnap
142
142
  @has_relative_paths = true if p.relative?
143
143
  next if p.non_directory?
144
144
 
145
+ p = p.to_realpath
146
+
145
147
  expanded_path = p.expanded_path
146
148
  entries, dirs = p.entries_and_dirs(@store)
147
149
  # push -> low precedence -> set only if unset
@@ -157,6 +159,8 @@ module Bootsnap
157
159
  p = Path.new(path)
158
160
  next if p.non_directory?
159
161
 
162
+ p = p.to_realpath
163
+
160
164
  expanded_path = p.expanded_path
161
165
  entries, dirs = p.entries_and_dirs(@store)
162
166
  # unshift -> high precedence -> unconditional set
@@ -1,70 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Bootsnap
4
- module LoadPathCache
5
- module CoreExt
6
- def self.make_load_error(path)
7
- err = LoadError.new(+"cannot load such file -- #{path}")
8
- err.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
9
- err.define_singleton_method(:path) { path }
10
- err
11
- end
12
- end
13
- end
14
- end
15
-
16
3
  module Kernel
17
4
  module_function
18
5
 
19
6
  alias_method(:require_without_bootsnap, :require)
20
7
 
21
8
  def require(path)
22
- fallback = false
23
- string_path = path.to_s
9
+ string_path = Bootsnap.rb_get_path(path)
24
10
  return false if Bootsnap::LoadPathCache.loaded_features_index.key?(string_path)
25
11
 
26
- if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(string_path))
27
- # Note that require registers to $LOADED_FEATURES while load does not.
28
- ret = require_without_bootsnap(resolved)
29
- Bootsnap::LoadPathCache.loaded_features_index.register(string_path, resolved)
30
- return ret
31
- end
32
-
33
- raise(Bootsnap::LoadPathCache::CoreExt.make_load_error(path))
34
- rescue LoadError => error
35
- error.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
36
- raise(error)
37
- rescue Bootsnap::LoadPathCache::ReturnFalse
38
- false
39
- rescue Bootsnap::LoadPathCache::FallbackScan
40
- fallback = true
41
- ensure
42
- # We raise from `ensure` so that any further exception don't have FallbackScan as a cause
43
- # See: https://github.com/Shopify/bootsnap/issues/250
44
- if fallback
12
+ resolved = Bootsnap::LoadPathCache.load_path_cache.find(string_path)
13
+ if Bootsnap::LoadPathCache::FALLBACK_SCAN.equal?(resolved)
45
14
  if (cursor = Bootsnap::LoadPathCache.loaded_features_index.cursor(string_path))
46
15
  ret = require_without_bootsnap(path)
47
16
  resolved = Bootsnap::LoadPathCache.loaded_features_index.identify(string_path, cursor)
48
17
  Bootsnap::LoadPathCache.loaded_features_index.register(string_path, resolved)
49
- ret
50
- else # If we're not given a cursor, it means we don't need to register the path (likely an absolute path)
51
- require_without_bootsnap(path)
18
+ return ret
19
+ else
20
+ return require_without_bootsnap(path)
52
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
53
33
  end
54
34
  end
55
35
 
56
- alias_method(:require_relative_without_bootsnap, :require_relative)
57
- def require_relative(path)
58
- location = caller_locations(1..1).first
59
- realpath = Bootsnap::LoadPathCache.realpath_cache.call(
60
- location.absolute_path || location.path, path
61
- )
62
- require(realpath)
63
- end
64
-
65
36
  alias_method(:load_without_bootsnap, :load)
66
37
  def load(path, wrap = false)
67
- 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))
68
39
  load_without_bootsnap(resolved, wrap)
69
40
  else
70
41
  load_without_bootsnap(path, wrap)
@@ -75,7 +46,6 @@ end
75
46
  class Module
76
47
  alias_method(:autoload_without_bootsnap, :autoload)
77
48
  def autoload(const, path)
78
- fallback = false
79
49
  # NOTE: This may defeat LoadedFeaturesIndex, but it's not immediately
80
50
  # obvious how to make it work. This feels like a pretty niche case, unclear
81
51
  # if it will ever burn anyone.
@@ -83,19 +53,13 @@ class Module
83
53
  # The challenge is that we don't control the point at which the entry gets
84
54
  # added to $LOADED_FEATURES and won't be able to hook that modification
85
55
  # since it's done in C-land.
86
- autoload_without_bootsnap(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
87
- rescue LoadError => error
88
- error.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
89
- raise(error)
90
- rescue Bootsnap::LoadPathCache::ReturnFalse
91
- false
92
- rescue Bootsnap::LoadPathCache::FallbackScan
93
- fallback = true
94
- ensure
95
- # We raise from `ensure` so that any further exception don't have FallbackScan as a cause
96
- # See: https://github.com/Shopify/bootsnap/issues/250
97
- if fallback
56
+ resolved = Bootsnap::LoadPathCache.load_path_cache.find(Bootsnap.rb_get_path(path))
57
+ if Bootsnap::LoadPathCache::FALLBACK_SCAN.equal?(resolved)
98
58
  autoload_without_bootsnap(const, path)
59
+ elsif resolved == false
60
+ return false
61
+ else
62
+ autoload_without_bootsnap(const, resolved || path)
99
63
  end
100
64
  end
101
65
  end
@@ -92,7 +92,7 @@ module Bootsnap
92
92
  # entry:
93
93
  #
94
94
  # If the user asked for e.g. `require 'bundler'`, and we went through the
95
- # `FallbackScan` pathway in `kernel_require.rb` and therefore did not
95
+ # `FALLBACK_SCAN` pathway in `kernel_require.rb` and therefore did not
96
96
  # pass `long` (the full expanded absolute path), then we did are not able
97
97
  # to confidently add the `bundler.rb` form to @lfi.
98
98
  #
@@ -21,8 +21,26 @@ 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
+ Path.new(realpath, real: true)
40
+ else
41
+ @real = true
42
+ self
43
+ end
26
44
  end
27
45
 
28
46
  # True if the path exists, but represents a non-directory object
@@ -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
@@ -69,7 +69,7 @@ module Bootsnap
69
69
  def load_data
70
70
  @data = begin
71
71
  data = File.open(@store_path, encoding: Encoding::BINARY) do |io|
72
- MessagePack.load(io)
72
+ MessagePack.load(io, freeze: true)
73
73
  end
74
74
  if data.is_a?(Hash) && data[VERSION_KEY] == CURRENT_VERSION
75
75
  data
@@ -89,19 +89,17 @@ module Bootsnap
89
89
  end
90
90
 
91
91
  def dump_data
92
- require "fileutils" unless defined? FileUtils
93
-
94
92
  # Change contents atomically so other processes can't get invalid
95
93
  # caches if they read at an inopportune time.
96
94
  tmp = "#{@store_path}.#{Process.pid}.#{(rand * 100_000).to_i}.tmp"
97
- FileUtils.mkpath(File.dirname(tmp))
95
+ mkdir_p(File.dirname(tmp))
98
96
  exclusive_write = File::Constants::CREAT | File::Constants::EXCL | File::Constants::WRONLY
99
97
  # `encoding:` looks redundant wrt `binwrite`, but necessary on windows
100
98
  # because binary is part of mode.
101
99
  File.open(tmp, mode: exclusive_write, encoding: Encoding::BINARY) do |io|
102
- MessagePack.dump(@data, io, freeze: true)
100
+ MessagePack.dump(@data, io)
103
101
  end
104
- FileUtils.mv(tmp, @store_path)
102
+ File.rename(tmp, @store_path)
105
103
  rescue Errno::EEXIST
106
104
  retry
107
105
  rescue SystemCallError
@@ -110,6 +108,21 @@ module Bootsnap
110
108
  def default_data
111
109
  {VERSION_KEY => CURRENT_VERSION}
112
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
125
+ end
113
126
  end
114
127
  end
115
128
  end
@@ -2,18 +2,12 @@
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
7
  DOT_RB = ".rb"
9
8
  DOT_SO = ".so"
10
9
  SLASH = "/"
11
10
 
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
16
-
17
11
  DL_EXTENSIONS = ::RbConfig::CONFIG
18
12
  .values_at("DLEXT", "DLEXT2")
19
13
  .reject { |ext| !ext || ext.empty? }
@@ -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,7 +33,6 @@ 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
38
  require_relative("load_path_cache/core_ext/kernel_require")
@@ -61,5 +54,4 @@ if Bootsnap::LoadPathCache.supported?
61
54
  require_relative("load_path_cache/store")
62
55
  require_relative("load_path_cache/change_observer")
63
56
  require_relative("load_path_cache/loaded_features_index")
64
- require_relative("load_path_cache/realpath_cache")
65
57
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bootsnap
4
- VERSION = "1.10.1"
4
+ VERSION = "1.11.0"
5
5
  end
data/lib/bootsnap.rb CHANGED
@@ -10,128 +10,139 @@ module Bootsnap
10
10
 
11
11
  class << self
12
12
  attr_reader :logger
13
- end
14
-
15
- def self.log!
16
- self.logger = $stderr.method(:puts)
17
- end
18
13
 
19
- def self.logger=(logger)
20
- @logger = logger
21
- self.instrumentation = if logger.respond_to?(:debug)
22
- ->(event, path) { @logger.debug("[Bootsnap] #{event} #{path}") }
23
- else
24
- ->(event, path) { @logger.call("[Bootsnap] #{event} #{path}") }
14
+ def log!
15
+ self.logger = $stderr.method(:puts)
25
16
  end
26
- end
27
17
 
28
- def self.instrumentation=(callback)
29
- @instrumentation = callback
30
- if respond_to?(:instrumentation_enabled=, true)
31
- self.instrumentation_enabled = !!callback
18
+ def logger=(logger)
19
+ @logger = logger
20
+ self.instrumentation = if logger.respond_to?(:debug)
21
+ ->(event, path) { @logger.debug("[Bootsnap] #{event} #{path}") }
22
+ else
23
+ ->(event, path) { @logger.call("[Bootsnap] #{event} #{path}") }
24
+ end
32
25
  end
33
- end
34
-
35
- def self._instrument(event, path)
36
- @instrumentation.call(event, path)
37
- end
38
26
 
39
- def self.setup(
40
- cache_dir:,
41
- development_mode: true,
42
- load_path_cache: true,
43
- autoload_paths_cache: nil,
44
- disable_trace: nil,
45
- compile_cache_iseq: true,
46
- compile_cache_yaml: true,
47
- compile_cache_json: true
48
- )
49
- unless autoload_paths_cache.nil?
50
- warn "[DEPRECATED] Bootsnap's `autoload_paths_cache:` option is deprecated and will be removed. " \
51
- "If you use Zeitwerk this option is useless, and if you are still using the classic autoloader " \
52
- "upgrading is recommended."
27
+ def instrumentation=(callback)
28
+ @instrumentation = callback
29
+ if respond_to?(:instrumentation_enabled=, true)
30
+ self.instrumentation_enabled = !!callback
31
+ end
53
32
  end
54
33
 
55
- unless disable_trace.nil?
56
- warn "[DEPRECATED] Bootsnap's `disable_trace:` option is deprecated and will be removed. " \
57
- "If you use Ruby 2.5 or newer this option is useless, if not upgrading is recommended."
34
+ def _instrument(event, path)
35
+ @instrumentation.call(event, path)
58
36
  end
59
37
 
60
- if compile_cache_iseq && !iseq_cache_supported?
61
- warn "Ruby 2.5 has a bug that break code tracing when code is loaded from cache. It is recommened " \
62
- "to turn `compile_cache_iseq` off on Ruby 2.5"
63
- end
38
+ def setup(
39
+ cache_dir:,
40
+ development_mode: true,
41
+ load_path_cache: true,
42
+ autoload_paths_cache: nil,
43
+ disable_trace: nil,
44
+ compile_cache_iseq: true,
45
+ compile_cache_yaml: true,
46
+ compile_cache_json: true
47
+ )
48
+ unless autoload_paths_cache.nil?
49
+ warn "[DEPRECATED] Bootsnap's `autoload_paths_cache:` option is deprecated and will be removed. " \
50
+ "If you use Zeitwerk this option is useless, and if you are still using the classic autoloader " \
51
+ "upgrading is recommended."
52
+ end
64
53
 
65
- if load_path_cache
66
- Bootsnap::LoadPathCache.setup(
67
- cache_path: cache_dir + "/bootsnap/load-path-cache",
68
- development_mode: development_mode,
54
+ unless disable_trace.nil?
55
+ warn "[DEPRECATED] Bootsnap's `disable_trace:` option is deprecated and will be removed. " \
56
+ "If you use Ruby 2.5 or newer this option is useless, if not upgrading is recommended."
57
+ end
58
+
59
+ if compile_cache_iseq && !iseq_cache_supported?
60
+ warn "Ruby 2.5 has a bug that break code tracing when code is loaded from cache. It is recommened " \
61
+ "to turn `compile_cache_iseq` off on Ruby 2.5"
62
+ end
63
+
64
+ if load_path_cache
65
+ Bootsnap::LoadPathCache.setup(
66
+ cache_path: cache_dir + "/bootsnap/load-path-cache",
67
+ development_mode: development_mode,
68
+ )
69
+ end
70
+
71
+ Bootsnap::CompileCache.setup(
72
+ cache_dir: cache_dir + "/bootsnap/compile-cache",
73
+ iseq: compile_cache_iseq,
74
+ yaml: compile_cache_yaml,
75
+ json: compile_cache_json,
69
76
  )
70
77
  end
71
78
 
72
- Bootsnap::CompileCache.setup(
73
- cache_dir: cache_dir + "/bootsnap/compile-cache",
74
- iseq: compile_cache_iseq,
75
- yaml: compile_cache_yaml,
76
- json: compile_cache_json,
77
- )
78
- end
79
+ def iseq_cache_supported?
80
+ return @iseq_cache_supported if defined? @iseq_cache_supported
79
81
 
80
- def self.iseq_cache_supported?
81
- return @iseq_cache_supported if defined? @iseq_cache_supported
82
+ ruby_version = Gem::Version.new(RUBY_VERSION)
83
+ @iseq_cache_supported = ruby_version < Gem::Version.new("2.5.0") || ruby_version >= Gem::Version.new("2.6.0")
84
+ end
82
85
 
83
- ruby_version = Gem::Version.new(RUBY_VERSION)
84
- @iseq_cache_supported = ruby_version < Gem::Version.new("2.5.0") || ruby_version >= Gem::Version.new("2.6.0")
85
- end
86
+ def default_setup
87
+ env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || ENV["ENV"]
88
+ development_mode = ["", nil, "development"].include?(env)
86
89
 
87
- def self.default_setup
88
- env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || ENV["ENV"]
89
- development_mode = ["", nil, "development"].include?(env)
90
+ unless ENV["DISABLE_BOOTSNAP"]
91
+ cache_dir = ENV["BOOTSNAP_CACHE_DIR"]
92
+ unless cache_dir
93
+ config_dir_frame = caller.detect do |line|
94
+ line.include?("/config/")
95
+ end
90
96
 
91
- unless ENV["DISABLE_BOOTSNAP"]
92
- cache_dir = ENV["BOOTSNAP_CACHE_DIR"]
93
- unless cache_dir
94
- config_dir_frame = caller.detect do |line|
95
- line.include?("/config/")
96
- end
97
+ unless config_dir_frame
98
+ $stderr.puts("[bootsnap/setup] couldn't infer cache directory! Either:")
99
+ $stderr.puts("[bootsnap/setup] 1. require bootsnap/setup from your application's config directory; or")
100
+ $stderr.puts("[bootsnap/setup] 2. Define the environment variable BOOTSNAP_CACHE_DIR")
97
101
 
98
- unless config_dir_frame
99
- $stderr.puts("[bootsnap/setup] couldn't infer cache directory! Either:")
100
- $stderr.puts("[bootsnap/setup] 1. require bootsnap/setup from your application's config directory; or")
101
- $stderr.puts("[bootsnap/setup] 2. Define the environment variable BOOTSNAP_CACHE_DIR")
102
+ raise("couldn't infer bootsnap cache directory")
103
+ end
102
104
 
103
- raise("couldn't infer bootsnap cache directory")
104
- end
105
+ path = config_dir_frame.split(/:\d+:/).first
106
+ path = File.dirname(path) until File.basename(path) == "config"
107
+ app_root = File.dirname(path)
105
108
 
106
- path = config_dir_frame.split(/:\d+:/).first
107
- path = File.dirname(path) until File.basename(path) == "config"
108
- app_root = File.dirname(path)
109
+ cache_dir = File.join(app_root, "tmp", "cache")
110
+ end
109
111
 
110
- cache_dir = File.join(app_root, "tmp", "cache")
112
+ setup(
113
+ cache_dir: cache_dir,
114
+ development_mode: development_mode,
115
+ load_path_cache: !ENV["DISABLE_BOOTSNAP_LOAD_PATH_CACHE"],
116
+ compile_cache_iseq: !ENV["DISABLE_BOOTSNAP_COMPILE_CACHE"] && iseq_cache_supported?,
117
+ compile_cache_yaml: !ENV["DISABLE_BOOTSNAP_COMPILE_CACHE"],
118
+ compile_cache_json: !ENV["DISABLE_BOOTSNAP_COMPILE_CACHE"],
119
+ )
120
+
121
+ if ENV["BOOTSNAP_LOG"]
122
+ log!
123
+ end
111
124
  end
125
+ end
112
126
 
113
- setup(
114
- cache_dir: cache_dir,
115
- development_mode: development_mode,
116
- load_path_cache: !ENV["DISABLE_BOOTSNAP_LOAD_PATH_CACHE"],
117
- compile_cache_iseq: !ENV["DISABLE_BOOTSNAP_COMPILE_CACHE"] && iseq_cache_supported?,
118
- compile_cache_yaml: !ENV["DISABLE_BOOTSNAP_COMPILE_CACHE"],
119
- compile_cache_json: !ENV["DISABLE_BOOTSNAP_COMPILE_CACHE"],
120
- )
121
-
122
- if ENV["BOOTSNAP_LOG"]
123
- log!
127
+ if RbConfig::CONFIG["host_os"] =~ /mswin|mingw|cygwin/
128
+ def absolute_path?(path)
129
+ path[1] == ":"
130
+ end
131
+ else
132
+ def absolute_path?(path)
133
+ path.start_with?("/")
124
134
  end
125
135
  end
126
- end
127
136
 
128
- if RbConfig::CONFIG["host_os"] =~ /mswin|mingw|cygwin/
129
- def self.absolute_path?(path)
130
- path[1] == ":"
131
- end
132
- else
133
- def self.absolute_path?(path)
134
- path.start_with?("/")
137
+ # This is a semi-accurate ruby implementation of the native `rb_get_path(VALUE)` function.
138
+ # The native version is very intricate and may behave differently on windows etc.
139
+ # But we only use it for non-MRI platform.
140
+ def rb_get_path(fname)
141
+ path_path = fname.respond_to?(:to_path) ? fname.to_path : fname
142
+ String.try_convert(path_path) || raise(TypeError, "no implicit conversion of #{path_path.class} into String")
135
143
  end
144
+
145
+ # Allow the C extension to redefine `rb_get_path` without warning.
146
+ alias_method :rb_get_path, :rb_get_path # rubocop:disable Lint/DuplicateMethods
136
147
  end
137
148
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bootsnap
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.10.1
4
+ version: 1.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Burke Libbey
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-01-17 00:00:00.000000000 Z
11
+ date: 2022-03-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: msgpack
@@ -57,7 +57,6 @@ files:
57
57
  - lib/bootsnap/load_path_cache/loaded_features_index.rb
58
58
  - lib/bootsnap/load_path_cache/path.rb
59
59
  - lib/bootsnap/load_path_cache/path_scanner.rb
60
- - lib/bootsnap/load_path_cache/realpath_cache.rb
61
60
  - lib/bootsnap/load_path_cache/store.rb
62
61
  - lib/bootsnap/setup.rb
63
62
  - lib/bootsnap/version.rb
@@ -77,7 +76,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
77
76
  requirements:
78
77
  - - ">="
79
78
  - !ruby/object:Gem::Version
80
- version: 2.3.0
79
+ version: 2.4.0
81
80
  required_rubygems_version: !ruby/object:Gem::Requirement
82
81
  requirements:
83
82
  - - ">="
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Bootsnap
4
- module LoadPathCache
5
- class RealpathCache
6
- def initialize
7
- @cache = Hash.new { |h, k| h[k] = realpath(*k) }
8
- end
9
-
10
- def call(*key)
11
- @cache[key]
12
- end
13
-
14
- private
15
-
16
- def realpath(caller_location, path)
17
- base = File.dirname(caller_location)
18
- abspath = File.expand_path(path, base).freeze
19
- find_file(abspath)
20
- end
21
-
22
- def find_file(name)
23
- return File.realpath(name).freeze if File.exist?(name)
24
-
25
- CACHED_EXTENSIONS.each do |ext|
26
- filename = "#{name}#{ext}"
27
- return File.realpath(filename).freeze if File.exist?(filename)
28
- end
29
- name
30
- end
31
- end
32
- end
33
- end