bootsnap 1.4.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.github/CODEOWNERS +2 -0
  3. data/.github/probots.yml +2 -0
  4. data/.gitignore +17 -0
  5. data/.rubocop.yml +20 -0
  6. data/.travis.yml +21 -0
  7. data/CHANGELOG.md +122 -0
  8. data/CODE_OF_CONDUCT.md +74 -0
  9. data/CONTRIBUTING.md +21 -0
  10. data/Gemfile +9 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.jp.md +231 -0
  13. data/README.md +304 -0
  14. data/Rakefile +13 -0
  15. data/bin/ci +10 -0
  16. data/bin/console +15 -0
  17. data/bin/setup +8 -0
  18. data/bin/test-minimal-support +7 -0
  19. data/bin/testunit +8 -0
  20. data/bootsnap.gemspec +46 -0
  21. data/dev.yml +10 -0
  22. data/ext/bootsnap/bootsnap.c +829 -0
  23. data/ext/bootsnap/bootsnap.h +6 -0
  24. data/ext/bootsnap/extconf.rb +19 -0
  25. data/lib/bootsnap.rb +48 -0
  26. data/lib/bootsnap/bundler.rb +15 -0
  27. data/lib/bootsnap/compile_cache.rb +43 -0
  28. data/lib/bootsnap/compile_cache/iseq.rb +73 -0
  29. data/lib/bootsnap/compile_cache/yaml.rb +63 -0
  30. data/lib/bootsnap/explicit_require.rb +50 -0
  31. data/lib/bootsnap/load_path_cache.rb +78 -0
  32. data/lib/bootsnap/load_path_cache/cache.rb +208 -0
  33. data/lib/bootsnap/load_path_cache/change_observer.rb +63 -0
  34. data/lib/bootsnap/load_path_cache/core_ext/active_support.rb +107 -0
  35. data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +93 -0
  36. data/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb +18 -0
  37. data/lib/bootsnap/load_path_cache/loaded_features_index.rb +148 -0
  38. data/lib/bootsnap/load_path_cache/path.rb +114 -0
  39. data/lib/bootsnap/load_path_cache/path_scanner.rb +50 -0
  40. data/lib/bootsnap/load_path_cache/realpath_cache.rb +32 -0
  41. data/lib/bootsnap/load_path_cache/store.rb +90 -0
  42. data/lib/bootsnap/setup.rb +39 -0
  43. data/lib/bootsnap/version.rb +4 -0
  44. data/shipit.rubygems.yml +0 -0
  45. metadata +174 -0
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+ module Bootsnap
3
+ module LoadPathCache
4
+ module ChangeObserver
5
+ module ArrayMixin
6
+ # For each method that adds items to one end or another of the array
7
+ # (<<, push, unshift, concat), override that method to also notify the
8
+ # observer of the change.
9
+ def <<(entry)
10
+ @lpc_observer.push_paths(self, entry.to_s)
11
+ super
12
+ end
13
+
14
+ def push(*entries)
15
+ @lpc_observer.push_paths(self, *entries.map(&:to_s))
16
+ super
17
+ end
18
+
19
+ def unshift(*entries)
20
+ @lpc_observer.unshift_paths(self, *entries.map(&:to_s))
21
+ super
22
+ end
23
+
24
+ def concat(entries)
25
+ @lpc_observer.push_paths(self, *entries.map(&:to_s))
26
+ super
27
+ end
28
+
29
+ # uniq! keeps the first occurance of each path, otherwise preserving
30
+ # order, preserving the effective load path
31
+ def uniq!(*args)
32
+ ret = super
33
+ @lpc_observer.reinitialize if block_given? || !args.empty?
34
+ ret
35
+ end
36
+
37
+ # For each method that modifies the array more aggressively, override
38
+ # the method to also have the observer completely reconstruct its state
39
+ # after the modification. Many of these could be made to modify the
40
+ # internal state of the LoadPathCache::Cache more efficiently, but the
41
+ # accounting cost would be greater than the hit from these, since we
42
+ # actively discourage calling them.
43
+ %i(
44
+ []= clear collect! compact! delete delete_at delete_if fill flatten!
45
+ insert keep_if map! pop reject! replace reverse! rotate! select!
46
+ shift shuffle! slice! sort! sort_by!
47
+ ).each do |method_name|
48
+ define_method(method_name) do |*args, &block|
49
+ ret = super(*args, &block)
50
+ @lpc_observer.reinitialize
51
+ ret
52
+ end
53
+ end
54
+ end
55
+
56
+ def self.register(observer, arr)
57
+ return if arr.frozen? # can't register observer, but no need to.
58
+ arr.instance_variable_set(:@lpc_observer, observer)
59
+ arr.extend(ArrayMixin)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+ module Bootsnap
3
+ module LoadPathCache
4
+ module CoreExt
5
+ module ActiveSupport
6
+ def self.without_bootsnap_cache
7
+ prev = Thread.current[:without_bootsnap_cache] || false
8
+ Thread.current[:without_bootsnap_cache] = true
9
+ yield
10
+ ensure
11
+ Thread.current[:without_bootsnap_cache] = prev
12
+ end
13
+
14
+ def self.allow_bootsnap_retry(allowed)
15
+ prev = Thread.current[:without_bootsnap_retry] || false
16
+ Thread.current[:without_bootsnap_retry] = !allowed
17
+ yield
18
+ ensure
19
+ Thread.current[:without_bootsnap_retry] = prev
20
+ end
21
+
22
+ module ClassMethods
23
+ def autoload_paths=(o)
24
+ super
25
+ Bootsnap::LoadPathCache.autoload_paths_cache.reinitialize(o)
26
+ end
27
+
28
+ def search_for_file(path)
29
+ return super if Thread.current[:without_bootsnap_cache]
30
+ begin
31
+ Bootsnap::LoadPathCache.autoload_paths_cache.find(path)
32
+ rescue Bootsnap::LoadPathCache::ReturnFalse
33
+ nil # doesn't really apply here
34
+ rescue Bootsnap::LoadPathCache::FallbackScan
35
+ nil # doesn't really apply here
36
+ end
37
+ end
38
+
39
+ def autoloadable_module?(path_suffix)
40
+ Bootsnap::LoadPathCache.autoload_paths_cache.load_dir(path_suffix)
41
+ end
42
+
43
+ def remove_constant(const)
44
+ CoreExt::ActiveSupport.without_bootsnap_cache { super }
45
+ end
46
+
47
+ def require_or_load(*)
48
+ CoreExt::ActiveSupport.allow_bootsnap_retry(true) do
49
+ super
50
+ end
51
+ end
52
+
53
+ # If we can't find a constant using the patched implementation of
54
+ # search_for_file, try again with the default implementation.
55
+ #
56
+ # These methods call search_for_file, and we want to modify its
57
+ # behaviour. The gymnastics here are a bit awkward, but it prevents
58
+ # 200+ lines of monkeypatches.
59
+ def load_missing_constant(from_mod, const_name)
60
+ CoreExt::ActiveSupport.allow_bootsnap_retry(false) do
61
+ super
62
+ end
63
+ rescue NameError => e
64
+ raise(e) if e.instance_variable_defined?(Bootsnap::LoadPathCache::ERROR_TAG_IVAR)
65
+ e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
66
+
67
+ # This function can end up called recursively, we only want to
68
+ # retry at the top-level.
69
+ raise(e) if Thread.current[:without_bootsnap_retry]
70
+ # If we already had cache disabled, there's no use retrying
71
+ raise(e) if Thread.current[:without_bootsnap_cache]
72
+ # NoMethodError is a NameError, but we only want to handle actual
73
+ # NameError instances.
74
+ raise(e) unless e.class == NameError
75
+ # We can only confidently handle cases when *this* constant fails
76
+ # to load, not other constants referred to by it.
77
+ raise(e) unless e.name == const_name
78
+ # If the constant was actually loaded, something else went wrong?
79
+ raise(e) if from_mod.const_defined?(const_name)
80
+ CoreExt::ActiveSupport.without_bootsnap_cache { super }
81
+ end
82
+
83
+ # Signature has changed a few times over the years; easiest to not
84
+ # reiterate it with version polymorphism here...
85
+ def depend_on(*)
86
+ super
87
+ rescue LoadError => e
88
+ raise(e) if e.instance_variable_defined?(Bootsnap::LoadPathCache::ERROR_TAG_IVAR)
89
+ e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
90
+
91
+ # If we already had cache disabled, there's no use retrying
92
+ raise(e) if Thread.current[:without_bootsnap_cache]
93
+ CoreExt::ActiveSupport.without_bootsnap_cache { super }
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ module ActiveSupport
102
+ module Dependencies
103
+ class << self
104
+ prepend(Bootsnap::LoadPathCache::CoreExt::ActiveSupport::ClassMethods)
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,93 @@
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
+
15
+ module Kernel
16
+ module_function # rubocop:disable Style/ModuleFunction
17
+
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)
24
+ end
25
+ end
26
+
27
+ 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
+ require_with_bootsnap_lfi(path)
42
+ end
43
+
44
+ alias_method(:require_relative_without_bootsnap, :require_relative)
45
+ def require_relative(path)
46
+ realpath = Bootsnap::LoadPathCache.realpath_cache.call(
47
+ caller_locations(1..1).first.absolute_path, path
48
+ )
49
+ require(realpath)
50
+ end
51
+
52
+ alias_method(:load_without_bootsnap, :load)
53
+ def load(path, wrap = false)
54
+ if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path))
55
+ return load_without_bootsnap(resolved, wrap)
56
+ end
57
+
58
+ # load also allows relative paths from pwd even when not in $:
59
+ if File.exist?(relative = File.expand_path(path))
60
+ return load_without_bootsnap(relative, wrap)
61
+ end
62
+
63
+ raise(Bootsnap::LoadPathCache::CoreExt.make_load_error(path))
64
+ rescue LoadError => e
65
+ e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
66
+ raise(e)
67
+ rescue Bootsnap::LoadPathCache::ReturnFalse
68
+ false
69
+ rescue Bootsnap::LoadPathCache::FallbackScan
70
+ load_without_bootsnap(path, wrap)
71
+ end
72
+ end
73
+
74
+ class Module
75
+ alias_method(:autoload_without_bootsnap, :autoload)
76
+ def autoload(const, path)
77
+ # NOTE: This may defeat LoadedFeaturesIndex, but it's not immediately
78
+ # obvious how to make it work. This feels like a pretty niche case, unclear
79
+ # if it will ever burn anyone.
80
+ #
81
+ # The challenge is that we don't control the point at which the entry gets
82
+ # added to $LOADED_FEATURES and won't be able to hook that modification
83
+ # since it's done in C-land.
84
+ autoload_without_bootsnap(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
85
+ rescue LoadError => e
86
+ e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
87
+ raise(e)
88
+ rescue Bootsnap::LoadPathCache::ReturnFalse
89
+ false
90
+ rescue Bootsnap::LoadPathCache::FallbackScan
91
+ autoload_without_bootsnap(const, path)
92
+ end
93
+ 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))
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 callling 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
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+ require_relative('path_scanner')
3
+
4
+ module Bootsnap
5
+ module LoadPathCache
6
+ class Path
7
+ # A path is considered 'stable' if it is part of a Gem.path or the ruby
8
+ # distribution. When adding or removing files in these paths, the cache
9
+ # must be cleared before the change will be noticed.
10
+ def stable?
11
+ stability == STABLE
12
+ end
13
+
14
+ # A path is considered volatile if it doesn't live under a Gem.path or
15
+ # the ruby distribution root. These paths are scanned for new additions
16
+ # more frequently.
17
+ def volatile?
18
+ stability == VOLATILE
19
+ end
20
+
21
+ attr_reader(:path)
22
+
23
+ def initialize(path)
24
+ @path = path.to_s
25
+ end
26
+
27
+ # True if the path exists, but represents a non-directory object
28
+ def non_directory?
29
+ !File.stat(path).directory?
30
+ rescue Errno::ENOENT, Errno::ENOTDIR
31
+ false
32
+ end
33
+
34
+ def relative?
35
+ !path.start_with?(SLASH)
36
+ end
37
+
38
+ # Return a list of all the requirable files and all of the subdirectories
39
+ # of this +Path+.
40
+ def entries_and_dirs(store)
41
+ if stable?
42
+ # the cached_mtime field is unused for 'stable' paths, but is
43
+ # set to zero anyway, just in case we change the stability heuristics.
44
+ _, entries, dirs = store.get(expanded_path)
45
+ return [entries, dirs] if entries # cache hit
46
+ entries, dirs = scan!
47
+ store.set(expanded_path, [0, entries, dirs])
48
+ return [entries, dirs]
49
+ end
50
+
51
+ cached_mtime, entries, dirs = store.get(expanded_path)
52
+
53
+ current_mtime = latest_mtime(expanded_path, dirs || [])
54
+ return [[], []] if current_mtime == -1 # path does not exist
55
+ return [entries, dirs] if cached_mtime == current_mtime
56
+
57
+ entries, dirs = scan!
58
+ store.set(expanded_path, [current_mtime, entries, dirs])
59
+ [entries, dirs]
60
+ end
61
+
62
+ def expanded_path
63
+ File.expand_path(path)
64
+ end
65
+
66
+ private
67
+
68
+ def scan! # (expensive) returns [entries, dirs]
69
+ PathScanner.call(expanded_path)
70
+ end
71
+
72
+ # last time a directory was modified in this subtree. +dirs+ should be a
73
+ # list of relative paths to directories under +path+. e.g. for /a/b and
74
+ # /a/b/c, pass ('/a/b', ['c'])
75
+ def latest_mtime(path, dirs)
76
+ max = -1
77
+ ["", *dirs].each do |dir|
78
+ curr = begin
79
+ File.mtime("#{path}/#{dir}").to_i
80
+ rescue Errno::ENOENT, Errno::ENOTDIR
81
+ -1
82
+ end
83
+ max = curr if curr > max
84
+ end
85
+ max
86
+ end
87
+
88
+ # a Path can be either stable of volatile, depending on how frequently we
89
+ # expect its contents may change. Stable paths aren't rescanned nearly as
90
+ # often.
91
+ STABLE = :stable
92
+ VOLATILE = :volatile
93
+
94
+ # 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']
98
+
99
+ 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
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end