bootsnap 1.1.8 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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