bootsnap 1.9.4 → 1.12.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,90 +1,44 @@
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)
9
+ string_path = Bootsnap.rb_get_path(path)
10
+ return false if Bootsnap::LoadPathCache.loaded_features_index.key?(string_path)
11
+
12
+ resolved = Bootsnap::LoadPathCache.load_path_cache.find(string_path)
13
+ if Bootsnap::LoadPathCache::FALLBACK_SCAN.equal?(resolved)
14
+ if (cursor = Bootsnap::LoadPathCache.loaded_features_index.cursor(string_path))
15
+ ret = require_without_bootsnap(path)
16
+ resolved = Bootsnap::LoadPathCache.loaded_features_index.identify(string_path, cursor)
17
+ Bootsnap::LoadPathCache.loaded_features_index.register(string_path, resolved)
18
+ return ret
19
+ else
20
+ return require_without_bootsnap(path)
21
+ end
22
+ elsif false == resolved
23
+ return false
24
+ elsif resolved.nil?
25
+ error = LoadError.new(+"cannot load such file -- #{path}")
26
+ error.instance_variable_set(:@path, path)
27
+ raise error
28
+ else
29
+ # Note that require registers to $LOADED_FEATURES while load does not.
30
+ ret = require_without_bootsnap(resolved)
31
+ Bootsnap::LoadPathCache.loaded_features_index.register(string_path, resolved)
32
+ return ret
45
33
  end
46
34
  end
47
35
 
48
- alias_method(:require_relative_without_bootsnap, :require_relative)
49
- def require_relative(path)
50
- location = caller_locations(1..1).first
51
- realpath = Bootsnap::LoadPathCache.realpath_cache.call(
52
- location.absolute_path || location.path, path
53
- )
54
- require(realpath)
55
- end
56
-
57
36
  alias_method(:load_without_bootsnap, :load)
58
37
  def load(path, wrap = false)
59
- if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path, try_extensions: false))
38
+ if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(Bootsnap.rb_get_path(path), try_extensions: false))
60
39
  load_without_bootsnap(resolved, wrap)
61
40
  else
62
41
  load_without_bootsnap(path, wrap)
63
42
  end
64
43
  end
65
44
  end
66
-
67
- class Module
68
- alias_method(:autoload_without_bootsnap, :autoload)
69
- def autoload(const, path)
70
- # NOTE: This may defeat LoadedFeaturesIndex, but it's not immediately
71
- # obvious how to make it work. This feels like a pretty niche case, unclear
72
- # if it will ever burn anyone.
73
- #
74
- # The challenge is that we don't control the point at which the entry gets
75
- # added to $LOADED_FEATURES and won't be able to hook that modification
76
- # since it's done in C-land.
77
- autoload_without_bootsnap(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
78
- rescue LoadError => e
79
- e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
80
- raise(e)
81
- rescue Bootsnap::LoadPathCache::ReturnFalse
82
- false
83
- rescue Bootsnap::LoadPathCache::FallbackScan
84
- fallback = true
85
- ensure
86
- if fallback
87
- autoload_without_bootsnap(const, path)
88
- end
89
- end
90
- end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  class << $LOADED_FEATURES
3
4
  alias_method(:delete_without_bootsnap, :delete)
4
5
  def delete(key)
@@ -29,14 +29,15 @@ module Bootsnap
29
29
  @mutex = Mutex.new
30
30
 
31
31
  # In theory the user could mutate $LOADED_FEATURES and invalidate our
32
- # cache. If this ever comes up in practice or if you, the
33
- # enterprising reader, feels inclined to solve this problem we could
32
+ # cache. If this ever comes up in practice - or if you, the
33
+ # enterprising reader, feels inclined to solve this problem - we could
34
34
  # parallel the work done with ChangeObserver on $LOAD_PATH to mirror
35
35
  # updates to our @lfi.
36
36
  $LOADED_FEATURES.each do |feat|
37
37
  hash = feat.hash
38
38
  $LOAD_PATH.each do |lpe|
39
39
  next unless feat.start_with?(lpe)
40
+
40
41
  # /a/b/lib/my/foo.rb
41
42
  # ^^^^^^^^^
42
43
  short = feat[(lpe.length + 1)..-1]
@@ -68,11 +69,30 @@ module Bootsnap
68
69
  @mutex.synchronize { @lfi.key?(feature) }
69
70
  end
70
71
 
72
+ def cursor(short)
73
+ unless Bootsnap.absolute_path?(short.to_s)
74
+ $LOADED_FEATURES.size
75
+ end
76
+ end
77
+
78
+ def identify(short, cursor)
79
+ $LOADED_FEATURES[cursor..-1].detect do |feat|
80
+ offset = 0
81
+ while (offset = feat.index(short, offset))
82
+ if feat.index(".", offset + 1) && !feat.index("/", offset + 2)
83
+ break true
84
+ else
85
+ offset += 1
86
+ end
87
+ end
88
+ end
89
+ end
90
+
71
91
  # There is a relatively uncommon case where we could miss adding an
72
92
  # entry:
73
93
  #
74
94
  # If the user asked for e.g. `require 'bundler'`, and we went through the
75
- # `FallbackScan` pathway in `kernel_require.rb` and therefore did not
95
+ # `FALLBACK_SCAN` pathway in `kernel_require.rb` and therefore did not
76
96
  # pass `long` (the full expanded absolute path), then we did are not able
77
97
  # to confidently add the `bundler.rb` form to @lfi.
78
98
  #
@@ -82,28 +102,8 @@ module Bootsnap
82
102
  # not quite right; or
83
103
  # 2. Inspect $LOADED_FEATURES upon return from yield to find the matching
84
104
  # entry.
85
- def register(short, long = nil)
86
- # Absolute paths are not a concern.
87
- if Bootsnap.absolute_path?(short.to_s)
88
- return yield
89
- end
90
-
91
- if long.nil?
92
- len = $LOADED_FEATURES.size
93
- ret = yield
94
- long = $LOADED_FEATURES[len..-1].detect do |feat|
95
- offset = 0
96
- while offset = feat.index(short, offset)
97
- if feat.index(".", offset + 1) && !feat.index("/", offset + 2)
98
- break true
99
- else
100
- offset += 1
101
- end
102
- end
103
- end
104
- else
105
- ret = yield
106
- end
105
+ def register(short, long)
106
+ return if Bootsnap.absolute_path?(short)
107
107
 
108
108
  hash = long.hash
109
109
 
@@ -122,13 +122,11 @@ module Bootsnap
122
122
  @lfi[short] = hash
123
123
  (@lfi[altname] = hash) if altname
124
124
  end
125
-
126
- ret
127
125
  end
128
126
 
129
127
  private
130
128
 
131
- STRIP_EXTENSION = /\.[^.]*?$/
129
+ STRIP_EXTENSION = /\.[^.]*?$/.freeze
132
130
  private_constant(:STRIP_EXTENSION)
133
131
 
134
132
  # Might Ruby automatically search for this extension if
@@ -145,15 +143,15 @@ module Bootsnap
145
143
  # with calling a Ruby file 'x.dylib.rb' and then requiring it as 'x.dylib'.)
146
144
  #
147
145
  # See <https://ruby-doc.org/core-2.6.4/Kernel.html#method-i-require>.
148
- def extension_elidable?(f)
149
- f.to_s.end_with?('.rb', '.so', '.o', '.dll', '.dylib')
146
+ def extension_elidable?(feature)
147
+ feature.to_s.end_with?(".rb", ".so", ".o", ".dll", ".dylib")
150
148
  end
151
149
 
152
- def strip_extension_if_elidable(f)
153
- if extension_elidable?(f)
154
- f.sub(STRIP_EXTENSION, '')
150
+ def strip_extension_if_elidable(feature)
151
+ if extension_elidable?(feature)
152
+ feature.sub(STRIP_EXTENSION, "")
155
153
  else
156
- f
154
+ feature
157
155
  end
158
156
  end
159
157
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
- require_relative('path_scanner')
2
+
3
+ require_relative("path_scanner")
3
4
 
4
5
  module Bootsnap
5
6
  module LoadPathCache
@@ -20,8 +21,26 @@ module Bootsnap
20
21
 
21
22
  attr_reader(:path)
22
23
 
23
- def initialize(path)
24
+ def initialize(path, real: false)
24
25
  @path = path.to_s.freeze
26
+ @real = real
27
+ end
28
+
29
+ def to_realpath
30
+ return self if @real
31
+
32
+ realpath = begin
33
+ File.realpath(path)
34
+ rescue Errno::ENOENT
35
+ return self
36
+ end
37
+
38
+ if realpath != path
39
+ Path.new(realpath, real: true)
40
+ else
41
+ @real = true
42
+ self
43
+ end
25
44
  end
26
45
 
27
46
  # True if the path exists, but represents a non-directory object
@@ -43,6 +62,7 @@ module Bootsnap
43
62
  # set to zero anyway, just in case we change the stability heuristics.
44
63
  _, entries, dirs = store.get(expanded_path)
45
64
  return [entries, dirs] if entries # cache hit
65
+
46
66
  entries, dirs = scan!
47
67
  store.set(expanded_path, [0, entries, dirs])
48
68
  return [entries, dirs]
@@ -60,7 +80,11 @@ module Bootsnap
60
80
  end
61
81
 
62
82
  def expanded_path
63
- File.expand_path(path).freeze
83
+ if @real
84
+ path
85
+ else
86
+ @expanded_path ||= File.expand_path(path).freeze
87
+ end
64
88
  end
65
89
 
66
90
  private
@@ -93,8 +117,8 @@ module Bootsnap
93
117
 
94
118
  # Built-in ruby lib stuff doesn't change, but things can occasionally be
95
119
  # installed into sitedir, which generally lives under libdir.
96
- RUBY_LIBDIR = RbConfig::CONFIG['libdir']
97
- RUBY_SITEDIR = RbConfig::CONFIG['sitedir']
120
+ RUBY_LIBDIR = RbConfig::CONFIG["libdir"]
121
+ RUBY_SITEDIR = RbConfig::CONFIG["sitedir"]
98
122
 
99
123
  def stability
100
124
  @stability ||= begin
@@ -1,18 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative('../explicit_require')
3
+ require_relative("../explicit_require")
4
4
 
5
5
  module Bootsnap
6
6
  module LoadPathCache
7
7
  module PathScanner
8
8
  REQUIRABLE_EXTENSIONS = [DOT_RB] + DL_EXTENSIONS
9
9
  NORMALIZE_NATIVE_EXTENSIONS = !DL_EXTENSIONS.include?(LoadPathCache::DOT_SO)
10
- ALTERNATIVE_NATIVE_EXTENSIONS_PATTERN = /\.(o|bundle|dylib)\z/
10
+ ALTERNATIVE_NATIVE_EXTENSIONS_PATTERN = /\.(o|bundle|dylib)\z/.freeze
11
11
 
12
12
  BUNDLE_PATH = if Bootsnap.bundler?
13
13
  (Bundler.bundle_path.cleanpath.to_s << LoadPathCache::SLASH).freeze
14
14
  else
15
- ''
15
+ ""
16
16
  end
17
17
 
18
18
  class << self
@@ -44,7 +44,8 @@ module Bootsnap
44
44
 
45
45
  def walk(absolute_dir_path, relative_dir_path, &block)
46
46
  Dir.foreach(absolute_dir_path) do |name|
47
- next if name.start_with?('.')
47
+ next if name.start_with?(".")
48
+
48
49
  relative_path = relative_dir_path ? File.join(relative_dir_path, name) : name
49
50
 
50
51
  absolute_path = "#{absolute_dir_path}/#{name}"
@@ -58,7 +59,7 @@ module Bootsnap
58
59
  end
59
60
  end
60
61
 
61
- if RUBY_VERSION >= '3.1'
62
+ if RUBY_VERSION >= "3.1"
62
63
  def os_path(path)
63
64
  path.freeze
64
65
  end
@@ -1,14 +1,14 @@
1
1
  # frozen_string_literal: true
2
- require_relative('../explicit_require')
3
2
 
4
- Bootsnap::ExplicitRequire.with_gems('msgpack') { require('msgpack') }
5
- Bootsnap::ExplicitRequire.from_rubylibdir('fileutils')
3
+ require_relative("../explicit_require")
4
+
5
+ Bootsnap::ExplicitRequire.with_gems("msgpack") { require("msgpack") }
6
6
 
7
7
  module Bootsnap
8
8
  module LoadPathCache
9
9
  class Store
10
- VERSION_KEY = '__bootsnap_ruby_version__'
11
- CURRENT_VERSION = "#{RUBY_REVISION}-#{RUBY_PLATFORM}".freeze
10
+ VERSION_KEY = "__bootsnap_ruby_version__"
11
+ CURRENT_VERSION = "#{RUBY_REVISION}-#{RUBY_PLATFORM}".freeze # rubocop:disable Style/RedundantFreeze
12
12
 
13
13
  NestedTransactionError = Class.new(StandardError)
14
14
  SetOutsideTransactionNotAllowed = Class.new(StandardError)
@@ -26,10 +26,11 @@ module Bootsnap
26
26
 
27
27
  def fetch(key)
28
28
  raise(SetOutsideTransactionNotAllowed) unless @txn_mutex.owned?
29
+
29
30
  v = get(key)
30
31
  unless v
31
- @dirty = true
32
32
  v = yield
33
+ mark_for_mutation!
33
34
  @data[key] = v
34
35
  end
35
36
  v
@@ -37,14 +38,16 @@ module Bootsnap
37
38
 
38
39
  def set(key, value)
39
40
  raise(SetOutsideTransactionNotAllowed) unless @txn_mutex.owned?
41
+
40
42
  if value != @data[key]
41
- @dirty = true
43
+ mark_for_mutation!
42
44
  @data[key] = value
43
45
  end
44
46
  end
45
47
 
46
48
  def transaction
47
49
  raise(NestedTransactionError) if @txn_mutex.owned?
50
+
48
51
  @txn_mutex.synchronize do
49
52
  begin
50
53
  yield
@@ -56,6 +59,11 @@ module Bootsnap
56
59
 
57
60
  private
58
61
 
62
+ def mark_for_mutation!
63
+ @dirty = true
64
+ @data = @data.dup if @data.frozen?
65
+ end
66
+
59
67
  def commit_transaction
60
68
  if @dirty
61
69
  dump_data
@@ -66,7 +74,7 @@ module Bootsnap
66
74
  def load_data
67
75
  @data = begin
68
76
  data = File.open(@store_path, encoding: Encoding::BINARY) do |io|
69
- MessagePack.load(io)
77
+ MessagePack.load(io, freeze: true)
70
78
  end
71
79
  if data.is_a?(Hash) && data[VERSION_KEY] == CURRENT_VERSION
72
80
  data
@@ -88,22 +96,37 @@ module Bootsnap
88
96
  def dump_data
89
97
  # Change contents atomically so other processes can't get invalid
90
98
  # caches if they read at an inopportune time.
91
- tmp = "#{@store_path}.#{Process.pid}.#{(rand * 100000).to_i}.tmp"
92
- FileUtils.mkpath(File.dirname(tmp))
99
+ tmp = "#{@store_path}.#{Process.pid}.#{(rand * 100_000).to_i}.tmp"
100
+ mkdir_p(File.dirname(tmp))
93
101
  exclusive_write = File::Constants::CREAT | File::Constants::EXCL | File::Constants::WRONLY
94
102
  # `encoding:` looks redundant wrt `binwrite`, but necessary on windows
95
103
  # because binary is part of mode.
96
104
  File.open(tmp, mode: exclusive_write, encoding: Encoding::BINARY) do |io|
97
- MessagePack.dump(@data, io, freeze: true)
105
+ MessagePack.dump(@data, io)
98
106
  end
99
- FileUtils.mv(tmp, @store_path)
107
+ File.rename(tmp, @store_path)
100
108
  rescue Errno::EEXIST
101
109
  retry
102
110
  rescue SystemCallError
103
111
  end
104
112
 
105
113
  def default_data
106
- { VERSION_KEY => CURRENT_VERSION }
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
107
130
  end
108
131
  end
109
132
  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.9.4"
4
+ VERSION = "1.12.0"
4
5
  end