bootsnap 1.1.0-java
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 +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
|