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.
- checksums.yaml +5 -5
- data/.rubocop.yml +13 -0
- data/.travis.yml +11 -3
- data/CHANGELOG.md +43 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.jp.md +229 -0
- data/README.md +21 -25
- data/Rakefile +1 -0
- data/bin/testunit +3 -3
- data/bootsnap.gemspec +2 -2
- data/dev.yml +3 -1
- data/ext/bootsnap/bootsnap.c +124 -78
- data/lib/bootsnap.rb +1 -0
- data/lib/bootsnap/bundler.rb +12 -0
- data/lib/bootsnap/compile_cache/iseq.rb +0 -1
- data/lib/bootsnap/compile_cache/yaml.rb +1 -0
- data/lib/bootsnap/explicit_require.rb +6 -1
- data/lib/bootsnap/load_path_cache.rb +7 -1
- data/lib/bootsnap/load_path_cache/cache.rb +13 -15
- data/lib/bootsnap/load_path_cache/core_ext/active_support.rb +15 -13
- data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +77 -33
- data/lib/bootsnap/load_path_cache/loaded_features_index.rb +95 -0
- data/lib/bootsnap/load_path_cache/path.rb +1 -1
- data/lib/bootsnap/load_path_cache/path_scanner.rb +16 -6
- data/lib/bootsnap/load_path_cache/realpath_cache.rb +32 -0
- data/lib/bootsnap/load_path_cache/store.rb +9 -2
- data/lib/bootsnap/setup.rb +4 -10
- data/lib/bootsnap/version.rb +1 -1
- data/shipit.rubygems.yml +4 -0
- metadata +12 -6
- data/LICENSE +0 -20
data/lib/bootsnap.rb
CHANGED
@@ -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.
|
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
|
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
|
-
|
35
|
-
|
34
|
+
features[feat] = nil # enumerator.so
|
35
|
+
features[base] = nil # enumerator
|
36
36
|
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
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).
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
36
|
+
require_with_bootsnap_lfi(path)
|
25
37
|
end
|
26
38
|
|
27
|
-
alias_method :
|
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
|
-
|
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
|
-
|
62
|
+
load_without_bootsnap(path, wrap)
|
43
63
|
end
|
44
64
|
end
|
45
65
|
|
46
66
|
class << Kernel
|
47
|
-
alias_method :
|
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
|
-
|
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
|
-
|
86
|
+
require_with_bootsnap_lfi(path)
|
58
87
|
end
|
59
88
|
|
60
|
-
alias_method :
|
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
|
-
|
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
|
-
|
112
|
+
load_without_bootsnap(path, wrap)
|
76
113
|
end
|
77
114
|
end
|
78
115
|
|
79
116
|
class Module
|
80
|
-
alias_method :
|
117
|
+
alias_method :autoload_without_bootsnap, :autoload
|
81
118
|
def autoload(const, path)
|
82
|
-
|
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
|
-
|
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
|