bootsnap-pr-184 1.3.1.pr.pre.184.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ #ifndef BOOTSNAP_H
2
+ #define BOOTSNAP_H 1
3
+
4
+ /* doesn't expose anything */
5
+
6
+ #endif /* BOOTSNAP_H */
@@ -0,0 +1,17 @@
1
+ require "mkmf"
2
+ $CFLAGS << ' -O3 '
3
+ $CFLAGS << ' -std=c99'
4
+
5
+ # ruby.h has some -Wpedantic fails in some cases
6
+ # (e.g. https://github.com/Shopify/bootsnap/issues/15)
7
+ unless ['0', '', nil].include?(ENV['BOOTSNAP_PEDANTIC'])
8
+ $CFLAGS << ' -Wall'
9
+ $CFLAGS << ' -Werror'
10
+ $CFLAGS << ' -Wextra'
11
+ $CFLAGS << ' -Wpedantic'
12
+
13
+ $CFLAGS << ' -Wno-unused-parameter' # VALUE self has to be there but we don't care what it is.
14
+ $CFLAGS << ' -Wno-keyword-macro' # hiding return
15
+ end
16
+
17
+ create_makefile("bootsnap/bootsnap")
@@ -0,0 +1,44 @@
1
+ require_relative 'bootsnap/version'
2
+ require_relative 'bootsnap/bundler'
3
+ require_relative 'bootsnap/load_path_cache'
4
+ require_relative 'bootsnap/compile_cache'
5
+
6
+ module Bootsnap
7
+ InvalidConfiguration = Class.new(StandardError)
8
+
9
+ def self.setup(
10
+ cache_dir:,
11
+ development_mode: true,
12
+ load_path_cache: true,
13
+ autoload_paths_cache: true,
14
+ disable_trace: false,
15
+ compile_cache_iseq: true,
16
+ compile_cache_yaml: true
17
+ )
18
+ if autoload_paths_cache && !load_path_cache
19
+ raise InvalidConfiguration, "feature 'autoload_paths_cache' depends on feature 'load_path_cache'"
20
+ end
21
+
22
+ setup_disable_trace if disable_trace
23
+
24
+ Bootsnap::LoadPathCache.setup(
25
+ cache_path: cache_dir + '/bootsnap-load-path-cache',
26
+ development_mode: development_mode,
27
+ active_support: autoload_paths_cache
28
+ ) if load_path_cache
29
+
30
+ Bootsnap::CompileCache.setup(
31
+ cache_dir: cache_dir + '/bootsnap-compile-cache',
32
+ iseq: compile_cache_iseq,
33
+ yaml: compile_cache_yaml
34
+ )
35
+ end
36
+
37
+ def self.setup_disable_trace
38
+ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.5.0')
39
+ warn("from #{caller_locations(1, 1)[0]}: The 'disable_trace' method is not allowed with this Ruby version. current: #{RUBY_VERSION}, allowed version: < 2.5.0")
40
+ else
41
+ RubyVM::InstructionSequence.compile_option = { trace_instruction: false }
42
+ end
43
+ end
44
+ end
@@ -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
@@ -0,0 +1,15 @@
1
+ module Bootsnap
2
+ module CompileCache
3
+ def self.setup(cache_dir:, iseq:, yaml:)
4
+ if iseq
5
+ require_relative 'compile_cache/iseq'
6
+ Bootsnap::CompileCache::ISeq.install!(cache_dir)
7
+ end
8
+
9
+ if yaml
10
+ require_relative 'compile_cache/yaml'
11
+ Bootsnap::CompileCache::YAML.install!(cache_dir)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,70 @@
1
+ require 'bootsnap/bootsnap'
2
+ require 'zlib'
3
+
4
+ module Bootsnap
5
+ module CompileCache
6
+ module ISeq
7
+ class << self
8
+ attr_accessor :cache_dir
9
+ end
10
+
11
+ def self.input_to_storage(_, path)
12
+ RubyVM::InstructionSequence.compile_file(path).to_binary
13
+ rescue SyntaxError
14
+ raise Uncompilable, 'syntax error'
15
+ end
16
+
17
+ def self.storage_to_output(binary)
18
+ RubyVM::InstructionSequence.load_from_binary(binary)
19
+ rescue RuntimeError => e
20
+ if e.message == 'broken binary format'
21
+ STDERR.puts "[Bootsnap::CompileCache] warning: rejecting broken binary"
22
+ return nil
23
+ else
24
+ raise
25
+ end
26
+ end
27
+
28
+ def self.input_to_output(_)
29
+ nil # ruby handles this
30
+ end
31
+
32
+ module InstructionSequenceMixin
33
+ def load_iseq(path)
34
+ # Having coverage enabled prevents iseq dumping/loading.
35
+ return nil if defined?(Coverage) && Bootsnap::CompileCache::Native.coverage_running?
36
+
37
+ Bootsnap::CompileCache::Native.fetch(
38
+ Bootsnap::CompileCache::ISeq.cache_dir,
39
+ path.to_s,
40
+ Bootsnap::CompileCache::ISeq
41
+ )
42
+ rescue RuntimeError => e
43
+ if e.message =~ /unmatched platform/
44
+ puts "unmatched platform for file #{path}"
45
+ end
46
+ raise
47
+ end
48
+
49
+ def compile_option=(hash)
50
+ super(hash)
51
+ Bootsnap::CompileCache::ISeq.compile_option_updated
52
+ end
53
+ end
54
+
55
+ def self.compile_option_updated
56
+ option = RubyVM::InstructionSequence.compile_option
57
+ crc = Zlib.crc32(option.inspect)
58
+ Bootsnap::CompileCache::Native.compile_option_crc32 = crc
59
+ end
60
+
61
+ def self.install!(cache_dir)
62
+ Bootsnap::CompileCache::ISeq.cache_dir = cache_dir
63
+ Bootsnap::CompileCache::ISeq.compile_option_updated
64
+ class << RubyVM::InstructionSequence
65
+ prepend InstructionSequenceMixin
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,58 @@
1
+ require 'bootsnap/bootsnap'
2
+
3
+ module Bootsnap
4
+ module CompileCache
5
+ module YAML
6
+ class << self
7
+ attr_accessor :msgpack_factory
8
+ end
9
+
10
+ def self.input_to_storage(contents, _)
11
+ raise Uncompilable if contents.index("!ruby/object")
12
+ obj = ::YAML.load(contents)
13
+ msgpack_factory.packer.write(obj).to_s
14
+ rescue NoMethodError, RangeError
15
+ # if the object included things that we can't serialize, fall back to
16
+ # Marshal. It's a bit slower, but can encode anything yaml can.
17
+ # NoMethodError is unexpected types; RangeError is Bignums
18
+ return Marshal.dump(obj)
19
+ end
20
+
21
+ def self.storage_to_output(data)
22
+ # This could have a meaning in messagepack, and we're being a little lazy
23
+ # about it. -- but a leading 0x04 would indicate the contents of the YAML
24
+ # is a positive integer, which is rare, to say the least.
25
+ if data[0] == 0x04.chr && data[1] == 0x08.chr
26
+ Marshal.load(data)
27
+ else
28
+ msgpack_factory.unpacker.feed(data).read
29
+ end
30
+ end
31
+
32
+ def self.input_to_output(data)
33
+ ::YAML.load(data)
34
+ end
35
+
36
+ def self.install!(cache_dir)
37
+ require 'yaml'
38
+ require 'msgpack'
39
+
40
+ # MessagePack serializes symbols as strings by default.
41
+ # We want them to roundtrip cleanly, so we use a custom factory.
42
+ # see: https://github.com/msgpack/msgpack-ruby/pull/122
43
+ factory = MessagePack::Factory.new
44
+ factory.register_type(0x00, Symbol)
45
+ Bootsnap::CompileCache::YAML.msgpack_factory = factory
46
+
47
+ klass = class << ::YAML; self; end
48
+ klass.send(:define_method, :load_file) do |path|
49
+ Bootsnap::CompileCache::Native.fetch(
50
+ cache_dir,
51
+ path.to_s,
52
+ Bootsnap::CompileCache::YAML
53
+ )
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,49 @@
1
+ module Bootsnap
2
+ module ExplicitRequire
3
+ ARCHDIR = RbConfig::CONFIG['archdir']
4
+ RUBYLIBDIR = RbConfig::CONFIG['rubylibdir']
5
+ DLEXT = RbConfig::CONFIG['DLEXT']
6
+
7
+ def self.from_self(feature)
8
+ require_relative "../#{feature}"
9
+ end
10
+
11
+ def self.from_rubylibdir(feature)
12
+ require(File.join(RUBYLIBDIR, "#{feature}.rb"))
13
+ end
14
+
15
+ def self.from_archdir(feature)
16
+ require(File.join(ARCHDIR, "#{feature}.#{DLEXT}"))
17
+ end
18
+
19
+ # Given a set of gems, run a block with the LOAD_PATH narrowed to include
20
+ # only core ruby source paths and these gems -- that is, roughly,
21
+ # temporarily remove all gems not listed in this call from the LOAD_PATH.
22
+ #
23
+ # This is useful before bootsnap is fully-initialized to load gems that it
24
+ # depends on, without forcing full LOAD_PATH traversals.
25
+ def self.with_gems(*gems)
26
+ orig = $LOAD_PATH.dup
27
+ $LOAD_PATH.clear
28
+ gems.each do |gem|
29
+ pat = %r{
30
+ /
31
+ (gems|extensions/[^/]+/[^/]+) # "gems" or "extensions/x64_64-darwin16/2.3.0"
32
+ /
33
+ #{Regexp.escape(gem)}-(\h{12}|(\d+\.)) # msgpack-1.2.3 or msgpack-1234567890ab
34
+ }x
35
+ $LOAD_PATH.concat(orig.grep(pat))
36
+ end
37
+ $LOAD_PATH << ARCHDIR
38
+ $LOAD_PATH << RUBYLIBDIR
39
+ begin
40
+ yield
41
+ rescue LoadError
42
+ $LOAD_PATH.replace(orig)
43
+ yield
44
+ end
45
+ ensure
46
+ $LOAD_PATH.replace(orig)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,58 @@
1
+ module Bootsnap
2
+ module LoadPathCache
3
+ ReturnFalse = Class.new(StandardError)
4
+ FallbackScan = Class.new(StandardError)
5
+
6
+ DOT_RB = '.rb'
7
+ DOT_SO = '.so'
8
+ SLASH = '/'
9
+
10
+ DL_EXTENSIONS = ::RbConfig::CONFIG
11
+ .values_at('DLEXT', 'DLEXT2')
12
+ .reject { |ext| !ext || ext.empty? }
13
+ .map { |ext| ".#{ext}" }
14
+ .freeze
15
+ DLEXT = DL_EXTENSIONS[0]
16
+ # This is nil on linux and darwin, but I think it's '.o' on some other
17
+ # platform. I'm not really sure which, but it seems better to replicate
18
+ # ruby's semantics as faithfully as possible.
19
+ DLEXT2 = DL_EXTENSIONS[1]
20
+
21
+ CACHED_EXTENSIONS = DLEXT2 ? [DOT_RB, DLEXT, DLEXT2] : [DOT_RB, DLEXT]
22
+
23
+ class << self
24
+ attr_reader :load_path_cache, :autoload_paths_cache,
25
+ :loaded_features_index, :realpath_cache
26
+
27
+ def setup(cache_path:, development_mode:, active_support: true)
28
+ store = Store.new(cache_path)
29
+
30
+ @loaded_features_index = LoadedFeaturesIndex.new
31
+ @realpath_cache = RealpathCache.new
32
+
33
+ @load_path_cache = Cache.new(store, $LOAD_PATH, development_mode: development_mode)
34
+ require_relative 'load_path_cache/core_ext/kernel_require'
35
+
36
+ if active_support
37
+ # this should happen after setting up the initial cache because it
38
+ # loads a lot of code. It's better to do after +require+ is optimized.
39
+ require 'active_support/dependencies'
40
+ @autoload_paths_cache = Cache.new(
41
+ store,
42
+ ::ActiveSupport::Dependencies.autoload_paths,
43
+ development_mode: development_mode
44
+ )
45
+ require_relative 'load_path_cache/core_ext/active_support'
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ require_relative 'load_path_cache/path_scanner'
53
+ require_relative 'load_path_cache/path'
54
+ require_relative 'load_path_cache/cache'
55
+ require_relative 'load_path_cache/store'
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'
@@ -0,0 +1,181 @@
1
+ require_relative '../explicit_require'
2
+
3
+ module Bootsnap
4
+ module LoadPathCache
5
+ class Cache
6
+ AGE_THRESHOLD = 30 # seconds
7
+
8
+ def initialize(store, path_obj, development_mode: false)
9
+ @development_mode = development_mode
10
+ @store = store
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
+ reinitialize
15
+ end
16
+
17
+ # Does this directory exist as a child of one of the path items?
18
+ # e.g. given "/a/b/c/d" exists, and the path is ["/a/b"], has_dir?("c/d")
19
+ # is true.
20
+ def has_dir?(dir)
21
+ reinitialize if stale?
22
+ @mutex.synchronize { @dirs[dir] }
23
+ end
24
+
25
+ # { 'enumerator' => nil, 'enumerator.so' => nil, ... }
26
+ BUILTIN_FEATURES = $LOADED_FEATURES.each_with_object({}) do |feat, features|
27
+ # Builtin features are of the form 'enumerator.so'.
28
+ # All others include paths.
29
+ next unless feat.size < 20 && !feat.include?('/')
30
+
31
+ base = File.basename(feat, '.*') # enumerator.so -> enumerator
32
+ ext = File.extname(feat) # .so
33
+
34
+ features[feat] = nil # enumerator.so
35
+ features[base] = nil # enumerator
36
+
37
+ next unless [DOT_SO, *DL_EXTENSIONS].include?(ext)
38
+ DL_EXTENSIONS.each do |dl_ext|
39
+ features["#{base}#{dl_ext}"] = nil # enumerator.bundle
40
+ end
41
+ end.freeze
42
+
43
+ # Try to resolve this feature to an absolute path without traversing the
44
+ # loadpath.
45
+ def find(feature)
46
+ reinitialize if (@has_relative_paths && dir_changed?) || stale?
47
+ feature = feature.to_s
48
+ return feature if absolute_path?(feature)
49
+ return File.expand_path(feature) if feature.start_with?('./')
50
+ @mutex.synchronize do
51
+ x = search_index(feature)
52
+ return x if x
53
+
54
+ # Ruby has some built-in features that require lies about.
55
+ # For example, 'enumerator' is built in. If you require it, ruby
56
+ # returns false as if it were already loaded; however, there is no
57
+ # file to find on disk. We've pre-built a list of these, and we
58
+ # return false if any of them is loaded.
59
+ raise LoadPathCache::ReturnFalse if BUILTIN_FEATURES.key?(feature)
60
+
61
+ # The feature wasn't found on our preliminary search through the index.
62
+ # We resolve this differently depending on what the extension was.
63
+ case File.extname(feature)
64
+ # If the extension was one of the ones we explicitly cache (.rb and the
65
+ # native dynamic extension, e.g. .bundle or .so), we know it was a
66
+ # failure and there's nothing more we can do to find the file.
67
+ # no extension, .rb, (.bundle or .so)
68
+ when '', *CACHED_EXTENSIONS # rubocop:disable Performance/CaseWhenSplat
69
+ nil
70
+ # Ruby allows specifying native extensions as '.so' even when DLEXT
71
+ # is '.bundle'. This is where we handle that case.
72
+ when DOT_SO
73
+ x = search_index(feature[0..-4] + DLEXT)
74
+ return x if x
75
+ if DLEXT2
76
+ search_index(feature[0..-4] + DLEXT2)
77
+ end
78
+ else
79
+ # other, unknown extension. For example, `.rake`. Since we haven't
80
+ # cached these, we legitimately need to run the load path search.
81
+ raise LoadPathCache::FallbackScan
82
+ end
83
+ end
84
+ end
85
+
86
+ if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
87
+ def absolute_path?(path)
88
+ path[1] == ':'
89
+ end
90
+ else
91
+ def absolute_path?(path)
92
+ path.start_with?(SLASH)
93
+ end
94
+ end
95
+
96
+ def unshift_paths(sender, *paths)
97
+ return unless sender == @path_obj
98
+ @mutex.synchronize { unshift_paths_locked(*paths) }
99
+ end
100
+
101
+ def push_paths(sender, *paths)
102
+ return unless sender == @path_obj
103
+ @mutex.synchronize { push_paths_locked(*paths) }
104
+ end
105
+
106
+ def reinitialize(path_obj = @path_obj)
107
+ @mutex.synchronize do
108
+ @path_obj = path_obj
109
+ ChangeObserver.register(self, @path_obj)
110
+ @index = {}
111
+ @dirs = Hash.new(false)
112
+ @generated_at = now
113
+ push_paths_locked(*@path_obj)
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ def dir_changed?
120
+ @prev_dir ||= Dir.pwd
121
+ if @prev_dir == Dir.pwd
122
+ false
123
+ else
124
+ @prev_dir = Dir.pwd
125
+ true
126
+ end
127
+ end
128
+
129
+ def push_paths_locked(*paths)
130
+ @store.transaction do
131
+ paths.map(&:to_s).each do |path|
132
+ p = Path.new(path)
133
+ @has_relative_paths = true if p.relative?
134
+ next if p.non_directory?
135
+ entries, dirs = p.entries_and_dirs(@store)
136
+ # push -> low precedence -> set only if unset
137
+ dirs.each { |dir| @dirs[dir] ||= true }
138
+ entries.each { |rel| @index[rel] ||= p.expanded_path }
139
+ end
140
+ end
141
+ end
142
+
143
+ def unshift_paths_locked(*paths)
144
+ @store.transaction do
145
+ paths.map(&:to_s).reverse_each do |path|
146
+ p = Path.new(path)
147
+ next if p.non_directory?
148
+ entries, dirs = p.entries_and_dirs(@store)
149
+ # unshift -> high precedence -> unconditional set
150
+ dirs.each { |dir| @dirs[dir] = true }
151
+ entries.each { |rel| @index[rel] = p.expanded_path }
152
+ end
153
+ end
154
+ end
155
+
156
+ def stale?
157
+ @development_mode && @generated_at + AGE_THRESHOLD < now
158
+ end
159
+
160
+ def now
161
+ Process.clock_gettime(Process::CLOCK_MONOTONIC).to_i
162
+ end
163
+
164
+ if DLEXT2
165
+ def search_index(f)
166
+ try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f + DLEXT2) || try_index(f)
167
+ end
168
+ else
169
+ def search_index(f)
170
+ try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f)
171
+ end
172
+ end
173
+
174
+ def try_index(f)
175
+ if p = @index[f]
176
+ p + '/' + f
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end