bootsnap 1.1.1 → 1.3.0

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.
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