bootsnap 1.1.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/bootsnap.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require_relative 'bootsnap/version'
2
+ require_relative 'bootsnap/bundler'
2
3
  require_relative 'bootsnap/load_path_cache'
3
4
  require_relative 'bootsnap/compile_cache'
4
5
 
@@ -0,0 +1,12 @@
1
+ module Bootsnap
2
+ module_function
3
+
4
+ def bundler?
5
+ # Bundler environment variable
6
+ ['BUNDLE_BIN_PATH', 'BUNDLE_GEMFILE'].each do |current|
7
+ return true if ENV.key?(current)
8
+ end
9
+
10
+ false
11
+ end
12
+ end
@@ -68,4 +68,3 @@ module Bootsnap
68
68
  end
69
69
  end
70
70
  end
71
-
@@ -8,6 +8,7 @@ module Bootsnap
8
8
  end
9
9
 
10
10
  def self.input_to_storage(contents, _)
11
+ raise Uncompilable if contents.index("!ruby/object")
11
12
  obj = ::YAML.load(contents)
12
13
  msgpack_factory.packer.write(obj).to_s
13
14
  rescue NoMethodError, RangeError
@@ -36,7 +36,12 @@ module Bootsnap
36
36
  end
37
37
  $LOAD_PATH << ARCHDIR
38
38
  $LOAD_PATH << RUBYLIBDIR
39
- yield
39
+ begin
40
+ yield
41
+ rescue LoadError
42
+ $LOAD_PATH.replace(orig)
43
+ yield
44
+ end
40
45
  ensure
41
46
  $LOAD_PATH.replace(orig)
42
47
  end
@@ -21,11 +21,15 @@ module Bootsnap
21
21
  CACHED_EXTENSIONS = DLEXT2 ? [DOT_RB, DLEXT, DLEXT2] : [DOT_RB, DLEXT]
22
22
 
23
23
  class << self
24
- attr_reader :load_path_cache, :autoload_paths_cache
24
+ attr_reader :load_path_cache, :autoload_paths_cache,
25
+ :loaded_features_index, :realpath_cache
25
26
 
26
27
  def setup(cache_path:, development_mode:, active_support: true)
27
28
  store = Store.new(cache_path)
28
29
 
30
+ @loaded_features_index = LoadedFeaturesIndex.new
31
+ @realpath_cache = RealpathCache.new
32
+
29
33
  @load_path_cache = Cache.new(store, $LOAD_PATH, development_mode: development_mode)
30
34
  require_relative 'load_path_cache/core_ext/kernel_require'
31
35
 
@@ -50,3 +54,5 @@ require_relative 'load_path_cache/path'
50
54
  require_relative 'load_path_cache/cache'
51
55
  require_relative 'load_path_cache/store'
52
56
  require_relative 'load_path_cache/change_observer'
57
+ require_relative 'load_path_cache/loaded_features_index'
58
+ require_relative 'load_path_cache/realpath_cache'
@@ -1,4 +1,3 @@
1
- require_relative '../load_path_cache'
2
1
  require_relative '../explicit_require'
3
2
 
4
3
  module Bootsnap
@@ -9,8 +8,9 @@ module Bootsnap
9
8
  def initialize(store, path_obj, development_mode: false)
10
9
  @development_mode = development_mode
11
10
  @store = store
12
- @mutex = ::Thread::Mutex.new
13
- @path_obj = path_obj
11
+ @mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new # TODO: Remove once Ruby 2.2 support is dropped.
12
+ @path_obj = path_obj.map! { |f| File.exist?(f) ? File.realpath(f) : f }
13
+ @has_relative_paths = nil
14
14
  reinitialize
15
15
  end
16
16
 
@@ -23,24 +23,21 @@ module Bootsnap
23
23
  end
24
24
 
25
25
  # { 'enumerator' => nil, 'enumerator.so' => nil, ... }
26
- BUILTIN_FEATURES = $LOADED_FEATURES.reduce({}) do |acc, feat|
26
+ BUILTIN_FEATURES = $LOADED_FEATURES.each_with_object({}) do |feat, features|
27
27
  # Builtin features are of the form 'enumerator.so'.
28
28
  # All others include paths.
29
- next acc unless feat.size < 20 && !feat.include?('/')
29
+ next unless feat.size < 20 && !feat.include?('/')
30
30
 
31
31
  base = File.basename(feat, '.*') # enumerator.so -> enumerator
32
32
  ext = File.extname(feat) # .so
33
33
 
34
- acc[feat] = nil # enumerator.so
35
- acc[base] = nil # enumerator
34
+ features[feat] = nil # enumerator.so
35
+ features[base] = nil # enumerator
36
36
 
37
- if [DOT_SO, *DL_EXTENSIONS].include?(ext)
38
- DL_EXTENSIONS.each do |ext|
39
- acc["#{base}#{ext}"] = nil # enumerator.bundle
40
- end
37
+ next unless [DOT_SO, *DL_EXTENSIONS].include?(ext)
38
+ DL_EXTENSIONS.each do |dl_ext|
39
+ features["#{base}#{dl_ext}"] = nil # enumerator.bundle
41
40
  end
42
-
43
- acc
44
41
  end.freeze
45
42
 
46
43
  # Try to resolve this feature to an absolute path without traversing the
@@ -67,7 +64,8 @@ module Bootsnap
67
64
  # If the extension was one of the ones we explicitly cache (.rb and the
68
65
  # native dynamic extension, e.g. .bundle or .so), we know it was a
69
66
  # failure and there's nothing more we can do to find the file.
70
- when '', *CACHED_EXTENSIONS # no extension, .rb, (.bundle or .so)
67
+ # no extension, .rb, (.bundle or .so)
68
+ when '', *CACHED_EXTENSIONS # rubocop:disable Performance/CaseWhenSplat
71
69
  nil
72
70
  # Ruby allows specifying native extensions as '.so' even when DLEXT
73
71
  # is '.bundle'. This is where we handle that case.
@@ -152,7 +150,7 @@ module Bootsnap
152
150
 
153
151
  def unshift_paths_locked(*paths)
154
152
  @store.transaction do
155
- paths.map(&:to_s).reverse.each do |path|
153
+ paths.map(&:to_s).reverse_each do |path|
156
154
  p = Path.new(path)
157
155
  next if p.non_directory?
158
156
  entries, dirs = p.entries_and_dirs(@store)
@@ -2,15 +2,6 @@ module Bootsnap
2
2
  module LoadPathCache
3
3
  module CoreExt
4
4
  module ActiveSupport
5
- def self.with_bootsnap_fallback(error)
6
- yield
7
- rescue error => e
8
- # NoMethodError is a NameError, but we only want to handle actual
9
- # NameError instances.
10
- raise unless e.class == error
11
- without_bootsnap_cache { yield }
12
- end
13
-
14
5
  def self.without_bootsnap_cache
15
6
  prev = Thread.current[:without_bootsnap_cache] || false
16
7
  Thread.current[:without_bootsnap_cache] = true
@@ -21,9 +12,8 @@ module Bootsnap
21
12
 
22
13
  module ClassMethods
23
14
  def autoload_paths=(o)
24
- r = super
15
+ super
25
16
  Bootsnap::LoadPathCache.autoload_paths_cache.reinitialize(o)
26
- r
27
17
  end
28
18
 
29
19
  def search_for_file(path)
@@ -50,13 +40,25 @@ module Bootsnap
50
40
  # behaviour. The gymnastics here are a bit awkward, but it prevents
51
41
  # 200+ lines of monkeypatches.
52
42
  def load_missing_constant(from_mod, const_name)
53
- CoreExt::ActiveSupport.with_bootsnap_fallback(NameError) { super }
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
54
  end
55
55
 
56
56
  # Signature has changed a few times over the years; easiest to not
57
57
  # reiterate it with version polymorphism here...
58
58
  def depend_on(*)
59
- CoreExt::ActiveSupport.with_bootsnap_fallback(LoadError) { super }
59
+ super
60
+ rescue LoadError
61
+ CoreExt::ActiveSupport.without_bootsnap_cache { super }
60
62
  end
61
63
  end
62
64
  end
@@ -11,78 +11,122 @@ module Bootsnap
11
11
  end
12
12
 
13
13
  module Kernel
14
- alias_method :require_without_cache, :require
14
+ private
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
+
15
25
  def require(path)
26
+ return false if Bootsnap::LoadPathCache.loaded_features_index.key?(path)
27
+
16
28
  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)
29
+ return require_with_bootsnap_lfi(path, resolved)
20
30
  end
31
+
32
+ raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
21
33
  rescue Bootsnap::LoadPathCache::ReturnFalse
22
34
  return false
23
35
  rescue Bootsnap::LoadPathCache::FallbackScan
24
- require_without_cache(path)
36
+ require_with_bootsnap_lfi(path)
25
37
  end
26
38
 
27
- alias_method :load_without_cache, :load
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
28
48
  def load(path, wrap = false)
29
49
  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)
50
+ return load_without_bootsnap(resolved, wrap)
38
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)
39
59
  rescue Bootsnap::LoadPathCache::ReturnFalse
40
60
  return false
41
61
  rescue Bootsnap::LoadPathCache::FallbackScan
42
- load_without_cache(path, wrap)
62
+ load_without_bootsnap(path, wrap)
43
63
  end
44
64
  end
45
65
 
46
66
  class << Kernel
47
- alias_method :require_without_cache, :require
67
+ alias_method :require_without_bootsnap, :require
68
+
69
+ def require_with_bootsnap_lfi(path, resolved = nil)
70
+ Bootsnap::LoadPathCache.loaded_features_index.register(path, resolved) do
71
+ require_without_bootsnap(resolved || path)
72
+ end
73
+ end
74
+
48
75
  def require(path)
76
+ return false if Bootsnap::LoadPathCache.loaded_features_index.key?(path)
77
+
49
78
  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)
79
+ return require_with_bootsnap_lfi(path, resolved)
53
80
  end
81
+
82
+ raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
54
83
  rescue Bootsnap::LoadPathCache::ReturnFalse
55
84
  return false
56
85
  rescue Bootsnap::LoadPathCache::FallbackScan
57
- require_without_cache(path)
86
+ require_with_bootsnap_lfi(path)
58
87
  end
59
88
 
60
- alias_method :load_without_cache, :load
89
+ alias_method :require_relative_without_bootsnap, :require_relative
90
+ def require_relative(path)
91
+ realpath = Bootsnap::LoadPathCache.realpath_cache.call(
92
+ caller_locations(1..1).first.absolute_path, path
93
+ )
94
+ require(realpath)
95
+ end
96
+
97
+ alias_method :load_without_bootsnap, :load
61
98
  def load(path, wrap = false)
62
99
  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)
100
+ return load_without_bootsnap(resolved, wrap)
71
101
  end
102
+
103
+ # load also allows relative paths from pwd even when not in $:
104
+ if File.exist?(relative = File.expand_path(path))
105
+ return load_without_bootsnap(relative, wrap)
106
+ end
107
+
108
+ raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
72
109
  rescue Bootsnap::LoadPathCache::ReturnFalse
73
110
  return false
74
111
  rescue Bootsnap::LoadPathCache::FallbackScan
75
- load_without_cache(path, wrap)
112
+ load_without_bootsnap(path, wrap)
76
113
  end
77
114
  end
78
115
 
79
116
  class Module
80
- alias_method :autoload_without_cache, :autoload
117
+ alias_method :autoload_without_bootsnap, :autoload
81
118
  def autoload(const, path)
82
- autoload_without_cache(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
119
+ # NOTE: This may defeat LoadedFeaturesIndex, but it's not immediately
120
+ # obvious how to make it work. This feels like a pretty niche case, unclear
121
+ # if it will ever burn anyone.
122
+ #
123
+ # The challenge is that we don't control the point at which the entry gets
124
+ # added to $LOADED_FEATURES and won't be able to hook that modification
125
+ # since it's done in C-land.
126
+ autoload_without_bootsnap(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
83
127
  rescue Bootsnap::LoadPathCache::ReturnFalse
84
128
  return false
85
129
  rescue Bootsnap::LoadPathCache::FallbackScan
86
- autoload_without_cache(const, path)
130
+ autoload_without_bootsnap(const, path)
87
131
  end
88
132
  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