bootsnap 1.9.1 → 1.16.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,6 @@
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
@@ -28,15 +28,16 @@ module Bootsnap
28
28
  BUILTIN_FEATURES = $LOADED_FEATURES.each_with_object({}) do |feat, features|
29
29
  # Builtin features are of the form 'enumerator.so'.
30
30
  # All others include paths.
31
- next unless feat.size < 20 && !feat.include?('/')
31
+ next unless feat.size < 20 && !feat.include?("/")
32
32
 
33
- base = File.basename(feat, '.*') # enumerator.so -> enumerator
33
+ base = File.basename(feat, ".*") # enumerator.so -> enumerator
34
34
  ext = File.extname(feat) # .so
35
35
 
36
36
  features[feat] = nil # enumerator.so
37
37
  features[base] = nil # enumerator
38
38
 
39
39
  next unless [DOT_SO, *DL_EXTENSIONS].include?(ext)
40
+
40
41
  DL_EXTENSIONS.each do |dl_ext|
41
42
  features["#{base}#{dl_ext}"] = nil # enumerator.bundle
42
43
  end
@@ -47,8 +48,13 @@ module Bootsnap
47
48
  def find(feature)
48
49
  reinitialize if (@has_relative_paths && dir_changed?) || stale?
49
50
  feature = feature.to_s.freeze
50
- return feature if absolute_path?(feature)
51
- return expand_path(feature) if feature.start_with?('./')
51
+
52
+ return feature if Bootsnap.absolute_path?(feature)
53
+
54
+ if feature.start_with?("./", "../")
55
+ return expand_path(feature)
56
+ end
57
+
52
58
  @mutex.synchronize do
53
59
  x = search_index(feature)
54
60
  return x if x
@@ -58,7 +64,7 @@ module Bootsnap
58
64
  # returns false as if it were already loaded; however, there is no
59
65
  # file to find on disk. We've pre-built a list of these, and we
60
66
  # return false if any of them is loaded.
61
- raise(LoadPathCache::ReturnFalse, '', []) if BUILTIN_FEATURES.key?(feature)
67
+ return false if BUILTIN_FEATURES.key?(feature)
62
68
 
63
69
  # The feature wasn't found on our preliminary search through the index.
64
70
  # We resolve this differently depending on what the extension was.
@@ -67,13 +73,14 @@ module Bootsnap
67
73
  # native dynamic extension, e.g. .bundle or .so), we know it was a
68
74
  # failure and there's nothing more we can do to find the file.
69
75
  # no extension, .rb, (.bundle or .so)
70
- when '', *CACHED_EXTENSIONS
76
+ when "", *CACHED_EXTENSIONS
71
77
  nil
72
78
  # Ruby allows specifying native extensions as '.so' even when DLEXT
73
79
  # is '.bundle'. This is where we handle that case.
74
80
  when DOT_SO
75
81
  x = search_index(feature[0..-4] + DLEXT)
76
82
  return x if x
83
+
77
84
  if DLEXT2
78
85
  x = search_index(feature[0..-4] + DLEXT2)
79
86
  return x if x
@@ -81,7 +88,7 @@ module Bootsnap
81
88
  else
82
89
  # other, unknown extension. For example, `.rake`. Since we haven't
83
90
  # cached these, we legitimately need to run the load path search.
84
- raise(LoadPathCache::FallbackScan, '', [])
91
+ return FALLBACK_SCAN
85
92
  end
86
93
  end
87
94
 
@@ -89,33 +96,25 @@ module Bootsnap
89
96
  # cases where the file doesn't appear to be on the load path. We should
90
97
  # be able to detect newly-created files without rebooting the
91
98
  # application.
92
- raise(LoadPathCache::FallbackScan, '', []) if @development_mode
93
- end
94
-
95
- if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
96
- def absolute_path?(path)
97
- path[1] == ':'
98
- end
99
- else
100
- def absolute_path?(path)
101
- path.start_with?(SLASH)
102
- end
99
+ return FALLBACK_SCAN if @development_mode
103
100
  end
104
101
 
105
102
  def unshift_paths(sender, *paths)
106
103
  return unless sender == @path_obj
104
+
107
105
  @mutex.synchronize { unshift_paths_locked(*paths) }
108
106
  end
109
107
 
110
108
  def push_paths(sender, *paths)
111
109
  return unless sender == @path_obj
110
+
112
111
  @mutex.synchronize { push_paths_locked(*paths) }
113
112
  end
114
113
 
115
114
  def reinitialize(path_obj = @path_obj)
116
115
  @mutex.synchronize do
117
116
  @path_obj = path_obj
118
- ChangeObserver.register(self, @path_obj)
117
+ ChangeObserver.register(@path_obj, self)
119
118
  @index = {}
120
119
  @dirs = {}
121
120
  @generated_at = now
@@ -141,6 +140,9 @@ module Bootsnap
141
140
  p = Path.new(path)
142
141
  @has_relative_paths = true if p.relative?
143
142
  next if p.non_directory?
143
+
144
+ p = p.to_realpath
145
+
144
146
  expanded_path = p.expanded_path
145
147
  entries, dirs = p.entries_and_dirs(@store)
146
148
  # push -> low precedence -> set only if unset
@@ -155,6 +157,9 @@ module Bootsnap
155
157
  paths.map(&:to_s).reverse_each do |path|
156
158
  p = Path.new(path)
157
159
  next if p.non_directory?
160
+
161
+ p = p.to_realpath
162
+
158
163
  expanded_path = p.expanded_path
159
164
  entries, dirs = p.entries_and_dirs(@store)
160
165
  # unshift -> high precedence -> unconditional set
@@ -177,48 +182,54 @@ module Bootsnap
177
182
  end
178
183
 
179
184
  if DLEXT2
180
- def search_index(f)
181
- try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f + DLEXT2) || try_index(f)
185
+ def search_index(feature)
186
+ try_index(feature + DOT_RB) ||
187
+ try_index(feature + DLEXT) ||
188
+ try_index(feature + DLEXT2) ||
189
+ try_index(feature)
182
190
  end
183
191
 
184
- def maybe_append_extension(f)
185
- try_ext(f + DOT_RB) || try_ext(f + DLEXT) || try_ext(f + DLEXT2) || f
192
+ def maybe_append_extension(feature)
193
+ try_ext(feature + DOT_RB) ||
194
+ try_ext(feature + DLEXT) ||
195
+ try_ext(feature + DLEXT2) ||
196
+ feature
186
197
  end
187
198
  else
188
- def search_index(f)
189
- try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f)
199
+ def search_index(feature)
200
+ try_index(feature + DOT_RB) || try_index(feature + DLEXT) || try_index(feature)
190
201
  end
191
202
 
192
- def maybe_append_extension(f)
193
- try_ext(f + DOT_RB) || try_ext(f + DLEXT) || f
203
+ def maybe_append_extension(feature)
204
+ try_ext(feature + DOT_RB) || try_ext(feature + DLEXT) || feature
194
205
  end
195
206
  end
196
207
 
197
208
  s = rand.to_s.force_encoding(Encoding::US_ASCII).freeze
198
209
  if s.respond_to?(:-@)
199
- if (-s).equal?(s) && (-s.dup).equal?(s) || RUBY_VERSION >= '2.7'
200
- def try_index(f)
201
- if (p = @index[f])
202
- -(File.join(p, f).freeze)
210
+ if ((-s).equal?(s) && (-s.dup).equal?(s)) || RUBY_VERSION >= "2.7"
211
+ def try_index(feature)
212
+ if (path = @index[feature])
213
+ -File.join(path, feature).freeze
203
214
  end
204
215
  end
205
216
  else
206
- def try_index(f)
207
- if (p = @index[f])
208
- -File.join(p, f).untaint
217
+ def try_index(feature)
218
+ if (path = @index[feature])
219
+ -File.join(path, feature).untaint
209
220
  end
210
221
  end
211
222
  end
212
223
  else
213
- def try_index(f)
214
- if (p = @index[f])
215
- File.join(p, f)
224
+ def try_index(feature)
225
+ if (path = @index[feature])
226
+ File.join(path, feature)
216
227
  end
217
228
  end
218
229
  end
219
230
 
220
- def try_ext(f)
221
- f if File.exist?(f)
231
+ def try_ext(feature)
232
+ feature if File.exist?(feature)
222
233
  end
223
234
  end
224
235
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Bootsnap
3
4
  module LoadPathCache
4
5
  module ChangeObserver
@@ -55,10 +56,22 @@ module Bootsnap
55
56
  end
56
57
  end
57
58
 
58
- def self.register(observer, arr)
59
+ def self.register(arr, observer)
59
60
  return if arr.frozen? # can't register observer, but no need to.
61
+
60
62
  arr.instance_variable_set(:@lpc_observer, observer)
61
- arr.extend(ArrayMixin)
63
+ ArrayMixin.instance_methods.each do |method_name|
64
+ arr.singleton_class.send(:define_method, method_name, ArrayMixin.instance_method(method_name))
65
+ end
66
+ end
67
+
68
+ def self.unregister(arr)
69
+ return unless arr.instance_variable_defined?(:@lpc_observer) && arr.instance_variable_get(:@lpc_observer)
70
+
71
+ ArrayMixin.instance_methods.each do |method_name|
72
+ arr.singleton_class.send(:remove_method, method_name)
73
+ end
74
+ arr.instance_variable_set(:@lpc_observer, nil)
62
75
  end
63
76
  end
64
77
  end
@@ -1,106 +1,37 @@
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)
45
- end
46
- end
47
-
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
- alias_method(:load_without_bootsnap, :load)
58
- def load(path, wrap = false)
59
- if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path))
60
- return load_without_bootsnap(resolved, wrap)
61
- end
62
-
63
- # load also allows relative paths from pwd even when not in $:
64
- if File.exist?(relative = File.expand_path(path).freeze)
65
- return load_without_bootsnap(relative, wrap)
66
- end
67
-
68
- raise(Bootsnap::LoadPathCache::CoreExt.make_load_error(path))
69
- rescue LoadError => e
70
- e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
71
- raise(e)
72
- rescue Bootsnap::LoadPathCache::ReturnFalse
73
- false
74
- rescue Bootsnap::LoadPathCache::FallbackScan
75
- fallback = true
76
- ensure
77
- if fallback
78
- load_without_bootsnap(path, wrap)
79
- end
80
- end
81
- end
82
-
83
- class Module
84
- alias_method(:autoload_without_bootsnap, :autoload)
85
- def autoload(const, path)
86
- # NOTE: This may defeat LoadedFeaturesIndex, but it's not immediately
87
- # obvious how to make it work. This feels like a pretty niche case, unclear
88
- # if it will ever burn anyone.
89
- #
90
- # The challenge is that we don't control the point at which the entry gets
91
- # added to $LOADED_FEATURES and won't be able to hook that modification
92
- # since it's done in C-land.
93
- autoload_without_bootsnap(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
94
- rescue LoadError => e
95
- e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
96
- raise(e)
97
- rescue Bootsnap::LoadPathCache::ReturnFalse
98
- false
99
- rescue Bootsnap::LoadPathCache::FallbackScan
100
- fallback = true
101
- ensure
102
- if fallback
103
- autoload_without_bootsnap(const, path)
9
+ return require_without_bootsnap(path) unless Bootsnap::LoadPathCache.enabled?
10
+
11
+ string_path = Bootsnap.rb_get_path(path)
12
+ return false if Bootsnap::LoadPathCache.loaded_features_index.key?(string_path)
13
+
14
+ resolved = Bootsnap::LoadPathCache.load_path_cache.find(string_path)
15
+ if Bootsnap::LoadPathCache::FALLBACK_SCAN.equal?(resolved)
16
+ if (cursor = Bootsnap::LoadPathCache.loaded_features_index.cursor(string_path))
17
+ ret = require_without_bootsnap(path)
18
+ resolved = Bootsnap::LoadPathCache.loaded_features_index.identify(string_path, cursor)
19
+ Bootsnap::LoadPathCache.loaded_features_index.register(string_path, resolved)
20
+ return ret
21
+ else
22
+ return require_without_bootsnap(path)
23
+ end
24
+ elsif false == resolved
25
+ return false
26
+ elsif resolved.nil?
27
+ error = LoadError.new(+"cannot load such file -- #{path}")
28
+ error.instance_variable_set(:@path, path)
29
+ raise error
30
+ else
31
+ # Note that require registers to $LOADED_FEATURES while load does not.
32
+ ret = require_without_bootsnap(resolved)
33
+ Bootsnap::LoadPathCache.loaded_features_index.register(string_path, resolved)
34
+ return ret
104
35
  end
105
36
  end
106
37
  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,17 +29,18 @@ 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
- short = feat[(lpe.length + 1)..-1]
43
+ short = feat[(lpe.length + 1)..]
43
44
  stripped = strip_extension_if_elidable(short)
44
45
  @lfi[short] = hash
45
46
  @lfi[stripped] = hash
@@ -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..].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,15 +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
- 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
105
+ def register(short, long)
106
+ return if Bootsnap.absolute_path?(short)
94
107
 
95
108
  hash = long.hash
96
109
 
@@ -109,13 +122,11 @@ module Bootsnap
109
122
  @lfi[short] = hash
110
123
  (@lfi[altname] = hash) if altname
111
124
  end
112
-
113
- ret
114
125
  end
115
126
 
116
127
  private
117
128
 
118
- STRIP_EXTENSION = /\.[^.]*?$/
129
+ STRIP_EXTENSION = /\.[^.]*?$/.freeze
119
130
  private_constant(:STRIP_EXTENSION)
120
131
 
121
132
  # Might Ruby automatically search for this extension if
@@ -132,15 +143,15 @@ module Bootsnap
132
143
  # with calling a Ruby file 'x.dylib.rb' and then requiring it as 'x.dylib'.)
133
144
  #
134
145
  # 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')
146
+ def extension_elidable?(feature)
147
+ feature.to_s.end_with?(".rb", ".so", ".o", ".dll", ".dylib")
137
148
  end
138
149
 
139
- def strip_extension_if_elidable(f)
140
- if extension_elidable?(f)
141
- f.sub(STRIP_EXTENSION, '')
150
+ def strip_extension_if_elidable(feature)
151
+ if extension_elidable?(feature)
152
+ feature.sub(STRIP_EXTENSION, "")
142
153
  else
143
- f
154
+ feature
144
155
  end
145
156
  end
146
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,14 +21,32 @@ 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
+ @real = true
40
+ self
41
+ else
42
+ Path.new(realpath, real: true)
43
+ end
25
44
  end
26
45
 
27
46
  # True if the path exists, but represents a non-directory object
28
47
  def non_directory?
29
48
  !File.stat(path).directory?
30
- rescue Errno::ENOENT, Errno::ENOTDIR
49
+ rescue Errno::ENOENT, Errno::ENOTDIR, Errno::EINVAL
31
50
  false
32
51
  end
33
52
 
@@ -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
@@ -77,7 +101,7 @@ module Bootsnap
77
101
  ["", *dirs].each do |dir|
78
102
  curr = begin
79
103
  File.mtime("#{path}/#{dir}").to_i
80
- rescue Errno::ENOENT, Errno::ENOTDIR
104
+ rescue Errno::ENOENT, Errno::ENOTDIR, Errno::EINVAL
81
105
  -1
82
106
  end
83
107
  max = curr if curr > max
@@ -92,21 +116,19 @@ module Bootsnap
92
116
  VOLATILE = :volatile
93
117
 
94
118
  # Built-in ruby lib stuff doesn't change, but things can occasionally be
95
- # installed into sitedir, which generally lives under libdir.
96
- RUBY_LIBDIR = RbConfig::CONFIG['libdir']
97
- RUBY_SITEDIR = RbConfig::CONFIG['sitedir']
119
+ # installed into sitedir, which generally lives under rubylibdir.
120
+ RUBY_LIBDIR = RbConfig::CONFIG["rubylibdir"]
121
+ RUBY_SITEDIR = RbConfig::CONFIG["sitedir"]
98
122
 
99
123
  def stability
100
- @stability ||= begin
101
- if Gem.path.detect { |p| expanded_path.start_with?(p.to_s) }
102
- STABLE
103
- elsif Bootsnap.bundler? && expanded_path.start_with?(Bundler.bundle_path.to_s)
104
- STABLE
105
- elsif expanded_path.start_with?(RUBY_LIBDIR) && !expanded_path.start_with?(RUBY_SITEDIR)
106
- STABLE
107
- else
108
- VOLATILE
109
- end
124
+ @stability ||= if Gem.path.detect { |p| expanded_path.start_with?(p.to_s) }
125
+ STABLE
126
+ elsif Bootsnap.bundler? && expanded_path.start_with?(Bundler.bundle_path.to_s)
127
+ STABLE
128
+ elsif expanded_path.start_with?(RUBY_LIBDIR) && !expanded_path.start_with?(RUBY_SITEDIR)
129
+ STABLE
130
+ else
131
+ VOLATILE
110
132
  end
111
133
  end
112
134
  end
@@ -1,21 +1,25 @@
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
+ @ignored_directories = %w(node_modules)
19
+
18
20
  class << self
21
+ attr_accessor :ignored_directories
22
+
19
23
  def call(path)
20
24
  path = File.expand_path(path.to_s).freeze
21
25
  return [[], []] unless File.directory?(path)
@@ -44,11 +48,14 @@ module Bootsnap
44
48
 
45
49
  def walk(absolute_dir_path, relative_dir_path, &block)
46
50
  Dir.foreach(absolute_dir_path) do |name|
47
- next if name.start_with?('.')
51
+ next if name.start_with?(".")
52
+
48
53
  relative_path = relative_dir_path ? File.join(relative_dir_path, name) : name
49
54
 
50
55
  absolute_path = "#{absolute_dir_path}/#{name}"
51
56
  if File.directory?(absolute_path)
57
+ next if ignored_directories.include?(name)
58
+
52
59
  if yield relative_path, absolute_path, true
53
60
  walk(absolute_path, relative_path, &block)
54
61
  end
@@ -58,7 +65,7 @@ module Bootsnap
58
65
  end
59
66
  end
60
67
 
61
- if RUBY_VERSION >= '3.1'
68
+ if RUBY_VERSION >= "3.1"
62
69
  def os_path(path)
63
70
  path.freeze
64
71
  end