bootsnap 1.1.8-java → 1.5.0-java

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
1
2
  module Bootsnap
2
3
  module LoadPathCache
3
4
  module CoreExt
4
5
  def self.make_load_error(path)
5
- err = LoadError.new("cannot load such file -- #{path}")
6
+ err = LoadError.new(+"cannot load such file -- #{path}")
7
+ err.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
6
8
  err.define_singleton_method(:path) { path }
7
9
  err
8
10
  end
@@ -11,78 +13,93 @@ module Bootsnap
11
13
  end
12
14
 
13
15
  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
16
+ module_function # rubocop:disable Style/ModuleFunction
26
17
 
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)
18
+ alias_method(:require_without_bootsnap, :require)
19
+
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)
38
24
  end
39
- rescue Bootsnap::LoadPathCache::ReturnFalse
40
- return false
41
- rescue Bootsnap::LoadPathCache::FallbackScan
42
- load_without_cache(path, wrap)
43
25
  end
44
- end
45
26
 
46
- class << Kernel
47
- alias_method :require_without_cache, :require
48
27
  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)
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)
53
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)
54
38
  rescue Bootsnap::LoadPathCache::ReturnFalse
55
- return false
39
+ false
56
40
  rescue Bootsnap::LoadPathCache::FallbackScan
57
- require_without_cache(path)
41
+ fallback = true
42
+ ensure
43
+ if fallback
44
+ require_with_bootsnap_lfi(path)
45
+ end
58
46
  end
59
47
 
60
- alias_method :load_without_cache, :load
48
+ alias_method(:require_relative_without_bootsnap, :require_relative)
49
+ def require_relative(path)
50
+ realpath = Bootsnap::LoadPathCache.realpath_cache.call(
51
+ caller_locations(1..1).first.absolute_path, path
52
+ )
53
+ require(realpath)
54
+ end
55
+
56
+ alias_method(:load_without_bootsnap, :load)
61
57
  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)
58
+ if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path))
59
+ return load_without_bootsnap(resolved, wrap)
71
60
  end
61
+
62
+ # load also allows relative paths from pwd even when not in $:
63
+ if File.exist?(relative = File.expand_path(path).freeze)
64
+ return load_without_bootsnap(relative, wrap)
65
+ end
66
+
67
+ raise(Bootsnap::LoadPathCache::CoreExt.make_load_error(path))
68
+ rescue LoadError => e
69
+ e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
70
+ raise(e)
72
71
  rescue Bootsnap::LoadPathCache::ReturnFalse
73
- return false
72
+ false
74
73
  rescue Bootsnap::LoadPathCache::FallbackScan
75
- load_without_cache(path, wrap)
74
+ fallback = true
75
+ ensure
76
+ if fallback
77
+ load_without_bootsnap(path, wrap)
78
+ end
76
79
  end
77
80
  end
78
81
 
79
82
  class Module
80
- alias_method :autoload_without_cache, :autoload
83
+ alias_method(:autoload_without_bootsnap, :autoload)
81
84
  def autoload(const, path)
82
- autoload_without_cache(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
85
+ # NOTE: This may defeat LoadedFeaturesIndex, but it's not immediately
86
+ # obvious how to make it work. This feels like a pretty niche case, unclear
87
+ # if it will ever burn anyone.
88
+ #
89
+ # The challenge is that we don't control the point at which the entry gets
90
+ # added to $LOADED_FEATURES and won't be able to hook that modification
91
+ # since it's done in C-land.
92
+ autoload_without_bootsnap(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
93
+ rescue LoadError => e
94
+ e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
95
+ raise(e)
83
96
  rescue Bootsnap::LoadPathCache::ReturnFalse
84
- return false
97
+ false
85
98
  rescue Bootsnap::LoadPathCache::FallbackScan
86
- autoload_without_cache(const, path)
99
+ fallback = true
100
+ ensure
101
+ if fallback
102
+ autoload_without_bootsnap(const, path)
103
+ end
87
104
  end
88
105
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ class << $LOADED_FEATURES
3
+ alias_method(:delete_without_bootsnap, :delete)
4
+ def delete(key)
5
+ Bootsnap::LoadPathCache.loaded_features_index.purge(key)
6
+ delete_without_bootsnap(key)
7
+ end
8
+
9
+ alias_method(:reject_without_bootsnap!, :reject!)
10
+ def reject!(&block)
11
+ backup = dup
12
+
13
+ # FIXME: if no block is passed we'd need to return a decorated iterator
14
+ reject_without_bootsnap!(&block)
15
+
16
+ Bootsnap::LoadPathCache.loaded_features_index.purge_multi(backup - self)
17
+ end
18
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bootsnap
4
+ module LoadPathCache
5
+ # LoadedFeaturesIndex partially mirrors an internal structure in ruby that
6
+ # we can't easily obtain an interface to.
7
+ #
8
+ # This works around an issue where, without bootsnap, *ruby* knows that it
9
+ # has already required a file by its short name (e.g. require 'bundler') if
10
+ # a new instance of bundler is added to the $LOAD_PATH which resolves to a
11
+ # different absolute path. This class makes bootsnap smart enough to
12
+ # realize that it has already loaded 'bundler', and not just
13
+ # '/path/to/bundler'.
14
+ #
15
+ # If you disable LoadedFeaturesIndex, you can see the problem this solves by:
16
+ #
17
+ # 1. `require 'a'`
18
+ # 2. Prepend a new $LOAD_PATH element containing an `a.rb`
19
+ # 3. `require 'a'`
20
+ #
21
+ # Ruby returns false from step 3.
22
+ # With bootsnap but with no LoadedFeaturesIndex, this loads two different
23
+ # `a.rb`s.
24
+ # With bootsnap and with LoadedFeaturesIndex, this skips the second load,
25
+ # returning false like ruby.
26
+ class LoadedFeaturesIndex
27
+ def initialize
28
+ @lfi = {}
29
+ @mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new # TODO: Remove once Ruby 2.2 support is dropped.
30
+
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
34
+ # parallel the work done with ChangeObserver on $LOAD_PATH to mirror
35
+ # updates to our @lfi.
36
+ $LOADED_FEATURES.each do |feat|
37
+ hash = feat.hash
38
+ $LOAD_PATH.each do |lpe|
39
+ next unless feat.start_with?(lpe)
40
+ # /a/b/lib/my/foo.rb
41
+ # ^^^^^^^^^
42
+ short = feat[(lpe.length + 1)..-1]
43
+ stripped = strip_extension_if_elidable(short)
44
+ @lfi[short] = hash
45
+ @lfi[stripped] = hash
46
+ end
47
+ end
48
+ end
49
+
50
+ # We've optimized for initialize and register to be fast, and purge to be tolerable.
51
+ # If access patterns make this not-okay, we can lazy-invert the LFI on
52
+ # first purge and work from there.
53
+ def purge(feature)
54
+ @mutex.synchronize do
55
+ feat_hash = feature.hash
56
+ @lfi.reject! { |_, hash| hash == feat_hash }
57
+ end
58
+ end
59
+
60
+ def purge_multi(features)
61
+ rejected_hashes = features.map(&:hash).to_set
62
+ @mutex.synchronize do
63
+ @lfi.reject! { |_, hash| rejected_hashes.include?(hash) }
64
+ end
65
+ end
66
+
67
+ def key?(feature)
68
+ @mutex.synchronize { @lfi.key?(feature) }
69
+ end
70
+
71
+ # There is a relatively uncommon case where we could miss adding an
72
+ # entry:
73
+ #
74
+ # 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
76
+ # pass `long` (the full expanded absolute path), then we did are not able
77
+ # to confidently add the `bundler.rb` form to @lfi.
78
+ #
79
+ # We could either:
80
+ #
81
+ # 1. Just add `bundler.rb`, `bundler.so`, and so on, which is close but
82
+ # not quite right; or
83
+ # 2. Inspect $LOADED_FEATURES upon return from yield to find the matching
84
+ # entry.
85
+ def register(short, long = nil)
86
+ if long.nil?
87
+ pat = %r{/#{Regexp.escape(short)}(\.[^/]+)?$}
88
+ len = $LOADED_FEATURES.size
89
+ ret = yield
90
+ long = $LOADED_FEATURES[len..-1].detect { |feat| feat =~ pat }
91
+ else
92
+ ret = yield
93
+ end
94
+
95
+ hash = long.hash
96
+
97
+ # Do we have a filename with an elidable extension, e.g.,
98
+ # 'bundler.rb', or 'libgit2.so'?
99
+ altname = if extension_elidable?(short)
100
+ # Strip the extension off, e.g. 'bundler.rb' -> 'bundler'.
101
+ strip_extension_if_elidable(short)
102
+ elsif long && (ext = File.extname(long.freeze))
103
+ # We already know the extension of the actual file this
104
+ # resolves to, so put that back on.
105
+ short + ext
106
+ end
107
+
108
+ @mutex.synchronize do
109
+ @lfi[short] = hash
110
+ (@lfi[altname] = hash) if altname
111
+ end
112
+
113
+ ret
114
+ end
115
+
116
+ private
117
+
118
+ STRIP_EXTENSION = /\.[^.]*?$/
119
+ private_constant(:STRIP_EXTENSION)
120
+
121
+ # Might Ruby automatically search for this extension if
122
+ # someone tries to 'require' the file without it? E.g. Ruby
123
+ # will implicitly try 'x.rb' if you ask for 'x'.
124
+ #
125
+ # This is complex and platform-dependent, and the Ruby docs are a little
126
+ # handwavy about what will be tried when and in what order.
127
+ # So optimistically pretend that all known elidable extensions
128
+ # will be tried on all platforms, and that people are unlikely
129
+ # to name files in a way that assumes otherwise.
130
+ # (E.g. It's unlikely that someone will know that their code
131
+ # will _never_ run on MacOS, and therefore think they can get away
132
+ # with calling a Ruby file 'x.dylib.rb' and then requiring it as 'x.dylib'.)
133
+ #
134
+ # See <https://ruby-doc.org/core-2.6.4/Kernel.html#method-i-require>.
135
+ def extension_elidable?(f)
136
+ f.to_s.end_with?('.rb', '.so', '.o', '.dll', '.dylib')
137
+ end
138
+
139
+ def strip_extension_if_elidable(f)
140
+ if extension_elidable?(f)
141
+ f.sub(STRIP_EXTENSION, '')
142
+ else
143
+ f
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -1,4 +1,5 @@
1
- require_relative 'path_scanner'
1
+ # frozen_string_literal: true
2
+ require_relative('path_scanner')
2
3
 
3
4
  module Bootsnap
4
5
  module LoadPathCache
@@ -17,16 +18,16 @@ module Bootsnap
17
18
  stability == VOLATILE
18
19
  end
19
20
 
20
- attr_reader :path
21
+ attr_reader(:path)
21
22
 
22
23
  def initialize(path)
23
- @path = path.to_s
24
+ @path = path.to_s.freeze
24
25
  end
25
26
 
26
27
  # True if the path exists, but represents a non-directory object
27
28
  def non_directory?
28
29
  !File.stat(path).directory?
29
- rescue Errno::ENOENT
30
+ rescue Errno::ENOENT, Errno::ENOTDIR
30
31
  false
31
32
  end
32
33
 
@@ -59,7 +60,7 @@ module Bootsnap
59
60
  end
60
61
 
61
62
  def expanded_path
62
- File.expand_path(path)
63
+ File.expand_path(path).freeze
63
64
  end
64
65
 
65
66
  private
@@ -76,8 +77,8 @@ module Bootsnap
76
77
  ["", *dirs].each do |dir|
77
78
  curr = begin
78
79
  File.mtime("#{path}/#{dir}").to_i
79
- rescue Errno::ENOENT
80
- -1
80
+ rescue Errno::ENOENT, Errno::ENOTDIR
81
+ -1
81
82
  end
82
83
  max = curr if curr > max
83
84
  end
@@ -1,51 +1,62 @@
1
- require_relative '../explicit_require'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative('../explicit_require')
2
4
 
3
5
  module Bootsnap
4
6
  module LoadPathCache
5
7
  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(',')},/}"
8
+ REQUIRABLE_EXTENSIONS = [DOT_RB] + DL_EXTENSIONS
16
9
  NORMALIZE_NATIVE_EXTENSIONS = !DL_EXTENSIONS.include?(LoadPathCache::DOT_SO)
17
10
  ALTERNATIVE_NATIVE_EXTENSIONS_PATTERN = /\.(o|bundle|dylib)\z/
18
- BUNDLE_PATH = Bootsnap.bundler? ?
19
- (Bundler.bundle_path.cleanpath.to_s << LoadPathCache::SLASH).freeze : ''.freeze
20
-
21
- def self.call(path)
22
- path = path.to_s
23
-
24
- relative_slice = (path.size + 1)..-1
25
- # If the bundle path is a descendent of this path, we do additional
26
- # checks to prevent recursing into the bundle path as we recurse
27
- # through this path. We don't want to scan the bundle path because
28
- # anything useful in it will be present on other load path items.
29
- #
30
- # This can happen if, for example, the user adds '.' to the load path,
31
- # and the bundle path is '.bundle'.
32
- contains_bundle_path = BUNDLE_PATH.start_with?(path)
33
-
34
- dirs = []
35
- requirables = []
36
-
37
- Dir.glob(path + REQUIRABLES_AND_DIRS).each do |absolute_path|
38
- next if contains_bundle_path && absolute_path.start_with?(BUNDLE_PATH)
39
- relative_path = absolute_path.slice!(relative_slice)
40
-
41
- if relative_path.end_with?('/')
42
- dirs << relative_path[0..-2]
43
- else
44
- requirables << relative_path
11
+
12
+ BUNDLE_PATH = if Bootsnap.bundler?
13
+ (Bundler.bundle_path.cleanpath.to_s << LoadPathCache::SLASH).freeze
14
+ else
15
+ ''
16
+ end
17
+
18
+ class << self
19
+ def call(path)
20
+ path = File.expand_path(path.to_s).freeze
21
+ return [[], []] unless File.directory?(path)
22
+
23
+ # If the bundle path is a descendent of this path, we do additional
24
+ # checks to prevent recursing into the bundle path as we recurse
25
+ # through this path. We don't want to scan the bundle path because
26
+ # anything useful in it will be present on other load path items.
27
+ #
28
+ # This can happen if, for example, the user adds '.' to the load path,
29
+ # and the bundle path is '.bundle'.
30
+ contains_bundle_path = BUNDLE_PATH.start_with?(path)
31
+
32
+ dirs = []
33
+ requirables = []
34
+ walk(path, nil) do |relative_path, absolute_path, is_directory|
35
+ if is_directory
36
+ dirs << relative_path
37
+ !contains_bundle_path || !absolute_path.start_with?(BUNDLE_PATH)
38
+ elsif relative_path.end_with?(*REQUIRABLE_EXTENSIONS)
39
+ requirables << relative_path
40
+ end
45
41
  end
42
+ [requirables, dirs]
46
43
  end
47
44
 
48
- [requirables, dirs]
45
+ def walk(absolute_dir_path, relative_dir_path, &block)
46
+ Dir.foreach(absolute_dir_path) do |name|
47
+ next if name.start_with?('.')
48
+ relative_path = relative_dir_path ? "#{relative_dir_path}/#{name}" : name.freeze
49
+
50
+ absolute_path = "#{absolute_dir_path}/#{name}"
51
+ if File.directory?(absolute_path)
52
+ if yield relative_path, absolute_path, true
53
+ walk(absolute_path, relative_path, &block)
54
+ end
55
+ else
56
+ yield relative_path, absolute_path, false
57
+ end
58
+ end
59
+ end
49
60
  end
50
61
  end
51
62
  end