bootsnap 1.1.0-java
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rubocop.yml +7 -0
- data/.travis.yml +5 -0
- data/CHANGELOG.md +31 -0
- data/CONTRIBUTING.md +21 -0
- data/Gemfile +4 -0
- data/LICENSE +20 -0
- data/README.md +284 -0
- data/Rakefile +11 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/bin/testunit +8 -0
- data/bootsnap.gemspec +39 -0
- data/dev.yml +8 -0
- data/ext/bootsnap/bootsnap.c +742 -0
- data/ext/bootsnap/bootsnap.h +6 -0
- data/ext/bootsnap/extconf.rb +17 -0
- data/lib/bootsnap.rb +39 -0
- data/lib/bootsnap/compile_cache.rb +15 -0
- data/lib/bootsnap/compile_cache/iseq.rb +71 -0
- data/lib/bootsnap/compile_cache/yaml.rb +57 -0
- data/lib/bootsnap/explicit_require.rb +44 -0
- data/lib/bootsnap/load_path_cache.rb +52 -0
- data/lib/bootsnap/load_path_cache/cache.rb +191 -0
- data/lib/bootsnap/load_path_cache/change_observer.rb +56 -0
- data/lib/bootsnap/load_path_cache/core_ext/active_support.rb +73 -0
- data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +88 -0
- data/lib/bootsnap/load_path_cache/path.rb +113 -0
- data/lib/bootsnap/load_path_cache/path_scanner.rb +42 -0
- data/lib/bootsnap/load_path_cache/store.rb +77 -0
- data/lib/bootsnap/setup.rb +47 -0
- data/lib/bootsnap/version.rb +3 -0
- metadata +160 -0
@@ -0,0 +1,56 @@
|
|
1
|
+
module Bootsnap
|
2
|
+
module LoadPathCache
|
3
|
+
module ChangeObserver
|
4
|
+
def self.register(observer, arr)
|
5
|
+
# Re-overriding these methods on an array that already has them would
|
6
|
+
# cause StackOverflowErrors
|
7
|
+
return if arr.respond_to?(:push_without_lpc)
|
8
|
+
|
9
|
+
# For each method that adds items to one end or another of the array
|
10
|
+
# (<<, push, unshift, concat), override that method to also notify the
|
11
|
+
# observer of the change.
|
12
|
+
sc = arr.singleton_class
|
13
|
+
sc.send(:alias_method, :shovel_without_lpc, :<<)
|
14
|
+
arr.define_singleton_method(:<<) do |entry|
|
15
|
+
observer.push_paths(self, entry.to_s)
|
16
|
+
shovel_without_lpc(entry)
|
17
|
+
end
|
18
|
+
|
19
|
+
sc.send(:alias_method, :push_without_lpc, :push)
|
20
|
+
arr.define_singleton_method(:push) do |*entries|
|
21
|
+
observer.push_paths(self, *entries.map(&:to_s))
|
22
|
+
push_without_lpc(*entries)
|
23
|
+
end
|
24
|
+
|
25
|
+
sc.send(:alias_method, :unshift_without_lpc, :unshift)
|
26
|
+
arr.define_singleton_method(:unshift) do |*entries|
|
27
|
+
observer.unshift_paths(self, *entries.map(&:to_s))
|
28
|
+
unshift_without_lpc(*entries)
|
29
|
+
end
|
30
|
+
|
31
|
+
sc.send(:alias_method, :concat_without_lpc, :concat)
|
32
|
+
arr.define_singleton_method(:concat) do |entries|
|
33
|
+
observer.push_paths(self, *entries.map(&:to_s))
|
34
|
+
concat_without_lpc(entries)
|
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
|
+
collect! compact! delete delete_at delete_if fill flatten! insert map!
|
45
|
+
reject! reverse! select! shuffle! shift slice! sort! sort_by!
|
46
|
+
).each do |meth|
|
47
|
+
sc.send(:alias_method, :"#{meth}_without_lpc", meth)
|
48
|
+
arr.define_singleton_method(meth) do |*a|
|
49
|
+
send(:"#{meth}_without_lpc", *a)
|
50
|
+
observer.reinitialize
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Bootsnap
|
2
|
+
module LoadPathCache
|
3
|
+
module CoreExt
|
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
|
+
def self.without_bootsnap_cache
|
15
|
+
prev = Thread.current[:without_bootsnap_cache] || false
|
16
|
+
Thread.current[:without_bootsnap_cache] = true
|
17
|
+
yield
|
18
|
+
ensure
|
19
|
+
Thread.current[:without_bootsnap_cache] = prev
|
20
|
+
end
|
21
|
+
|
22
|
+
module ClassMethods
|
23
|
+
def autoload_paths=(o)
|
24
|
+
r = super
|
25
|
+
Bootsnap::LoadPathCache.autoload_paths_cache.reinitialize(o)
|
26
|
+
r
|
27
|
+
end
|
28
|
+
|
29
|
+
def search_for_file(path)
|
30
|
+
return super if Thread.current[:without_bootsnap_cache]
|
31
|
+
begin
|
32
|
+
Bootsnap::LoadPathCache.autoload_paths_cache.find(path)
|
33
|
+
rescue Bootsnap::LoadPathCache::ReturnFalse
|
34
|
+
nil # doesn't really apply here
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def autoloadable_module?(path_suffix)
|
39
|
+
Bootsnap::LoadPathCache.autoload_paths_cache.has_dir?(path_suffix)
|
40
|
+
end
|
41
|
+
|
42
|
+
def remove_constant(const)
|
43
|
+
CoreExt::ActiveSupport.without_bootsnap_cache { super }
|
44
|
+
end
|
45
|
+
|
46
|
+
# If we can't find a constant using the patched implementation of
|
47
|
+
# search_for_file, try again with the default implementation.
|
48
|
+
#
|
49
|
+
# These methods call search_for_file, and we want to modify its
|
50
|
+
# behaviour. The gymnastics here are a bit awkward, but it prevents
|
51
|
+
# 200+ lines of monkeypatches.
|
52
|
+
def load_missing_constant(from_mod, const_name)
|
53
|
+
CoreExt::ActiveSupport.with_bootsnap_fallback(NameError) { 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
|
+
CoreExt::ActiveSupport.with_bootsnap_fallback(LoadError) { super }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
module ActiveSupport
|
68
|
+
module Dependencies
|
69
|
+
class << self
|
70
|
+
prepend Bootsnap::LoadPathCache::CoreExt::ActiveSupport::ClassMethods
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,88 @@
|
|
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
|
+
alias_method :require_without_cache, :require
|
15
|
+
def require(path)
|
16
|
+
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)
|
20
|
+
end
|
21
|
+
rescue Bootsnap::LoadPathCache::ReturnFalse
|
22
|
+
return false
|
23
|
+
rescue Bootsnap::LoadPathCache::FallbackScan
|
24
|
+
require_without_cache(path)
|
25
|
+
end
|
26
|
+
|
27
|
+
alias_method :load_without_cache, :load
|
28
|
+
def load(path, wrap = false)
|
29
|
+
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)
|
38
|
+
end
|
39
|
+
rescue Bootsnap::LoadPathCache::ReturnFalse
|
40
|
+
return false
|
41
|
+
rescue Bootsnap::LoadPathCache::FallbackScan
|
42
|
+
load_without_cache(path, wrap)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class << Kernel
|
47
|
+
alias_method :require_without_cache, :require
|
48
|
+
def require(path)
|
49
|
+
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)
|
53
|
+
end
|
54
|
+
rescue Bootsnap::LoadPathCache::ReturnFalse
|
55
|
+
return false
|
56
|
+
rescue Bootsnap::LoadPathCache::FallbackScan
|
57
|
+
require_without_cache(path)
|
58
|
+
end
|
59
|
+
|
60
|
+
alias_method :load_without_cache, :load
|
61
|
+
def load(path, wrap = false)
|
62
|
+
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)
|
71
|
+
end
|
72
|
+
rescue Bootsnap::LoadPathCache::ReturnFalse
|
73
|
+
return false
|
74
|
+
rescue Bootsnap::LoadPathCache::FallbackScan
|
75
|
+
load_without_cache(path, wrap)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class Module
|
80
|
+
alias_method :autoload_without_cache, :autoload
|
81
|
+
def autoload(const, path)
|
82
|
+
autoload_without_cache(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
|
83
|
+
rescue Bootsnap::LoadPathCache::ReturnFalse
|
84
|
+
return false
|
85
|
+
rescue Bootsnap::LoadPathCache::FallbackScan
|
86
|
+
autoload_without_cache(const, path)
|
87
|
+
end
|
88
|
+
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 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,42 @@
|
|
1
|
+
require_relative '../load_path_cache'
|
2
|
+
|
3
|
+
module Bootsnap
|
4
|
+
module LoadPathCache
|
5
|
+
module PathScanner
|
6
|
+
REQUIRABLES_AND_DIRS = "/**/*{#{DOT_RB},#{DL_EXTENSIONS.join(',')},/}"
|
7
|
+
IS_DIR = %r{(.*)/\z}
|
8
|
+
NORMALIZE_NATIVE_EXTENSIONS = !DL_EXTENSIONS.include?(LoadPathCache::DOT_SO)
|
9
|
+
ALTERNATIVE_NATIVE_EXTENSIONS_PATTERN = /\.(o|bundle|dylib)\z/
|
10
|
+
BUNDLE_PATH = (Bundler.bundle_path.cleanpath.to_s << LoadPathCache::SLASH).freeze
|
11
|
+
|
12
|
+
def self.call(path)
|
13
|
+
path = path.to_s
|
14
|
+
|
15
|
+
relative_slice = (path.size + 1)..-1
|
16
|
+
# If the bundle path is a descendent of this path, we do additional
|
17
|
+
# checks to prevent recursing into the bundle path as we recurse
|
18
|
+
# through this path. We don't want to scan the bundle path because
|
19
|
+
# anything useful in it will be present on other load path items.
|
20
|
+
#
|
21
|
+
# This can happen if, for example, the user adds '.' to the load path,
|
22
|
+
# and the bundle path is '.bundle'.
|
23
|
+
contains_bundle_path = BUNDLE_PATH.start_with?(path)
|
24
|
+
|
25
|
+
dirs = []
|
26
|
+
requirables = []
|
27
|
+
|
28
|
+
Dir.glob(path + REQUIRABLES_AND_DIRS).each do |absolute_path|
|
29
|
+
next if contains_bundle_path && absolute_path.start_with?(BUNDLE_PATH)
|
30
|
+
relative_path = absolute_path.slice!(relative_slice)
|
31
|
+
|
32
|
+
if md = relative_path.match(IS_DIR)
|
33
|
+
dirs << md[1]
|
34
|
+
else
|
35
|
+
requirables << relative_path
|
36
|
+
end
|
37
|
+
end
|
38
|
+
[requirables, dirs]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require_relative '../explicit_require'
|
2
|
+
|
3
|
+
Bootsnap::ExplicitRequire.with_gems('msgpack') { require 'msgpack' }
|
4
|
+
Bootsnap::ExplicitRequire.from_rubylibdir('fileutils')
|
5
|
+
|
6
|
+
module Bootsnap
|
7
|
+
module LoadPathCache
|
8
|
+
class Store
|
9
|
+
NestedTransactionError = Class.new(StandardError)
|
10
|
+
SetOutsideTransactionNotAllowed = Class.new(StandardError)
|
11
|
+
|
12
|
+
def initialize(store_path)
|
13
|
+
@store_path = store_path
|
14
|
+
load_data
|
15
|
+
end
|
16
|
+
|
17
|
+
def get(key)
|
18
|
+
@data[key]
|
19
|
+
end
|
20
|
+
|
21
|
+
def fetch(key)
|
22
|
+
raise SetOutsideTransactionNotAllowed unless @in_txn
|
23
|
+
v = get(key)
|
24
|
+
unless v
|
25
|
+
@dirty = true
|
26
|
+
v = yield
|
27
|
+
@data[key] = v
|
28
|
+
end
|
29
|
+
v
|
30
|
+
end
|
31
|
+
|
32
|
+
def set(key, value)
|
33
|
+
raise SetOutsideTransactionNotAllowed unless @in_txn
|
34
|
+
if value != @data[key]
|
35
|
+
@dirty = true
|
36
|
+
@data[key] = value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def transaction
|
41
|
+
raise NestedTransactionError if @in_txn
|
42
|
+
@in_txn = true
|
43
|
+
yield
|
44
|
+
ensure
|
45
|
+
commit_transaction
|
46
|
+
@in_txn = false
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def commit_transaction
|
52
|
+
if @dirty
|
53
|
+
dump_data
|
54
|
+
@dirty = false
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def load_data
|
59
|
+
@data = begin
|
60
|
+
MessagePack.load(File.binread(@store_path))
|
61
|
+
# handle malformed data due to upgrade incompatability
|
62
|
+
rescue Errno::ENOENT, MessagePack::MalformedFormatError, MessagePack::UnknownExtTypeError, EOFError
|
63
|
+
{}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def dump_data
|
68
|
+
# Change contents atomically so other processes can't get invalid
|
69
|
+
# caches if they read at an inopportune time.
|
70
|
+
tmp = "#{@store_path}.#{(rand * 100000).to_i}.tmp"
|
71
|
+
FileUtils.mkpath(File.dirname(tmp))
|
72
|
+
File.binwrite(tmp, MessagePack.dump(@data))
|
73
|
+
FileUtils.mv(tmp, @store_path)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|