bootsnap 1.4.5 → 1.18.3

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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +264 -0
  3. data/LICENSE.txt +1 -1
  4. data/README.md +63 -23
  5. data/exe/bootsnap +5 -0
  6. data/ext/bootsnap/bootsnap.c +504 -184
  7. data/ext/bootsnap/extconf.rb +30 -15
  8. data/lib/bootsnap/bundler.rb +3 -1
  9. data/lib/bootsnap/cli/worker_pool.rb +136 -0
  10. data/lib/bootsnap/cli.rb +283 -0
  11. data/lib/bootsnap/compile_cache/iseq.rb +72 -21
  12. data/lib/bootsnap/compile_cache/json.rb +89 -0
  13. data/lib/bootsnap/compile_cache/yaml.rb +316 -41
  14. data/lib/bootsnap/compile_cache.rb +27 -17
  15. data/lib/bootsnap/explicit_require.rb +5 -3
  16. data/lib/bootsnap/load_path_cache/cache.rb +73 -37
  17. data/lib/bootsnap/load_path_cache/change_observer.rb +25 -3
  18. data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +27 -82
  19. data/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb +2 -0
  20. data/lib/bootsnap/load_path_cache/loaded_features_index.rb +63 -29
  21. data/lib/bootsnap/load_path_cache/path.rb +42 -19
  22. data/lib/bootsnap/load_path_cache/path_scanner.rb +60 -29
  23. data/lib/bootsnap/load_path_cache/store.rb +64 -23
  24. data/lib/bootsnap/load_path_cache.rb +40 -38
  25. data/lib/bootsnap/setup.rb +3 -36
  26. data/lib/bootsnap/version.rb +3 -1
  27. data/lib/bootsnap.rb +141 -36
  28. metadata +15 -99
  29. data/.github/CODEOWNERS +0 -2
  30. data/.github/probots.yml +0 -2
  31. data/.gitignore +0 -17
  32. data/.rubocop.yml +0 -20
  33. data/.travis.yml +0 -21
  34. data/CODE_OF_CONDUCT.md +0 -74
  35. data/CONTRIBUTING.md +0 -21
  36. data/Gemfile +0 -8
  37. data/README.jp.md +0 -231
  38. data/Rakefile +0 -12
  39. data/bin/ci +0 -10
  40. data/bin/console +0 -14
  41. data/bin/setup +0 -8
  42. data/bin/test-minimal-support +0 -7
  43. data/bin/testunit +0 -8
  44. data/bootsnap.gemspec +0 -45
  45. data/dev.yml +0 -10
  46. data/lib/bootsnap/load_path_cache/core_ext/active_support.rb +0 -106
  47. data/lib/bootsnap/load_path_cache/realpath_cache.rb +0 -32
  48. data/shipit.rubygems.yml +0 -0
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Bootsnap
2
4
  module LoadPathCache
3
5
  module ChangeObserver
@@ -14,18 +16,20 @@ module Bootsnap
14
16
  @lpc_observer.push_paths(self, *entries.map(&:to_s))
15
17
  super
16
18
  end
19
+ alias_method :append, :push
17
20
 
18
21
  def unshift(*entries)
19
22
  @lpc_observer.unshift_paths(self, *entries.map(&:to_s))
20
23
  super
21
24
  end
25
+ alias_method :prepend, :unshift
22
26
 
23
27
  def concat(entries)
24
28
  @lpc_observer.push_paths(self, *entries.map(&:to_s))
25
29
  super
26
30
  end
27
31
 
28
- # uniq! keeps the first occurance of each path, otherwise preserving
32
+ # uniq! keeps the first occurrence of each path, otherwise preserving
29
33
  # order, preserving the effective load path
30
34
  def uniq!(*args)
31
35
  ret = super
@@ -50,12 +54,30 @@ module Bootsnap
50
54
  ret
51
55
  end
52
56
  end
57
+
58
+ def dup
59
+ [] + self
60
+ end
61
+
62
+ alias_method :clone, :dup
53
63
  end
54
64
 
55
- def self.register(observer, arr)
65
+ def self.register(arr, observer)
56
66
  return if arr.frozen? # can't register observer, but no need to.
67
+
57
68
  arr.instance_variable_set(:@lpc_observer, observer)
58
- arr.extend(ArrayMixin)
69
+ ArrayMixin.instance_methods.each do |method_name|
70
+ arr.singleton_class.send(:define_method, method_name, ArrayMixin.instance_method(method_name))
71
+ end
72
+ end
73
+
74
+ def self.unregister(arr)
75
+ return unless arr.instance_variable_defined?(:@lpc_observer) && arr.instance_variable_get(:@lpc_observer)
76
+
77
+ ArrayMixin.instance_methods.each do |method_name|
78
+ arr.singleton_class.send(:remove_method, method_name)
79
+ end
80
+ arr.instance_variable_set(:@lpc_observer, nil)
59
81
  end
60
82
  end
61
83
  end
@@ -1,92 +1,37 @@
1
- module Bootsnap
2
- module LoadPathCache
3
- module CoreExt
4
- def self.make_load_error(path)
5
- err = LoadError.new("cannot load such file -- #{path}")
6
- err.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
7
- err.define_singleton_method(:path) { path }
8
- err
9
- end
10
- end
11
- end
12
- end
1
+ # frozen_string_literal: true
13
2
 
14
3
  module Kernel
15
- module_function # rubocop:disable Style/ModuleFunction
16
-
17
- alias_method(:require_without_bootsnap, :require)
18
-
19
- # Note that require registers to $LOADED_FEATURES while load does not.
20
- def require_with_bootsnap_lfi(path, resolved = nil)
21
- Bootsnap::LoadPathCache.loaded_features_index.register(path, resolved) do
22
- require_without_bootsnap(resolved || path)
23
- end
24
- end
25
-
26
- def require(path)
27
- return false if Bootsnap::LoadPathCache.loaded_features_index.key?(path)
4
+ alias_method :require_without_bootsnap, :require
28
5
 
29
- if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path))
30
- return require_with_bootsnap_lfi(path, resolved)
31
- end
6
+ alias_method :require, :require # Avoid method redefinition warnings
32
7
 
33
- raise(Bootsnap::LoadPathCache::CoreExt.make_load_error(path))
34
- rescue LoadError => e
35
- e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
36
- raise(e)
37
- rescue Bootsnap::LoadPathCache::ReturnFalse
38
- false
39
- rescue Bootsnap::LoadPathCache::FallbackScan
40
- require_with_bootsnap_lfi(path)
41
- end
8
+ def require(path) # rubocop:disable Lint/DuplicateMethods
9
+ return require_without_bootsnap(path) unless Bootsnap::LoadPathCache.enabled?
42
10
 
43
- alias_method(:require_relative_without_bootsnap, :require_relative)
44
- def require_relative(path)
45
- realpath = Bootsnap::LoadPathCache.realpath_cache.call(
46
- caller_locations(1..1).first.absolute_path, path
47
- )
48
- require(realpath)
49
- end
50
-
51
- alias_method(:load_without_bootsnap, :load)
52
- def load(path, wrap = false)
53
- if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path))
54
- return load_without_bootsnap(resolved, wrap)
55
- end
11
+ string_path = Bootsnap.rb_get_path(path)
12
+ return false if Bootsnap::LoadPathCache.loaded_features_index.key?(string_path)
56
13
 
57
- # load also allows relative paths from pwd even when not in $:
58
- if File.exist?(relative = File.expand_path(path))
59
- return load_without_bootsnap(relative, wrap)
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
+ return require_without_bootsnap(path)
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
60
33
  end
61
-
62
- raise(Bootsnap::LoadPathCache::CoreExt.make_load_error(path))
63
- rescue LoadError => e
64
- e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
65
- raise(e)
66
- rescue Bootsnap::LoadPathCache::ReturnFalse
67
- false
68
- rescue Bootsnap::LoadPathCache::FallbackScan
69
- load_without_bootsnap(path, wrap)
70
34
  end
71
- end
72
35
 
73
- class Module
74
- alias_method(:autoload_without_bootsnap, :autoload)
75
- def autoload(const, path)
76
- # NOTE: This may defeat LoadedFeaturesIndex, but it's not immediately
77
- # obvious how to make it work. This feels like a pretty niche case, unclear
78
- # if it will ever burn anyone.
79
- #
80
- # The challenge is that we don't control the point at which the entry gets
81
- # added to $LOADED_FEATURES and won't be able to hook that modification
82
- # since it's done in C-land.
83
- autoload_without_bootsnap(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
84
- rescue LoadError => e
85
- e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
86
- raise(e)
87
- rescue Bootsnap::LoadPathCache::ReturnFalse
88
- false
89
- rescue Bootsnap::LoadPathCache::FallbackScan
90
- autoload_without_bootsnap(const, path)
91
- end
36
+ private :require
92
37
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class << $LOADED_FEATURES
2
4
  alias_method(:delete_without_bootsnap, :delete)
3
5
  def delete(key)
@@ -26,21 +26,22 @@ module Bootsnap
26
26
  class LoadedFeaturesIndex
27
27
  def initialize
28
28
  @lfi = {}
29
- @mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new # TODO: Remove once Ruby 2.2 support is dropped.
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
- stripped = strip_extension(short)
43
+ short = feat[(lpe.length + 1)..]
44
+ stripped = strip_extension_if_elidable(short)
44
45
  @lfi[short] = hash
45
46
  @lfi[stripped] = hash
46
47
  end
@@ -58,9 +59,9 @@ module Bootsnap
58
59
  end
59
60
 
60
61
  def purge_multi(features)
61
- rejected_hashes = features.map(&:hash).to_set
62
+ rejected_hashes = features.each_with_object({}) { |f, h| h[f.hash] = true }
62
63
  @mutex.synchronize do
63
- @lfi.reject! { |_, hash| rejected_hashes.include?(hash) }
64
+ @lfi.reject! { |_, hash| rejected_hashes.key?(hash) }
64
65
  end
65
66
  end
66
67
 
@@ -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,25 +102,19 @@ 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
 
97
- # do we have 'bundler' or 'bundler.rb'?
98
- altname = if File.extname(short) != ''
99
- # strip the path from 'bundler.rb' -> 'bundler'
100
- strip_extension(short)
101
- elsif long && (ext = File.extname(long))
102
- # get the extension from the expanded path if given
103
- # 'bundler' + '.rb'
110
+ # Do we have a filename with an elidable extension, e.g.,
111
+ # 'bundler.rb', or 'libgit2.so'?
112
+ altname = if extension_elidable?(short)
113
+ # Strip the extension off, e.g. 'bundler.rb' -> 'bundler'.
114
+ strip_extension_if_elidable(short)
115
+ elsif long && (ext = File.extname(long.freeze))
116
+ # We already know the extension of the actual file this
117
+ # resolves to, so put that back on.
104
118
  short + ext
105
119
  end
106
120
 
@@ -108,17 +122,37 @@ module Bootsnap
108
122
  @lfi[short] = hash
109
123
  (@lfi[altname] = hash) if altname
110
124
  end
111
-
112
- ret
113
125
  end
114
126
 
115
127
  private
116
128
 
117
- STRIP_EXTENSION = /\.[^.]*?$/
129
+ STRIP_EXTENSION = /\.[^.]*?$/.freeze
118
130
  private_constant(:STRIP_EXTENSION)
119
131
 
120
- def strip_extension(f)
121
- f.sub(STRIP_EXTENSION, '')
132
+ # Might Ruby automatically search for this extension if
133
+ # someone tries to 'require' the file without it? E.g. Ruby
134
+ # will implicitly try 'x.rb' if you ask for 'x'.
135
+ #
136
+ # This is complex and platform-dependent, and the Ruby docs are a little
137
+ # handwavy about what will be tried when and in what order.
138
+ # So optimistically pretend that all known elidable extensions
139
+ # will be tried on all platforms, and that people are unlikely
140
+ # to name files in a way that assumes otherwise.
141
+ # (E.g. It's unlikely that someone will know that their code
142
+ # will _never_ run on MacOS, and therefore think they can get away
143
+ # with calling a Ruby file 'x.dylib.rb' and then requiring it as 'x.dylib'.)
144
+ #
145
+ # See <https://ruby-doc.org/core-2.6.4/Kernel.html#method-i-require>.
146
+ def extension_elidable?(feature)
147
+ feature.to_s.end_with?(".rb", ".so", ".o", ".dll", ".dylib")
148
+ end
149
+
150
+ def strip_extension_if_elidable(feature)
151
+ if extension_elidable?(feature)
152
+ feature.sub(STRIP_EXTENSION, "")
153
+ else
154
+ feature
155
+ end
122
156
  end
123
157
  end
124
158
  end
@@ -1,4 +1,6 @@
1
- require_relative('path_scanner')
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "path_scanner"
2
4
 
3
5
  module Bootsnap
4
6
  module LoadPathCache
@@ -19,14 +21,32 @@ module Bootsnap
19
21
 
20
22
  attr_reader(:path)
21
23
 
22
- def initialize(path)
23
- @path = path.to_s
24
+ def initialize(path, real: false)
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
24
44
  end
25
45
 
26
46
  # True if the path exists, but represents a non-directory object
27
47
  def non_directory?
28
48
  !File.stat(path).directory?
29
- rescue Errno::ENOENT, Errno::ENOTDIR
49
+ rescue Errno::ENOENT, Errno::ENOTDIR, Errno::EINVAL
30
50
  false
31
51
  end
32
52
 
@@ -42,6 +62,7 @@ module Bootsnap
42
62
  # set to zero anyway, just in case we change the stability heuristics.
43
63
  _, entries, dirs = store.get(expanded_path)
44
64
  return [entries, dirs] if entries # cache hit
65
+
45
66
  entries, dirs = scan!
46
67
  store.set(expanded_path, [0, entries, dirs])
47
68
  return [entries, dirs]
@@ -59,7 +80,11 @@ module Bootsnap
59
80
  end
60
81
 
61
82
  def expanded_path
62
- File.expand_path(path)
83
+ if @real
84
+ path
85
+ else
86
+ @expanded_path ||= File.expand_path(path).freeze
87
+ end
63
88
  end
64
89
 
65
90
  private
@@ -76,7 +101,7 @@ module Bootsnap
76
101
  ["", *dirs].each do |dir|
77
102
  curr = begin
78
103
  File.mtime("#{path}/#{dir}").to_i
79
- rescue Errno::ENOENT, Errno::ENOTDIR
104
+ rescue Errno::ENOENT, Errno::ENOTDIR, Errno::EINVAL
80
105
  -1
81
106
  end
82
107
  max = curr if curr > max
@@ -91,21 +116,19 @@ module Bootsnap
91
116
  VOLATILE = :volatile
92
117
 
93
118
  # Built-in ruby lib stuff doesn't change, but things can occasionally be
94
- # installed into sitedir, which generally lives under libdir.
95
- RUBY_LIBDIR = RbConfig::CONFIG['libdir']
96
- 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"]
97
122
 
98
123
  def stability
99
- @stability ||= begin
100
- if Gem.path.detect { |p| expanded_path.start_with?(p.to_s) }
101
- STABLE
102
- elsif Bootsnap.bundler? && expanded_path.start_with?(Bundler.bundle_path.to_s)
103
- STABLE
104
- elsif expanded_path.start_with?(RUBY_LIBDIR) && !expanded_path.start_with?(RUBY_SITEDIR)
105
- STABLE
106
- else
107
- VOLATILE
108
- 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
109
132
  end
110
133
  end
111
134
  end
@@ -1,49 +1,80 @@
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
- ALL_FILES = "/{,**/*/**/}*"
9
8
  REQUIRABLE_EXTENSIONS = [DOT_RB] + DL_EXTENSIONS
10
9
  NORMALIZE_NATIVE_EXTENSIONS = !DL_EXTENSIONS.include?(LoadPathCache::DOT_SO)
11
- ALTERNATIVE_NATIVE_EXTENSIONS_PATTERN = /\.(o|bundle|dylib)\z/
10
+ ALTERNATIVE_NATIVE_EXTENSIONS_PATTERN = /\.(o|bundle|dylib)\z/.freeze
12
11
 
13
12
  BUNDLE_PATH = if Bootsnap.bundler?
14
13
  (Bundler.bundle_path.cleanpath.to_s << LoadPathCache::SLASH).freeze
15
14
  else
16
- ''
15
+ ""
17
16
  end
18
17
 
19
- def self.call(path)
20
- path = path.to_s
21
-
22
- relative_slice = (path.size + 1)..-1
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
-
35
- Dir.glob(path + ALL_FILES).each do |absolute_path|
36
- next if contains_bundle_path && absolute_path.start_with?(BUNDLE_PATH)
37
- relative_path = absolute_path.slice(relative_slice)
38
-
39
- if File.directory?(absolute_path)
40
- dirs << relative_path
41
- elsif REQUIRABLE_EXTENSIONS.include?(File.extname(relative_path))
42
- requirables << relative_path
18
+ @ignored_directories = %w(node_modules)
19
+
20
+ class << self
21
+ attr_accessor :ignored_directories
22
+
23
+ def call(path)
24
+ path = File.expand_path(path.to_s).freeze
25
+ return [[], []] unless File.directory?(path)
26
+
27
+ # If the bundle path is a descendent of this path, we do additional
28
+ # checks to prevent recursing into the bundle path as we recurse
29
+ # through this path. We don't want to scan the bundle path because
30
+ # anything useful in it will be present on other load path items.
31
+ #
32
+ # This can happen if, for example, the user adds '.' to the load path,
33
+ # and the bundle path is '.bundle'.
34
+ contains_bundle_path = BUNDLE_PATH.start_with?(path)
35
+
36
+ dirs = []
37
+ requirables = []
38
+ walk(path, nil) do |relative_path, absolute_path, is_directory|
39
+ if is_directory
40
+ dirs << os_path(relative_path)
41
+ !contains_bundle_path || !absolute_path.start_with?(BUNDLE_PATH)
42
+ elsif relative_path.end_with?(*REQUIRABLE_EXTENSIONS)
43
+ requirables << os_path(relative_path)
44
+ end
43
45
  end
46
+ [requirables, dirs]
44
47
  end
45
48
 
46
- [requirables, dirs]
49
+ def walk(absolute_dir_path, relative_dir_path, &block)
50
+ Dir.foreach(absolute_dir_path) do |name|
51
+ next if name.start_with?(".")
52
+
53
+ relative_path = relative_dir_path ? File.join(relative_dir_path, name) : name
54
+
55
+ absolute_path = "#{absolute_dir_path}/#{name}"
56
+ if File.directory?(absolute_path)
57
+ next if ignored_directories.include?(name) || ignored_directories.include?(absolute_path)
58
+
59
+ if yield relative_path, absolute_path, true
60
+ walk(absolute_path, relative_path, &block)
61
+ end
62
+ else
63
+ yield relative_path, absolute_path, false
64
+ end
65
+ end
66
+ end
67
+
68
+ if RUBY_VERSION >= "3.1"
69
+ def os_path(path)
70
+ path.freeze
71
+ end
72
+ else
73
+ def os_path(path)
74
+ path.force_encoding(Encoding::US_ASCII) if path.ascii_only?
75
+ path.freeze
76
+ end
77
+ end
47
78
  end
48
79
  end
49
80
  end