bootsnap 1.1.8 → 1.4.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.
@@ -10,6 +10,14 @@ module Bootsnap
10
10
  Thread.current[:without_bootsnap_cache] = prev
11
11
  end
12
12
 
13
+ def self.allow_bootsnap_retry(allowed)
14
+ prev = Thread.current[:without_bootsnap_retry] || false
15
+ Thread.current[:without_bootsnap_retry] = !allowed
16
+ yield
17
+ ensure
18
+ Thread.current[:without_bootsnap_retry] = prev
19
+ end
20
+
13
21
  module ClassMethods
14
22
  def autoload_paths=(o)
15
23
  super
@@ -22,17 +30,25 @@ module Bootsnap
22
30
  Bootsnap::LoadPathCache.autoload_paths_cache.find(path)
23
31
  rescue Bootsnap::LoadPathCache::ReturnFalse
24
32
  nil # doesn't really apply here
33
+ rescue Bootsnap::LoadPathCache::FallbackScan
34
+ nil # doesn't really apply here
25
35
  end
26
36
  end
27
37
 
28
38
  def autoloadable_module?(path_suffix)
29
- Bootsnap::LoadPathCache.autoload_paths_cache.has_dir?(path_suffix)
39
+ Bootsnap::LoadPathCache.autoload_paths_cache.load_dir(path_suffix)
30
40
  end
31
41
 
32
42
  def remove_constant(const)
33
43
  CoreExt::ActiveSupport.without_bootsnap_cache { super }
34
44
  end
35
45
 
46
+ def require_or_load(*)
47
+ CoreExt::ActiveSupport.allow_bootsnap_retry(true) do
48
+ super
49
+ end
50
+ end
51
+
36
52
  # If we can't find a constant using the patched implementation of
37
53
  # search_for_file, try again with the default implementation.
38
54
  #
@@ -40,8 +56,15 @@ module Bootsnap
40
56
  # behaviour. The gymnastics here are a bit awkward, but it prevents
41
57
  # 200+ lines of monkeypatches.
42
58
  def load_missing_constant(from_mod, const_name)
43
- super
59
+ CoreExt::ActiveSupport.allow_bootsnap_retry(false) do
60
+ super
61
+ end
44
62
  rescue NameError => e
63
+ # This function can end up called recursively, we only want to
64
+ # retry at the top-level.
65
+ raise if Thread.current[:without_bootsnap_retry]
66
+ # If we already had cache disabled, there's no use retrying
67
+ raise if Thread.current[:without_bootsnap_cache]
45
68
  # NoMethodError is a NameError, but we only want to handle actual
46
69
  # NameError instances.
47
70
  raise unless e.class == NameError
@@ -58,6 +81,8 @@ module Bootsnap
58
81
  def depend_on(*)
59
82
  super
60
83
  rescue LoadError
84
+ # If we already had cache disabled, there's no use retrying
85
+ raise if Thread.current[:without_bootsnap_cache]
61
86
  CoreExt::ActiveSupport.without_bootsnap_cache { super }
62
87
  end
63
88
  end
@@ -69,7 +94,7 @@ end
69
94
  module ActiveSupport
70
95
  module Dependencies
71
96
  class << self
72
- prepend Bootsnap::LoadPathCache::CoreExt::ActiveSupport::ClassMethods
97
+ prepend(Bootsnap::LoadPathCache::CoreExt::ActiveSupport::ClassMethods)
73
98
  end
74
99
  end
75
100
  end
@@ -11,78 +11,72 @@ module Bootsnap
11
11
  end
12
12
 
13
13
  module Kernel
14
- alias_method :require_without_cache, :require
15
- def require(path)
16
- if resolved = Bootsnap::LoadPathCache.load_path_cache.find(path)
17
- require_without_cache(resolved)
18
- else
19
- raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
20
- end
21
- rescue Bootsnap::LoadPathCache::ReturnFalse
22
- return false
23
- rescue Bootsnap::LoadPathCache::FallbackScan
24
- require_without_cache(path)
25
- end
14
+ module_function # rubocop:disable Style/ModuleFunction
26
15
 
27
- alias_method :load_without_cache, :load
28
- def load(path, wrap = false)
29
- if resolved = Bootsnap::LoadPathCache.load_path_cache.find(path)
30
- load_without_cache(resolved, wrap)
31
- else
32
- # load also allows relative paths from pwd even when not in $:
33
- relative = File.expand_path(path)
34
- if File.exist?(File.expand_path(path))
35
- return load_without_cache(relative, wrap)
36
- end
37
- raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
16
+ alias_method(:require_without_bootsnap, :require)
17
+
18
+ # Note that require registers to $LOADED_FEATURES while load does not.
19
+ def require_with_bootsnap_lfi(path, resolved = nil)
20
+ Bootsnap::LoadPathCache.loaded_features_index.register(path, resolved) do
21
+ require_without_bootsnap(resolved || path)
38
22
  end
39
- rescue Bootsnap::LoadPathCache::ReturnFalse
40
- return false
41
- rescue Bootsnap::LoadPathCache::FallbackScan
42
- load_without_cache(path, wrap)
43
23
  end
44
- end
45
24
 
46
- class << Kernel
47
- alias_method :require_without_cache, :require
48
25
  def require(path)
49
- if resolved = Bootsnap::LoadPathCache.load_path_cache.find(path)
50
- require_without_cache(resolved)
51
- else
52
- raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
26
+ return false if Bootsnap::LoadPathCache.loaded_features_index.key?(path)
27
+
28
+ if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path))
29
+ return require_with_bootsnap_lfi(path, resolved)
53
30
  end
31
+
32
+ raise(Bootsnap::LoadPathCache::CoreExt.make_load_error(path))
54
33
  rescue Bootsnap::LoadPathCache::ReturnFalse
55
- return false
34
+ false
56
35
  rescue Bootsnap::LoadPathCache::FallbackScan
57
- require_without_cache(path)
36
+ require_with_bootsnap_lfi(path)
37
+ end
38
+
39
+ alias_method(:require_relative_without_bootsnap, :require_relative)
40
+ def require_relative(path)
41
+ realpath = Bootsnap::LoadPathCache.realpath_cache.call(
42
+ caller_locations(1..1).first.absolute_path, path
43
+ )
44
+ require(realpath)
58
45
  end
59
46
 
60
- alias_method :load_without_cache, :load
47
+ alias_method(:load_without_bootsnap, :load)
61
48
  def load(path, wrap = false)
62
- if resolved = Bootsnap::LoadPathCache.load_path_cache.find(path)
63
- load_without_cache(resolved, wrap)
64
- else
65
- # load also allows relative paths from pwd even when not in $:
66
- relative = File.expand_path(path)
67
- if File.exist?(relative)
68
- return load_without_cache(relative, wrap)
69
- end
70
- raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
49
+ if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path))
50
+ return load_without_bootsnap(resolved, wrap)
51
+ end
52
+
53
+ # load also allows relative paths from pwd even when not in $:
54
+ if File.exist?(relative = File.expand_path(path))
55
+ return load_without_bootsnap(relative, wrap)
71
56
  end
57
+
58
+ raise(Bootsnap::LoadPathCache::CoreExt.make_load_error(path))
72
59
  rescue Bootsnap::LoadPathCache::ReturnFalse
73
- return false
60
+ false
74
61
  rescue Bootsnap::LoadPathCache::FallbackScan
75
- load_without_cache(path, wrap)
62
+ load_without_bootsnap(path, wrap)
76
63
  end
77
64
  end
78
65
 
79
66
  class Module
80
- alias_method :autoload_without_cache, :autoload
67
+ alias_method(:autoload_without_bootsnap, :autoload)
81
68
  def autoload(const, path)
82
- autoload_without_cache(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
69
+ # NOTE: This may defeat LoadedFeaturesIndex, but it's not immediately
70
+ # obvious how to make it work. This feels like a pretty niche case, unclear
71
+ # if it will ever burn anyone.
72
+ #
73
+ # The challenge is that we don't control the point at which the entry gets
74
+ # added to $LOADED_FEATURES and won't be able to hook that modification
75
+ # since it's done in C-land.
76
+ autoload_without_bootsnap(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
83
77
  rescue Bootsnap::LoadPathCache::ReturnFalse
84
- return false
78
+ false
85
79
  rescue Bootsnap::LoadPathCache::FallbackScan
86
- autoload_without_cache(const, path)
80
+ autoload_without_bootsnap(const, path)
87
81
  end
88
82
  end
@@ -0,0 +1,7 @@
1
+ class << $LOADED_FEATURES
2
+ alias_method(:delete_without_bootsnap, :delete)
3
+ def delete(key)
4
+ Bootsnap::LoadPathCache.loaded_features_index.purge(key)
5
+ delete_without_bootsnap(key)
6
+ end
7
+ end
@@ -0,0 +1,116 @@
1
+ module Bootsnap
2
+ module LoadPathCache
3
+ # LoadedFeaturesIndex partially mirrors an internal structure in ruby that
4
+ # we can't easily obtain an interface to.
5
+ #
6
+ # This works around an issue where, without bootsnap, *ruby* knows that it
7
+ # has already required a file by its short name (e.g. require 'bundler') if
8
+ # a new instance of bundler is added to the $LOAD_PATH which resolves to a
9
+ # different absolute path. This class makes bootsnap smart enough to
10
+ # realize that it has already loaded 'bundler', and not just
11
+ # '/path/to/bundler'.
12
+ #
13
+ # If you disable LoadedFeaturesIndex, you can see the problem this solves by:
14
+ #
15
+ # 1. `require 'a'`
16
+ # 2. Prepend a new $LOAD_PATH element containing an `a.rb`
17
+ # 3. `require 'a'`
18
+ #
19
+ # Ruby returns false from step 3.
20
+ # With bootsnap but with no LoadedFeaturesIndex, this loads two different
21
+ # `a.rb`s.
22
+ # With bootsnap and with LoadedFeaturesIndex, this skips the second load,
23
+ # returning false like ruby.
24
+ class LoadedFeaturesIndex
25
+ def initialize
26
+ @lfi = {}
27
+ @mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new # TODO: Remove once Ruby 2.2 support is dropped.
28
+
29
+ # In theory the user could mutate $LOADED_FEATURES and invalidate our
30
+ # cache. If this ever comes up in practice — or if you, the
31
+ # enterprising reader, feels inclined to solve this problem — we could
32
+ # parallel the work done with ChangeObserver on $LOAD_PATH to mirror
33
+ # updates to our @lfi.
34
+ $LOADED_FEATURES.each do |feat|
35
+ hash = feat.hash
36
+ $LOAD_PATH.each do |lpe|
37
+ next unless feat.start_with?(lpe)
38
+ # /a/b/lib/my/foo.rb
39
+ # ^^^^^^^^^
40
+ short = feat[(lpe.length + 1)..-1]
41
+ stripped = strip_extension(short)
42
+ @lfi[short] = hash
43
+ @lfi[stripped] = hash
44
+ end
45
+ end
46
+ end
47
+
48
+ # We've optimized for initialize and register to be fast, and purge to be tolerable.
49
+ # If access patterns make this not-okay, we can lazy-invert the LFI on
50
+ # first purge and work from there.
51
+ def purge(feature)
52
+ @mutex.synchronize do
53
+ feat_hash = feature.hash
54
+ @lfi.reject! { |_, hash| hash == feat_hash }
55
+ end
56
+ end
57
+
58
+ def key?(feature)
59
+ @mutex.synchronize { @lfi.key?(feature) }
60
+ end
61
+
62
+ # There is a relatively uncommon case where we could miss adding an
63
+ # entry:
64
+ #
65
+ # If the user asked for e.g. `require 'bundler'`, and we went through the
66
+ # `FallbackScan` pathway in `kernel_require.rb` and therefore did not
67
+ # pass `long` (the full expanded absolute path), then we did are not able
68
+ # to confidently add the `bundler.rb` form to @lfi.
69
+ #
70
+ # We could either:
71
+ #
72
+ # 1. Just add `bundler.rb`, `bundler.so`, and so on, which is close but
73
+ # not quite right; or
74
+ # 2. Inspect $LOADED_FEATURES upon return from yield to find the matching
75
+ # entry.
76
+ def register(short, long = nil)
77
+ if long.nil?
78
+ pat = %r{/#{Regexp.escape(short)}(\.[^/]+)?$}
79
+ len = $LOADED_FEATURES.size
80
+ ret = yield
81
+ long = $LOADED_FEATURES[len..-1].detect { |feat| feat =~ pat }
82
+ else
83
+ ret = yield
84
+ end
85
+
86
+ hash = long.hash
87
+
88
+ # do we have 'bundler' or 'bundler.rb'?
89
+ altname = if File.extname(short) != ''
90
+ # strip the path from 'bundler.rb' -> 'bundler'
91
+ strip_extension(short)
92
+ elsif long && (ext = File.extname(long))
93
+ # get the extension from the expanded path if given
94
+ # 'bundler' + '.rb'
95
+ short + ext
96
+ end
97
+
98
+ @mutex.synchronize do
99
+ @lfi[short] = hash
100
+ (@lfi[altname] = hash) if altname
101
+ end
102
+
103
+ ret
104
+ end
105
+
106
+ private
107
+
108
+ STRIP_EXTENSION = /\.[^.]*?$/
109
+ private_constant(:STRIP_EXTENSION)
110
+
111
+ def strip_extension(f)
112
+ f.sub(STRIP_EXTENSION, '')
113
+ end
114
+ end
115
+ end
116
+ end
@@ -1,4 +1,4 @@
1
- require_relative 'path_scanner'
1
+ require_relative('path_scanner')
2
2
 
3
3
  module Bootsnap
4
4
  module LoadPathCache
@@ -17,7 +17,7 @@ module Bootsnap
17
17
  stability == VOLATILE
18
18
  end
19
19
 
20
- attr_reader :path
20
+ attr_reader(:path)
21
21
 
22
22
  def initialize(path)
23
23
  @path = path.to_s
@@ -26,7 +26,7 @@ module Bootsnap
26
26
  # True if the path exists, but represents a non-directory object
27
27
  def non_directory?
28
28
  !File.stat(path).directory?
29
- rescue Errno::ENOENT
29
+ rescue Errno::ENOENT, Errno::ENOTDIR
30
30
  false
31
31
  end
32
32
 
@@ -76,8 +76,8 @@ module Bootsnap
76
76
  ["", *dirs].each do |dir|
77
77
  curr = begin
78
78
  File.mtime("#{path}/#{dir}").to_i
79
- rescue Errno::ENOENT
80
- -1
79
+ rescue Errno::ENOENT, Errno::ENOTDIR
80
+ -1
81
81
  end
82
82
  max = curr if curr > max
83
83
  end
@@ -1,22 +1,18 @@
1
- require_relative '../explicit_require'
1
+ require_relative('../explicit_require')
2
2
 
3
3
  module Bootsnap
4
4
  module LoadPathCache
5
5
  module PathScanner
6
- # Glob pattern to find requirable files and subdirectories in given path.
7
- # It expands to:
8
- #
9
- # * `/*{.rb,.so,/}` - It matches requirable files, directories and
10
- # symlinks to directories at given path.
11
- # * `/*/**/*{.rb,.so,/}` - It matches requirable files and
12
- # subdirectories in any (even symlinked) directory at given path at
13
- # any directory tree depth.
14
- #
15
- REQUIRABLES_AND_DIRS = "/{,*/**/}*{#{DOT_RB},#{DL_EXTENSIONS.join(',')},/}"
6
+ ALL_FILES = "/{,**/*/**/}*"
7
+ REQUIRABLE_EXTENSIONS = [DOT_RB] + DL_EXTENSIONS
16
8
  NORMALIZE_NATIVE_EXTENSIONS = !DL_EXTENSIONS.include?(LoadPathCache::DOT_SO)
17
9
  ALTERNATIVE_NATIVE_EXTENSIONS_PATTERN = /\.(o|bundle|dylib)\z/
18
- BUNDLE_PATH = Bootsnap.bundler? ?
19
- (Bundler.bundle_path.cleanpath.to_s << LoadPathCache::SLASH).freeze : ''.freeze
10
+
11
+ BUNDLE_PATH = if Bootsnap.bundler?
12
+ (Bundler.bundle_path.cleanpath.to_s << LoadPathCache::SLASH).freeze
13
+ else
14
+ ''.freeze
15
+ end
20
16
 
21
17
  def self.call(path)
22
18
  path = path.to_s
@@ -34,13 +30,13 @@ module Bootsnap
34
30
  dirs = []
35
31
  requirables = []
36
32
 
37
- Dir.glob(path + REQUIRABLES_AND_DIRS).each do |absolute_path|
33
+ Dir.glob(path + ALL_FILES).each do |absolute_path|
38
34
  next if contains_bundle_path && absolute_path.start_with?(BUNDLE_PATH)
39
- relative_path = absolute_path.slice!(relative_slice)
35
+ relative_path = absolute_path.slice(relative_slice)
40
36
 
41
- if relative_path.end_with?('/')
42
- dirs << relative_path[0..-2]
43
- else
37
+ if File.directory?(absolute_path)
38
+ dirs << relative_path
39
+ elsif REQUIRABLE_EXTENSIONS.include?(File.extname(relative_path))
44
40
  requirables << relative_path
45
41
  end
46
42
  end
@@ -0,0 +1,32 @@
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
+ file = find_file(File.expand_path(path, base))
19
+ dir = File.dirname(file)
20
+ File.join(dir, File.basename(file))
21
+ end
22
+
23
+ def find_file(name)
24
+ ['', *CACHED_EXTENSIONS].each do |ext|
25
+ filename = "#{name}#{ext}"
26
+ return File.realpath(filename) if File.exist?(filename)
27
+ end
28
+ name
29
+ end
30
+ end
31
+ end
32
+ end
@@ -1,6 +1,6 @@
1
- require_relative '../explicit_require'
1
+ require_relative('../explicit_require')
2
2
 
3
- Bootsnap::ExplicitRequire.with_gems('msgpack') { require 'msgpack' }
3
+ Bootsnap::ExplicitRequire.with_gems('msgpack') { require('msgpack') }
4
4
  Bootsnap::ExplicitRequire.from_rubylibdir('fileutils')
5
5
 
6
6
  module Bootsnap
@@ -21,7 +21,7 @@ module Bootsnap
21
21
  end
22
22
 
23
23
  def fetch(key)
24
- raise SetOutsideTransactionNotAllowed unless @in_txn
24
+ raise(SetOutsideTransactionNotAllowed) unless @in_txn
25
25
  v = get(key)
26
26
  unless v
27
27
  @dirty = true
@@ -32,7 +32,7 @@ module Bootsnap
32
32
  end
33
33
 
34
34
  def set(key, value)
35
- raise SetOutsideTransactionNotAllowed unless @in_txn
35
+ raise(SetOutsideTransactionNotAllowed) unless @in_txn
36
36
  if value != @data[key]
37
37
  @dirty = true
38
38
  @data[key] = value
@@ -40,7 +40,7 @@ module Bootsnap
40
40
  end
41
41
 
42
42
  def transaction
43
- raise NestedTransactionError if @in_txn
43
+ raise(NestedTransactionError) if @in_txn
44
44
  @in_txn = true
45
45
  yield
46
46
  ensure
@@ -60,9 +60,9 @@ module Bootsnap
60
60
  def load_data
61
61
  @data = begin
62
62
  MessagePack.load(File.binread(@store_path))
63
- # handle malformed data due to upgrade incompatability
64
- rescue Errno::ENOENT, MessagePack::MalformedFormatError, MessagePack::UnknownExtTypeError, EOFError
65
- {}
63
+ # handle malformed data due to upgrade incompatability
64
+ rescue Errno::ENOENT, MessagePack::MalformedFormatError, MessagePack::UnknownExtTypeError, EOFError
65
+ {}
66
66
  end
67
67
  end
68
68