bootsnap-pr-184 1.3.1.pr.pre.184.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,53 @@
1
+ module Bootsnap
2
+ module LoadPathCache
3
+ module ChangeObserver
4
+ module ArrayMixin
5
+ # For each method that adds items to one end or another of the array
6
+ # (<<, push, unshift, concat), override that method to also notify the
7
+ # observer of the change.
8
+ def <<(entry)
9
+ @lpc_observer.push_paths(self, entry.to_s)
10
+ super
11
+ end
12
+
13
+ def push(*entries)
14
+ @lpc_observer.push_paths(self, *entries.map(&:to_s))
15
+ super
16
+ end
17
+
18
+ def unshift(*entries)
19
+ @lpc_observer.unshift_paths(self, *entries.map(&:to_s))
20
+ super
21
+ end
22
+
23
+ def concat(entries)
24
+ @lpc_observer.push_paths(self, *entries.map(&:to_s))
25
+ super
26
+ end
27
+
28
+ # For each method that modifies the array more aggressively, override
29
+ # the method to also have the observer completely reconstruct its state
30
+ # after the modification. Many of these could be made to modify the
31
+ # internal state of the LoadPathCache::Cache more efficiently, but the
32
+ # accounting cost would be greater than the hit from these, since we
33
+ # actively discourage calling them.
34
+ %i(
35
+ []= clear collect! compact! delete delete_at delete_if fill flatten!
36
+ insert keep_if map! pop reject! replace reverse! rotate! select!
37
+ shift shuffle! slice! sort! sort_by! uniq!
38
+ ).each do |method_name|
39
+ define_method(method_name) do |*args, &block|
40
+ ret = super(*args, &block)
41
+ @lpc_observer.reinitialize
42
+ ret
43
+ end
44
+ end
45
+ end
46
+
47
+ def self.register(observer, arr)
48
+ arr.instance_variable_set(:@lpc_observer, observer)
49
+ arr.extend(ArrayMixin)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,75 @@
1
+ module Bootsnap
2
+ module LoadPathCache
3
+ module CoreExt
4
+ module ActiveSupport
5
+ def self.without_bootsnap_cache
6
+ prev = Thread.current[:without_bootsnap_cache] || false
7
+ Thread.current[:without_bootsnap_cache] = true
8
+ yield
9
+ ensure
10
+ Thread.current[:without_bootsnap_cache] = prev
11
+ end
12
+
13
+ module ClassMethods
14
+ def autoload_paths=(o)
15
+ super
16
+ Bootsnap::LoadPathCache.autoload_paths_cache.reinitialize(o)
17
+ end
18
+
19
+ def search_for_file(path)
20
+ return super if Thread.current[:without_bootsnap_cache]
21
+ begin
22
+ Bootsnap::LoadPathCache.autoload_paths_cache.find(path)
23
+ rescue Bootsnap::LoadPathCache::ReturnFalse
24
+ nil # doesn't really apply here
25
+ end
26
+ end
27
+
28
+ def autoloadable_module?(path_suffix)
29
+ Bootsnap::LoadPathCache.autoload_paths_cache.has_dir?(path_suffix)
30
+ end
31
+
32
+ def remove_constant(const)
33
+ CoreExt::ActiveSupport.without_bootsnap_cache { super }
34
+ end
35
+
36
+ # If we can't find a constant using the patched implementation of
37
+ # search_for_file, try again with the default implementation.
38
+ #
39
+ # These methods call search_for_file, and we want to modify its
40
+ # behaviour. The gymnastics here are a bit awkward, but it prevents
41
+ # 200+ lines of monkeypatches.
42
+ def load_missing_constant(from_mod, const_name)
43
+ super
44
+ rescue NameError => e
45
+ # NoMethodError is a NameError, but we only want to handle actual
46
+ # NameError instances.
47
+ raise unless e.class == NameError
48
+ # We can only confidently handle cases when *this* constant fails
49
+ # to load, not other constants referred to by it.
50
+ raise unless e.name == const_name
51
+ # If the constant was actually loaded, something else went wrong?
52
+ raise if from_mod.const_defined?(const_name)
53
+ CoreExt::ActiveSupport.without_bootsnap_cache { super }
54
+ end
55
+
56
+ # Signature has changed a few times over the years; easiest to not
57
+ # reiterate it with version polymorphism here...
58
+ def depend_on(*)
59
+ super
60
+ rescue LoadError
61
+ CoreExt::ActiveSupport.without_bootsnap_cache { super }
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ module ActiveSupport
70
+ module Dependencies
71
+ class << self
72
+ prepend Bootsnap::LoadPathCache::CoreExt::ActiveSupport::ClassMethods
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,82 @@
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.define_singleton_method(:path) { path }
7
+ err
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ module Kernel
14
+ module_function
15
+
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)
22
+ end
23
+ end
24
+
25
+ def require(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)
30
+ end
31
+
32
+ raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
33
+ rescue Bootsnap::LoadPathCache::ReturnFalse
34
+ return false
35
+ rescue Bootsnap::LoadPathCache::FallbackScan
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)
45
+ end
46
+
47
+ alias_method :load_without_bootsnap, :load
48
+ def load(path, wrap = false)
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)
56
+ end
57
+
58
+ raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
59
+ rescue Bootsnap::LoadPathCache::ReturnFalse
60
+ return false
61
+ rescue Bootsnap::LoadPathCache::FallbackScan
62
+ load_without_bootsnap(path, wrap)
63
+ end
64
+ end
65
+
66
+ class Module
67
+ alias_method :autoload_without_bootsnap, :autoload
68
+ def autoload(const, 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)
77
+ rescue Bootsnap::LoadPathCache::ReturnFalse
78
+ return false
79
+ rescue Bootsnap::LoadPathCache::FallbackScan
80
+ autoload_without_bootsnap(const, path)
81
+ end
82
+ end
@@ -0,0 +1,95 @@
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
+ $LOAD_PATH.each do |lpe|
36
+ next unless feat.start_with?(lpe)
37
+ # /a/b/lib/my/foo.rb
38
+ # ^^^^^^^^^
39
+ short = feat[(lpe.length + 1)..-1]
40
+ @lfi[short] = true
41
+ @lfi[strip_extension(short)] = true
42
+ end
43
+ end
44
+ end
45
+
46
+ def key?(feature)
47
+ @mutex.synchronize { @lfi.key?(feature) }
48
+ end
49
+
50
+ # There is a relatively uncommon case where we could miss adding an
51
+ # entry:
52
+ #
53
+ # If the user asked for e.g. `require 'bundler'`, and we went through the
54
+ # `FallbackScan` pathway in `kernel_require.rb` and therefore did not
55
+ # pass `long` (the full expanded absolute path), then we did are not able
56
+ # to confidently add the `bundler.rb` form to @lfi.
57
+ #
58
+ # We could either:
59
+ #
60
+ # 1. Just add `bundler.rb`, `bundler.so`, and so on, which is close but
61
+ # not quite right; or
62
+ # 2. Inspect $LOADED_FEATURES upon return from yield to find the matching
63
+ # entry.
64
+ def register(short, long = nil)
65
+ ret = yield
66
+
67
+ # do we have 'bundler' or 'bundler.rb'?
68
+ altname = if File.extname(short) != ''
69
+ # strip the path from 'bundler.rb' -> 'bundler'
70
+ strip_extension(short)
71
+ elsif long && ext = File.extname(long)
72
+ # get the extension from the expanded path if given
73
+ # 'bundler' + '.rb'
74
+ short + ext
75
+ end
76
+
77
+ @mutex.synchronize do
78
+ @lfi[short] = true
79
+ (@lfi[altname] = true) if altname
80
+ end
81
+
82
+ ret
83
+ end
84
+
85
+ private
86
+
87
+ STRIP_EXTENSION = /\.[^.]*?$/
88
+ private_constant :STRIP_EXTENSION
89
+
90
+ def strip_extension(f)
91
+ f.sub(STRIP_EXTENSION, '')
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,113 @@
1
+ require_relative 'path_scanner'
2
+
3
+ module Bootsnap
4
+ module LoadPathCache
5
+ class Path
6
+ # A path is considered 'stable' if it is part of a Gem.path or the ruby
7
+ # distribution. When adding or removing files in these paths, the cache
8
+ # must be cleared before the change will be noticed.
9
+ def stable?
10
+ stability == STABLE
11
+ end
12
+
13
+ # A path is considered volatile if it doesn't live under a Gem.path or
14
+ # the ruby distribution root. These paths are scanned for new additions
15
+ # more frequently.
16
+ def volatile?
17
+ stability == VOLATILE
18
+ end
19
+
20
+ attr_reader :path
21
+
22
+ def initialize(path)
23
+ @path = path.to_s
24
+ end
25
+
26
+ # True if the path exists, but represents a non-directory object
27
+ def non_directory?
28
+ !File.stat(path).directory?
29
+ rescue Errno::ENOENT
30
+ false
31
+ end
32
+
33
+ def relative?
34
+ !path.start_with?(SLASH)
35
+ end
36
+
37
+ # Return a list of all the requirable files and all of the subdirectories
38
+ # of this +Path+.
39
+ def entries_and_dirs(store)
40
+ if stable?
41
+ # the cached_mtime field is unused for 'stable' paths, but is
42
+ # set to zero anyway, just in case we change the stability heuristics.
43
+ _, entries, dirs = store.get(expanded_path)
44
+ return [entries, dirs] if entries # cache hit
45
+ entries, dirs = scan!
46
+ store.set(expanded_path, [0, entries, dirs])
47
+ return [entries, dirs]
48
+ end
49
+
50
+ cached_mtime, entries, dirs = store.get(expanded_path)
51
+
52
+ current_mtime = latest_mtime(expanded_path, dirs || [])
53
+ return [[], []] if current_mtime == -1 # path does not exist
54
+ return [entries, dirs] if cached_mtime == current_mtime
55
+
56
+ entries, dirs = scan!
57
+ store.set(expanded_path, [current_mtime, entries, dirs])
58
+ [entries, dirs]
59
+ end
60
+
61
+ def expanded_path
62
+ File.expand_path(path)
63
+ end
64
+
65
+ private
66
+
67
+ def scan! # (expensive) returns [entries, dirs]
68
+ PathScanner.call(expanded_path)
69
+ end
70
+
71
+ # last time a directory was modified in this subtree. +dirs+ should be a
72
+ # list of relative paths to directories under +path+. e.g. for /a/b and
73
+ # /a/b/c, pass ('/a/b', ['c'])
74
+ def latest_mtime(path, dirs)
75
+ max = -1
76
+ ["", *dirs].each do |dir|
77
+ curr = begin
78
+ File.mtime("#{path}/#{dir}").to_i
79
+ rescue Errno::ENOENT
80
+ -1
81
+ end
82
+ max = curr if curr > max
83
+ end
84
+ max
85
+ end
86
+
87
+ # a Path can be either stable of volatile, depending on how frequently we
88
+ # expect its contents may change. Stable paths aren't rescanned nearly as
89
+ # often.
90
+ STABLE = :stable
91
+ VOLATILE = :volatile
92
+
93
+ # 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']
97
+
98
+ 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
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,44 @@
1
+ require_relative '../explicit_require'
2
+
3
+ module Bootsnap
4
+ module LoadPathCache
5
+ module PathScanner
6
+ ALL_FILES = "/{,**/*/**/}*"
7
+ REQUIRABLE_EXTENSIONS = [DOT_RB] + DL_EXTENSIONS
8
+ NORMALIZE_NATIVE_EXTENSIONS = !DL_EXTENSIONS.include?(LoadPathCache::DOT_SO)
9
+ ALTERNATIVE_NATIVE_EXTENSIONS_PATTERN = /\.(o|bundle|dylib)\z/
10
+ BUNDLE_PATH = Bootsnap.bundler? ?
11
+ (Bundler.bundle_path.cleanpath.to_s << LoadPathCache::SLASH).freeze : ''.freeze
12
+
13
+ def self.call(path)
14
+ path = path.to_s
15
+
16
+ relative_slice = (path.size + 1)..-1
17
+ # If the bundle path is a descendent of this path, we do additional
18
+ # checks to prevent recursing into the bundle path as we recurse
19
+ # through this path. We don't want to scan the bundle path because
20
+ # anything useful in it will be present on other load path items.
21
+ #
22
+ # This can happen if, for example, the user adds '.' to the load path,
23
+ # and the bundle path is '.bundle'.
24
+ contains_bundle_path = BUNDLE_PATH.start_with?(path)
25
+
26
+ dirs = []
27
+ requirables = []
28
+
29
+ Dir.glob(path + ALL_FILES).each do |absolute_path|
30
+ next if contains_bundle_path && absolute_path.start_with?(BUNDLE_PATH)
31
+ relative_path = absolute_path.slice(relative_slice)
32
+
33
+ if File.directory?(absolute_path)
34
+ dirs << relative_path
35
+ elsif REQUIRABLE_EXTENSIONS.include?(File.extname(relative_path))
36
+ requirables << relative_path
37
+ end
38
+ end
39
+
40
+ [requirables, dirs]
41
+ end
42
+ end
43
+ end
44
+ end